一、简介
JWT将用户的一些信息存储在客户端,访问后台时会带着JWT,服务器要对这个JWT进行检验。
由于signKey是存放在服务器端的,所以比较安全只要JWT被篡改就会立刻发现。
JWT认证的优势
1.简洁:JWT Token数据量小,传输速度也很快。
2.因为JWT Token是以JSON加密形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
3.不需要在服务端保存会话信息,也就是说不依赖于cookie和session,所以没有了传统session认证的弊端,特别适用于分布式微服务。
4.单点登录友好:使用Session进行身份认证的话,由于cookie无法跨域,难以实现单点登录。但是,使用token进行认证的话, token可以被保存在客户端的任意位置的内存中,不一定是cookie,所以不依赖cookie,不会存在这些问题。
5.适合移动端应用:使用Session进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。
JWT的缺点
1.安全性没法保证,所以 jwt 里不能存储敏感数据。jwt 的 载荷(payload) 没加密,只是用 Base64 编码。
2.无法中途废弃。因为一旦签发了一个 jwt,在到期之前始终都是有效的,如果用户信息发生更新了,只能等旧的 jwt 过期后重新签发新的 jwt。
3.续签问题。当签发的 jwt 保存在客户端,客户端一直在操作页面,按道理应该一直为客户端续长有效时间,要引入 Redis 解决。
1、session认证、 token认证
1.session认证
服务器保存一份用户信息在session中,认证成功后返回 cookie 值传递给浏览器,下次请求时就可以带上 cookie 值,服务器可识别是哪个用户发送的请求,是否已认证,是否登录过期等等。
2.token认证
是一串随机的字符(比如UUID),value 一般是用户ID,并且设置一个过期时间。 请求时请求头里封装token,后端接收到token 则根据 token 查下 redis,如果 token 不存在则跳到登录界面让用户重新登录,登录成功后返回一个 token 值给客户端。
2、JWT的数据结构
标头(Header)、有效载荷(Payload)、签名(Signature)。
在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。
JWTString = Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
1.标头(Header)
通常由两部分组成: Token的类型(即JWT)和所使用的签名算法(如 HMAC SHA256或 RSA),如:{ “alg”: “HS256”, “typ”: “JWT”}
将由base64进行加密(该加密是可以对称解密的),用于构成 JWT 的第一部分,如: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2.有效载荷(Payload)
系统业务需要的数据,主要包含几个部分:
iss(签发者),exp(过期时间戳), sub(面向的用户), aud(接收方), iat(签发时间),
大致样式:{ “sub”: “1234567890”, “name”: “John Doe”, “admin”: true},
这部也会通过base64进行加密,最终形成JWT的第二部分,如:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
3.签名(Signature)
把:标头(header)、编码的有效载荷(payload)、秘钥(secret)、标头中指定的算法,并对其进行签名。
公式为:编码后的header、编码后的payload、secret进行加密HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)。
最终生成JWT的第三部分SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和验证,是服务端的私钥,在任何场景都不应该流露出去。
3、JWT的种类
nonsecure JWT: 未经过签名,不安全的JWT
JWS: 经过签名的JWT
JWE: payload部分经过加密的JWT
4、JWT的签名算法
创建签名,保证jwt不被随意篡改,通常用的JWT一般都是JWS
HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)
加密的算法
对称加密:secretKey 指加密密钥,可生成签名与验签
非对称加密:secretKey 指私钥,只用来生成签名,不能用来验签(验签用的是公钥)
JWT的密钥或者密钥对,一般统一称为JSON Web Key,就是JWK。
二、使用
1、JWT的流程(生成、校验)
1.生成
1、设置加密方式,payload 和 signingkey。
2、设置加密方式为 header,并进行base编码。
3、设置claims信息为payload,并进行base64编码。
4、对header和payload用"."拼接成jwt,使用signingkey按照加密方式进行加密,生成sign。
5、对Jwt和sign用"."生成最终的jwt。
2.校验
1、设置jwt和signingkey。
2、按"."对jwt分成三部分,即:header、payload、sign。
3、取第一部分进行base64解码,获取加密方式。
4、取第二部分进行base64解码,获取业务参数,即payload。
5、使用加密方式和signingkey创建校验器,对header+payload进行加密,并与sign(即第三部分)进行对比。
6、取出payload的有效期进行校验,是否过期。
7、通过校验,返回claims信息。
2、java-jwt 版本使用
1.引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.0.0</version>
</dependency>
2.代码
public class JWTUtils {
// 过期时间(分钟)
private static final int EXPIRE_TIME = 30;
// 签名密钥(对称加密)
private static final String SECRET = "!DAR$";
// 非对称
private static final String RSA_PRIVATE_KEY = "jun_south";
// 非对称
private static final String RSA_PUBLIC_KEY = "south_jun";
/**
* 获取Token(对称)
*/
public static String getToken(Map<String,String> payload){
// 指定token过期时间为1天
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, EXPIRE_TIME);
JWTCreator.Builder builder = JWT.create();
// 构建payload
payload.forEach((k,v) -> builder.withClaim(k,v));
// 指定过期时间和签名算法
String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SECRET));
return token;
}
/**
* 解析 token (对称)
*/
public static DecodedJWT decode(String token){
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT;
}
/**
* 获取Token(非对称)
* @param payload token携带的信息
* @return token字符串
*/
public static String getTokenRsa(Map<String,String> payload){
// 指定token过期时间为7天
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, EXPIRE_TIME);
JWTCreator.Builder builder = JWT.create();
// 构建payload
payload.forEach((k,v) -> builder.withClaim(k,v));
// 利用hutool创建RSA
RSA rsa = new RSA(RSA_PRIVATE_KEY, null);
// 获取私钥
RSAPrivateKey privateKey = (RSAPrivateKey) rsa.getPrivateKey();
// 签名时传入私钥
String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.RSA256(null, privateKey));
return token;
}
/**
* 解析token(非对称)
* @param token token字符串
* @return 解析后的token
*/
public static DecodedJWT decodeRsa(String token){
// 利用hutool创建RSA
RSA rsa = new RSA(null, RSA_PUBLIC_KEY);
// 获取RSA公钥
RSAPublicKey publicKey = (RSAPublicKey) rsa.getPublicKey();
// 验签时传入公钥
JWTVerifier jwtVerifier = JWT.require(Algorithm.RSA256(publicKey, null)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT;
}
}
3、jjwt-root 版本使用
1.引入依赖
<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>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2.代码
public class JwtUtils {
// token时效:30 分钟
public static final long EXPIRE = 1000 * 60 * 30 ;
// 签名哈希的密钥,对于不同的加密算法来说含义不同
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHOsdadasdasfdssfeweee";
private static final String RSA_PRIVATE_KEY = "jun-south";
private static final String RSA_PUBLIC_KEY = "south-jun";
/**
* 根据用户id和名称生成token(对称)
*/
public static String getJwtToken(String id, String nickname){
String JwtToken = Jwts.builder()
.setSubject("jun-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
// 传入Key对象
.signWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
.compact();
return JwtToken;
}
/**
* 解析token(对称)
*/
public static Jws<Claims> decode(String jwtToken) {
// 传入Key对象
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(APP_SECRET.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(jwtToken);
return claimsJws;
}
/**
* 根据用户id和名称生成token(非对称)
*/
public static String getJwtTokenRsa(String id, String nickname){
// 利用hutool创建RSA
RSA rsa = new RSA(RSA_PRIVATE_KEY, null);
RSAPrivateKey privateKey = (RSAPrivateKey) rsa.getPrivateKey();
String JwtToken = Jwts.builder()
.setSubject("jun-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
// 签名指定私钥
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
return JwtToken;
}
/**
* 解析token(非对称)
*/
public static Jws<Claims> decodeRsa(String jwtToken) {
RSA rsa = new RSA(null, RSA_PUBLIC_KEY);
RSAPublicKey publicKey = (RSAPublicKey) rsa.getPublicKey();
// 验签指定公钥
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(jwtToken);
return claimsJws;
}
}
4、过滤器里的代码
String JWT = request.getHeader("Authorization");
try {
// 1.校验JWT字符串
DecodedJWT decodedJWT = JWTUtils.decode(JWT);
// 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
...
return true;
}catch (SignatureVerificationException e){
System.out.println("无效签名");
e.printStackTrace();
}catch (TokenExpiredException e){
System.out.println("token已经过期");
e.printStackTrace();
}catch (AlgorithmMismatchException e){
System.out.println("算法不一致");
e.printStackTrace();
}catch (Exception e){
System.out.println("token无效");
e.printStackTrace();
}