1 什么是JWT
JSON Web Token(JWT)是一种开放标准(RFC7519),它定义了一种紧凑而独立的方式,用于在各方之间作为JSON对象安全地传输信息。此信息是可以验证和信任的,因为它是经过数字签名的。
来源JWT官网(https://jwt.io/introduction)
通俗的来说,jwt就是应用认证的一种解决方案,在讨论jwt之前,需要先了解的传统的session认证和token认证区别。
1.1 传统的session认证步骤:
- 客户端向服务端发送账号密码进行认证。
- 服务端在校验账号密码正确之后,将当前用户的基本信息保存到当前会话(session)中,并将sessionid返回给客户端。
- 客户端在拿到sessionid之后将其保存到cookie中,并在以后的每次请求中都携带该sessionid。
- 服务端根据客户端传过来的sessionid对当前用户进行认证,判断是否合法。
1.1.1 session认证存在的问题:
- 所有的session都在服务端进行保存,当用户量不断增大的时候服务器的开销也会随之增大。
- 在一个应用的服务器有多台的情况下,针对一个用户,就必须在每台服务器上都维护一个相同的session,或者引入Redis等中间件对session进行统一的管理。
1.2 token认证步骤:
- 客户端向服务端发送账号密码进行认证。
- 服务端在校验账号密码正确之后,生成token字符串返回给客户端。
- 客户端收到token之后进行保存,并在之后的每一次请求中将该token放到请求头中一并发送。
- 服务端在收到客户端传递的token之后,对token进行验证,判断是否合法。
- 使用token进行认证的话,服务端不需要保存任何会话信息或者用户信息,所有的信息都分散保存在客户端中,客户端每次请求的时候自己带上token即可。
2 JWT的构成
jwt是一个很长的字符串,中间用“.”分割成三部分,实际的jwt如下所示:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsZWUiLCJpc3MiOiJsZWUiLCJpZCI6IjEyMzQ1NiIsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE2MzQwMTU2MTgsImlhdCI6MTYzNDAwODQxOCwianRpIjoiZTY4ODAwNzUtODA5Mi00ZTM0LWEzZDctYmQ1YmQ4ZTA0YjBkIn0.MOPUmnWzbFijlM_vGESF6YdBDvSW3QtIh1qS8i_yuPc
jwt的三个部分如下:
- Header(头部)
- Payload(负载)
- Signature(签名)
2.1 Header
Header部分是一个json字符串,保存的是jwt的元数据,格式如下:
{
"alg": "HS256",
"typ": "JWT"
}
- alg:签名的加密方式
- typ:表示这个令牌(token)的类型(type),JWT令牌统一写为JWT
最后将Header的json字符串进行Base64编码之后就得到了jwt的第一部分
2.2 Payload
Payload部分同样是一个json字符串,其中保存的是有效信息,这些信息主要包含三个部分:
- 标准中注册的声明
- 公共的声明
- 私有的声明
2.2.1 标准中注册的声明(JWT官方规定的字段):
- iss:jwt签发者
- sub:jwt所面向的用户
- aud:接收jwt的一方
- exp:jwt的过期时间,这个过期时间必须要大于签发时间
- nbf:定义在什么时间之前,该jwt都是不可用的
- iat:jwt的签发时间
- jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
2.2.2 公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。
2.2.3 私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
Payload示例:
{
"sub":"lee",
"iss":"lee",
"id":"123456",
"exp":"1634015618",
"iat":"1634008418",
"jti":"e6880075-8092-4e34-a3d7-bd5bd8e04b0d",
"userName":"admin"
}
最后将Payload的json字符串进行Base64编码之后就得到了jwt的第二部分
2.3 Signature
jwt的第三部分就是签名部分,针对以下三个部分进行加密即可:
- Header(Base64编码后的)
- Payload(Base64编码后的)
- secret
将Base64编码后的Header和Payload用“.”进行连接,然后使用Header中的加密算法进行加盐secret加密。
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
最后加密出的字符串就是jwt的第三部分。
3 JWT的使用
当客户端接收到服务端发来的jwt字符串之后,将其保存到Cookie或者localStorage里面,然后每次向服务器发送请求的时候都带上JWT,将jwt放到HTTP请求的头信息Authorization中。
Authorization: Bearer <token>
3.1 服务端验证步骤
- 当服务端接收到客户端的token字符串之后,首先对字符串的第一部分进行解码,得到Header。
- 取出Header中的加密方式,使用该加密方式和secret对字符串的第一部分和第二部分进行加密,如果得到的签名和传递过来的签名一致,则认为该token合法。
- 对第二部分进行解码,得到需要的数据。
3.2 代码
使用jjwt实现jwt的签发和解析获取payload中的数据
- 引入
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
- Java代码
package com.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class JwtDemo {
//私钥 生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取
private final static String secret = "123456789";
/**
* 创建jwt
*/
public static String createJwtToken(){
// Header
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
//Payload
Map<String,Object> claims = new HashMap<String,Object>();
//自定义数据,根据业务需要添加
claims.put("id","123456");
claims.put("userName", "admin");
//标准中注册的声明
claims.put("iss", "lee");
//生成jwt
return Jwts.builder()
.setHeader(map) // 添加Header信息
.setClaims(claims) // 添加Payload信息
.setId(UUID.randomUUID().toString()) // 设置jti:是JWT的唯一标识
.setIssuedAt(new Date()) // 设置iat: jwt的签发时间
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置exp:jwt过期时间,3600秒=1小时
.setSubject("Jack") //设置sub:代表这个jwt所面向的用户
.signWith(SignatureAlgorithm.HS256, secret)//设置签名:通过签名算法和秘钥生成签名
.compact();
}
/**
* 从jwt中获取 Payload 信息
*/
private static Claims getClaimsFromJwt(String jwt) {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
public static void main(String[] args) {
String jwtToken = createJwtToken();
System.out.println("JWT Token: "+ jwtToken);
System.out.println("=======================================================");
Claims claims = getClaimsFromJwt(jwtToken);
System.out.println("claims: " + claims);
}
}
运行结果
JWT Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKYWNrIiwiaXNzIjoibGVlIiwiaWQiOiIxMjM0NTYiLCJ1c2VyTmFtZSI6ImFkbWluIiwiZXhwIjoxNjM0MDMzMjEwLCJpYXQiOjE2MzQwMjYwMTAsImp0aSI6ImQ4ZDk3ZjMzLTIwMTctNGQzMi04ZDRlLWM0Yzc3ZjU2NDUzMyJ9.N9SRwmwmFEFGU5hgRsQqJSngmasvTGZ5WXi1t5JO3MI
=======================================================
claims: {sub=Jack, iss=lee, id=123456, userName=admin, exp=1634033210, iat=1634026010, jti=d8d97f33-2017-4d32-8d4e-c4c77f564533}
3.3 建议
- Payload中不要存放敏感信息。
- 保存好secret私钥,不要泄露。
- jwt的有效期需要根据应用来决定,时间不宜设置过长。
- 如果可以,请使用HTTPS。