学习后端这么久,认证机制无疑是非常重要的一个部分,而且缺一不可,你知道哪些常见的认证机制呢?它们实现的过程和优缺点又有哪些呢?
🤓🤓这里附上一篇博文,写的还不错,在这里感谢作者:😗😗
认证机制HTTP Basic Auth、Cookie Auth、OAuth、TokenAuth
对于这么多不同的认证方式,现在来说使用最多的应该就是基于JWT-token那一套的认证方案,最重要的原因就是token支持跨域访问,而cookie是不允许跨域访问的,在像现在最常使用的架构模式就是前后端分离这种模式,那么这种模式不可避免的就是要解决前后端跨域问题,所以会更经常去使用token那一套认证机制解决方案😗
本文较长,后面有JWT完整测试类,拿来即可使用😏😏😏
这篇博文里对jwt的使用讲述的很清楚,这里也谢谢作者大大:
JJWT使用详解
⭐⭐⭐最后,附上JWT官网链接:
JWT官网
下面的内容token和令牌指代的是同一个东西🐸并且不了解base64加密的朋友可以先去了解一下
文章目录
1.什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
2.JWT的组成
JWT由三部分构成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
完整的jwt长啥样?
xxxxx.yyyyy.zzzzz
其实就是一个以.分开来的字符串,总共有2个.所以它共有三个部分
2.1 header头部
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256或者RSA之类的
完整头部(未加密)
{
'typ': 'JWT',
'alg': 'HS256'
}
加密:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2.2 payload(有效载荷)
令牌的第二部分是有效载荷,其中包含声明。声明是关于实体(通常是用户)和附加数据的声明。索赔有三种类型:注册声明、公开声明和私人声明。
相当于这里就会扯到三个应用场景🤔🤔
2.2.1 注册声明
注册声明:这些是一组预定义的声明,不是强制性的,但建议使用,以提供一组有用的、可互操作的声明。其中一些是:iss(发布者)、exp(过期时间)、sub(主题)、aud(受众)等。
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
使用工具类:
String jws = Jwts.builder()
.setIssuer("me")
.setSubject("Bob")
.setAudience("you")
.setExpiration(expiration) //a java.util.Date
.setNotBefore(notBefore) //a java.util.Date
.setIssuedAt(new Date()) // for example, now
.setId(UUID.randomUUID()) //just an example id
/// ... etc ...
还有一种是使用自定义的,在下面的实战中有演示
2.2.2 公开声明
公开声明:使用JWT的人可以随意定义这些声明。但为了避免冲突,它们应该在IANA JSON Web令牌注册表中定义,或者定义为包含抗冲突命名空间的URI。一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
2.2.3私有声明
私有声明是提供者和消费者所共同定义的声明,相当于共享信息,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
2.3example
完整的载荷:
{
"sub": "1234567890",
"name": "xmonster",
"admin": true
}
对其进行base64加密,即可得到令牌的第二部分
3.签证(signature)
这应该是令牌里最重要也是最复杂的部分了
要创建签名部分,您必须获取编码的头(header)、编码的有效载荷、secret、头中指定的算法,才能对其进行签名。
这里的secret,指的是“加盐”🧂🧂🧂🧂
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串(头部在前),然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,所以需要保护好。
举个例子,如果你使用的是HMAC SHA256加密算法:(前面说了是在header里指明算法)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
在JWT官网上,有一个验证token的功能:
下面的实战再演示怎么玩
3.1 签名过程、验证
由于签名使用的秘钥保存在服务器,这样一来,客户端就无法伪造出签名,因为它拿不到秘钥。
换句话说,之所以说无法伪造jwt,就是因为第三部分的签名过程的存在。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg字段指明了我们的加密算法了。
如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。
签名验证的过程非常简单:给我token,再加上我们服务器存储的secret,两个通过算法一同操作之后又会得到新的token值,如果这个token和你传过来的一摸一样,证明这个token可以用,没有被修改过,否则就是被人修改过或者过期了。😼😼
4.实战
实战主要测试下⾯功能:
(1)⽣成token
(2)解析token
(3)测试token是否过期
这里的测试使用的场景比较普遍,是对jwt较为完整的体验和工具类
1.导入依赖
<!-- jwt start-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- jwt end -->
JwtTest.java
这里使用的是自定义声明,
package com.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @program: thread
* @author: xmonster_大魔王
* @create: 2022-10-13 17:26
**/
public class JwtTest {
/**
* 有效期:5s
*/
private static final long EXPIRE_TIME = 20*1000;
/**
* 秘钥:主要就是验证JWT签名功能
* 也就是secret~
* 使用足以SecretKey与JWT HMAC-SHA 算法一起使用的强度
* 可以使用Keys.secretKeyFor方法,这个方法接收一个算法参数
* 如果想要保存此代码的secretKey 可以使用Base64(或者Base64URL)进行编码
*
* */
static Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
/**
* 秘钥的编码值(哎呀就是给你看看这个秘钥长啥样,他可不是随便能看的,要通过Base64编码之后才能看的)
*/
static String secretKey = Encoders.BASE64.encode(key.getEncoded());
/**
* 创建时间
*/
private static final String CREATED = "created";
private static final String USERNAME = "username";
private static final String EXPIRE = "expire";
private static final String TOKEN_KEY = "token_key";
/**
* 生成声明数据claims, k-v形式
* @param username
* @return
*/
public static Map<String, Object> generateClaim(String username){
Map claims = new HashMap<>();
claims.put(USERNAME,username);
claims.put(CREATED,new Date());
claims.put(TOKEN_KEY, UUID.randomUUID().toString());
return claims;
}
/**
* 根据声明数据生成jwt token
* @param claims
* @return
*/
public static String generateToken(Map<String, Object> claims){
//定义过期时间: 当前时间过5s即过期
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
System.out.println("secretKey=="+secretKey);
//存放数据声明和过期时间
return Jwts.builder().
//载荷payload
setClaims(claims).
//token过期时间
setExpiration(expirationDate).
//秘钥
signWith(key,SignatureAlgorithm.HS512).
compact();
}
/**
* 从令牌中获取数据声明
* @param token
* @return
*/
private static Claims getClaimsFromToken(String token){
Claims claims;
try {
claims = Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody();
//System.out.println("获得的claims为:"+claims);
}catch (Exception e){
claims = null;
}
return claims;
}
/**
* 判断令牌是否过期
* @param token
* @return
*/
public static Boolean isTokenExpired(String token){
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return System.currentTimeMillis() - expiration.getTime() >= 0;
}catch (Exception e){
return true;
}
}
/**
* 从令牌中获取用户名
* @param token
* @return
*/
public static String getUsernameFromToken(String token){
String username = null;
try {
Claims claims = getClaimsFromToken(token);
username = (String) claims.get(USERNAME);
}catch (Exception e){
username = null;
}
return username;
}
/**
* 刷新令牌
* @param token
* @return
*/
public static String refreshToken(String token){
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("CREATED", new Date());
refreshedToken = generateToken(claims);
}catch (Exception e){
refreshedToken = null;
}
return refreshedToken;
}
/**
* 根据用户名生成token
* @param username
* @return
*/
public static String generateTokenFromUsername(String username){
return generateToken(generateClaim(username));
}
public static void verifyToken(String token){
try {
System.out.println(
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody()
); //true
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
String username = "admin";
//生成token
String token = generateTokenFromUsername(username);
System.out.println("1.生成的jwt token为:"+token);
Claims claims = getClaimsFromToken(token);
System.out.println("2.生成的claims为:"+claims);
Boolean tokenExpired = isTokenExpired(token);
String isExpired = tokenExpired ? "过期了" : "未过期";
System.out.println("3.此时token过期了吗:"+ isExpired);
String username1 = getUsernameFromToken(token);
System.out.println("4.通过token得到的username为:"+username1);
}
}
这里在jwt官网演示效果:
首先,将token放上去:
来验证一下~,将上述的解密过的secretKey放到verify上
在放之前,我们先来看看token的第三部分长啥样,前两部分默认不变:
这里蓝色这一段就是第三部分,比较长哈,将secretKey放上去,下面的打勾
验证成功~