JWT是什么?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object。
This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
- 基于
JSON
格式用于网络传输的令牌。- 紧凑的
Claims
声明格式。Claim
有索赔、声称、要求或者权利要求的含义。
在JWT之前
在JWT技术大规模应用之前,使用cookie+session技术做交互验证、存储由来已久。尤其在单体结构中更加突出(即使现在好多单体架构的项目依然使用cookie+session的经典组合)。
cookie+session技术之所以产生是由于HTTP协议在创建之初为加快服务器的响应,采用“无记忆性“来提高服务器的响应。但是会产生一系列的问题。例如:服务器如何确定客户端已经连接,已经登录了系统多长时间,是否允许该用户继续访问,该用户是否任然活跃,该用户如何找到其私有信息等问题等。
为此cookie+session技术很好的补充了http协议中“无记忆性“的问题。解决方案如图。
- 服务器产生存储session对象,每个session对象有唯一的ID,有存活时间,可以记录数据。
- 发起请求后,服务器将session的id写入客户端(浏览器),客户端通过cookie记录session的id
- 客户端每一次发起请求需要携带session的id。
- 服务器根据session的id查询指定session是否存在,然后交互。
cookie-session模型很好的补充了http协议的无记忆性问题,但是如果在分布式环境下,如果每个服务器产生一个session或同步已有的session很困难,如果网络,服务器等由于延迟保证数据一致性比较困难。
为此我们需要一种轻量级,在分布式情况下依然能使用的技术尤为重要,JWT技术由此产生。
jwt的结构
xxx.yyy.pppp
Header 部分是一个 JSON 对象,描述 JWT 的元数据。
加密算法
-
HSA256
SHA(Secure Hash Algorithm,安全散列算法)是一个密码散列函数家族,由美国国家安全局(NSA)设计,并由美国国家标准与技术研究院(NIST)发布,是美国的政府标准。
- 无论输入多长,都输出
64
个字符,共32字节(byte),256位(bit) - 输出只包含数字
0
`9`和字母`A`F
,大小写不敏感
- 无论输入多长,都输出
-
RSA256
由美国麻 省理工 学院三 位学者 Riv est、Sh amir 及Adleman 研 究发 展出 一套 可实 际使用 的公 开金 钥密码系 统,那 就是RSA(Rivest-Shamir-Adleman)密码系统。
RS256(带有SHA-256的 RSA 签名)是一种非对称算法,它使用公钥/私钥对:身份提供者拥有用于生成签名的私钥(秘密)密钥,而 JWT 的消费者获得公钥验证签名。由于与私钥相反,公钥不需要保持安全,因此大多数身份提供者都可以让消费者轻松获取和使用(通常通过元数据 URL)。
Base64
Base64是一种基于64个可打印字符来表示二进制数据的表示方法。Base64不是加密算法,只是一种编码方式,可以用Base64来简单的“加密”来保护某些数据,所以每 6 个比特为一个单元,对应某个可打印字符。
“hello” 编码
h–01101000 e --01100101
01101000
String str="hello";byte[] encode = Base64.getEncoder().encode(str.getBytes());String s = new String(encode);System.out.println("base64编码: "+s);byte[] decode = Base64.getDecoder().decode(s);String x = new String(decode);System.out.println("解析:" + x);
Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
Payload 也使用BASE64编码
Registered claims
这些是一组预先定义的声明,它们不是强制性的,但推荐使用,以提供一组有用的,可互操作的声明。 其中一些是:
iss
(发行者),exp
(到期时间),sub
(主题),aud
(受众)等
序号 | 名称 | 解释 |
---|---|---|
1 | iss (issuer) | 签发人 |
2 | exp (expiration time) | 过期时间 |
3 | sub (subject) | 主题 |
4 | aud (audience) | 受众(接收jwt的一方) |
5 | nbf (Not Before) | 生效时间 |
6 | iat (Issued At) | 签发时间 |
7 | jti (JWT ID) | jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击 |
Public claims
这些可以由使用JWT的人员随意定义。
Private claims
这些是为了同意使用它们但是既没有登记,也没有公开声明的各方之间共享信息,而创建的定制声明。
Payload案例
{"sub": "456781234","name": "Mr.zhang","admin": true
}
Signature
The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.
Signature 部分是对前两部分的签名,防止数据篡改。
https://jwt.io/
Signature部分由编码后的Header、Payload和自定义的秘钥使用Header中指定的算法(HSA256)进行加密签名;
JWT构成
jwt使用
maven中引入token依赖
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.3.0</version>
</dependency>
创建token字符串
//数据
User user = new User(1, "tom", "123", "tomcat");
Map<String, String> map = new HashMap<>();
map.put("username", user.getUsername());
map.put("name", user.getName());String token = JWT.create().withAudience(user.getUsername()).withExpiresAt(new Date(System.currentTimeMillis()+1000*30)) //设置过期时间为30ms.withClaim("data", map).sign(Algorithm.HMAC256(user.getPassword()));
输出token结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ0b20iLCJkYXRhIjp7ImFtZSI6InRvbWNhdCIsInVzZXJuYW1lIjoidG9tIn0sImV4cCI6MTY4NDIyNTk5NX0.oLSSNY9JeBRrKilOroUZ_xT5AS7AODotUxBXeOZ_D40
解析token
DecodedJWT decode = JWT.decode(token);
获取token的内容
System.out.println("Header: " + decode.getHeader());
System.out.println("Payload: " + decode.getPayload());
System.out.println("Audience: " + decode.getAudience());
System.out.println("Signature: " + decode.getSignature());
验证token
JWTVerifier build = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
获取信息
System.out.println(build.verify(token).getClaim("data"));
{“name”:“tomcat”,“username”:“tom”}
token出现的问题
私钥salt被人篡改
String salt ="guess";String token = JWT.create().withClaim("name", "jack").withClaim("password","123").sign(Algorithm.HMAC256(salt));System.out.println(token);JWTVerifier build = JWT.require(Algorithm.HMAC256("abc")).build();build.verify(token);
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMyIsIm5hbWUiOiJqYWNrIn0.ghK3Y79d1VU66eaay9YknYyW9MhWCrsth93UTx75-fk
Exception in thread "main" com.auth0.jwt.exceptions.SignatureVerificationException: The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256at com.auth0.jwt.algorithms.HMACAlgorithm.verify(HMACAlgorithm.java:57)at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:463)at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:445)at com.wnhz.bms.preview.jwt.JwtDemo.main(JwtDemo.java:19)
篡改token中的信息
String salt ="guess";String token = JWT.create().withClaim("name", "jack").withClaim("password","123").sign(Algorithm.HMAC256(salt));System.out.println(token);token = token.replace("eyJwYXNzd29yZCI6IjEyMyIsIm5hbWUiOiJqYWNrIn0","eyJwYXNzd29yZCI7IjEyMyIsIm5hbWUiOiJqYWNrIn0");// token = token.replace("K","W");//黑客// JWTVerifier build = JWT.require(Algorithm.HMAC256("abc")).build();JWTVerifier build = JWT.require(Algorithm.HMAC256(salt)).build();build.verify(token);
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMyIsIm5hbWUiOiJqYWNrIn0.ghK3Y79d1VU66eaay9YknYyW9MhWCrsth93UTx75-fk
Exception in thread "main" com.auth0.jwt.exceptions.JWTDecodeException: The string '{"password";"123","name":"jack"}' doesn't have a valid JSON format.at com.auth0.jwt.impl.JWTParser.decodeException(JWTParser.java:90)at com.auth0.jwt.impl.JWTParser.parsePayload(JWTParser.java:47)at com.auth0.jwt.JWTDecoder.<init>(JWTDecoder.java:49)at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:444)at com.wnhz.bms.preview.jwt.JwtDemo.main(JwtDemo.java:26)
token过期
String salt ="guess";String token = JWT.create().withClaim("name", "jack").withClaim("password","123").withExpiresAt(new Date(System.currentTimeMillis()+10)).sign(Algorithm.HMAC256(salt));System.out.println(token);Thread.sleep(1000);JWTVerifier build = JWT.require(Algorithm.HMAC256(salt)).build();build.verify(token);
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMyIsIm5hbWUiOiJqYWNrIiwiZXhwIjoxNjk4MzA4MTA1fQ.KGA8e6HhO3CSxanvhBaO9X2EpCQY20iZCn2fkSqdlNo
Exception in thread "main" com.auth0.jwt.exceptions.TokenExpiredException: The Token has expired on 2023-10-26T08:15:05Z.at com.auth0.jwt.JWTVerifier$BaseVerification.assertValidInstantClaim(JWTVerifier.java:346)at com.auth0.jwt.JWTVerifier$BaseVerification.lambda$addMandatoryClaimChecks$17(JWTVerifier.java:308)at com.auth0.jwt.JWTVerifier$BaseVerification$1.verify(JWTVerifier.java:405)at com.auth0.jwt.JWTVerifier.verifyClaims(JWTVerifier.java:482)at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:464)at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:445)at com.wnhz.bms.preview.jwt.JwtDemo.main(JwtDemo.java:23)
使用拦截器统一拦截方式
@Component
public class AuthInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("bm_token");if (Objects.isNull(token) || "".equals(token.trim())) {throw new UserNoLoginException("用户名还没有登录异常");}return true;}
}
@Configuration
public class BmWebConfig implements WebMvcConfigurer {@Resourceprivate AuthInterceptor authInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authInterceptor).excludePathPatterns("/api/login");}
}