JWT学习
概述
JWT 即 JSON Web Token,通过 JSON 形式作为 Web 应用中的令牌,用于在各方安全的将信息作为 JSON 对象传输,在传输过程中还可以完成数据加密、签名等操作(就是可以做数据交换或者安全验证)
例如用来维持登陆状态,传统的形式是通过 Session,但是使用 Session 有诸多麻烦(具体看过往文章),所以可以使用 JWT 来把维持状态放在客户端,服务端只需取得该 JWT 即可判断当前用户的状态
认证流程
用法和之前写的报修系统后台是一样的,首先前端通过 Web 表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个 HTTP 的 POST 请求,后端核对用户名和密码成功后,将用户的 id 等其他信息作为 JWT Payload(负载),将其与头部分别进行 Base64 编码拼接后签名,形成的 JWT 就是一个形同 lll.zzz.xxx 的字符串
后端将 JWT 字符串返回给前端,前端把这个 JWT 存到 localStorage 上,以后前端每次发送请求时把这个 JWT 放到 Header 中的 Authorization 里面发送(全局)
令牌组成
参考资料 什么是 JWT -- JSON WEB TOKEN 参考资料 JWT 到底应该怎么用才对?
标头:Header 有效载荷:Payload 签名:Signature
所以结构为:Header.Payload.Signature
Header
标头通常由两部分组成:令牌类型(JWT)和所使用的签名算法,例如 HMAC、SHA256 或 RSA。它会使用 Base64 编码组成 JWT 结构的第一部分
{
"alg" : "HS256",
"typ" : "JWT"
}
Payload
令牌的第二部分是有效荷载,主要携带用户的数据信息,使用的也是 Base64 编码(所以不要在这里存放敏感信息);载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用):
- iss: jwt签发者
- sub: jwt所面向的用户(也可理解为面向用户的类型)
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共字段和私有字段都是用户可以任意添加的字段,区别在于公共字段是一些约定俗成,被普遍使用的字段,而私有字段更符合实际的应用场景。
当前已有的公共字段可以从 JSON Web Token Claims 中找到。
{
"sub": "1234567890",
"id" : "123456",
"name" : "john",
"admin" : "true",
"iat": 1516239022
}
Signature
前两部分都是使用 Base64 进行编码的,即前端可以解开指定里面的信息,而最后这个 Signature 需要使用编码后的 header 和 payload 以及自己提供的一个密钥进行签名(Header 指定的算法),签名的作用是确保 JWT 没有被篡改
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
服务端会对返回的数据的这个 Signature 与前面的数据重新加密,如果重新加密的结果是一样的则放行,如果不一样则丢弃
注:Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2 的 6 次方等于 64,所以每 6 比特为一个单元,对应某个可打印字符。三个字节有 24 比特,对应于 4 个 Base64 单元,即 3 个字节需要 4 个可打印字符来表示.JDK 中提供了非常方便的
BASE64Encoder和BASE64Decoder,直接使用它们就能完成编程和解码
使用 auth0-JWT
引入环境
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
生成 JWT
@Test
void contextLoads() {
// 创建一个 日历 对象
// 用法
// //加3年
// calendar.add(Calendar.YEAR, 3);
// //加2月
// calendar.add(Calendar.MONTH, 2);
// //减30天,对天的加减只用DAY_OF_YEAR
// calendar.add(Calendar.DAY_OF_YEAR, -30);
Calendar instance = Calendar.getInstance();
// 表示偏移 90 秒
instance.add(Calendar.SECOND, 90);
String token = JWT.create()// header 可以省略,因为有默认值
.withClaim("userId", "1234") // payload
.withClaim("username", "john")
.withExpiresAt(instance.getTime()) // 设置超时时间
.sign(Algorithm.HMAC256("this_is_secret_key")); // 设置密钥
System.out.println(token);
}
生成的 JWT 如下
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM3NjgxOTIsInVzZXJJZCI6IjEyMzQiLCJ1c2VybmFtZSI6ImpvaG4ifQ.Nhx3Rsjx0tQ6Zg8oqs7v7T_UFgwBoqmS_j9ht6qcmsk
解析 JWT
@Test
void test() {
// 密钥要一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("this_is_secret_key")).build();
String token =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJleHAiOjE2MDM3Njk2NTQsInVzZXJJZCI6IjEyMzQiLCJ1c2VybmFtZSI6ImpvaG4ifQ." +
"j4r05XEWoobgbBt6aJ47HVB2-bcq2VdQWR8Go17H7WU";
DecodedJWT decodedJWT = jwtVerifier.verify(token);
// 注意:如果有多个 Claim 时要通过 getClaims 来取
// 且,这里要取得参数要使用对应类型的 例如 String 就是 asString、Int 类型就是 asInt
System.out.println("用户Id:" + decodedJWT.getClaims().get("userId").asString());
System.out.println("用户名:" + decodedJWT.getClaims().get("username").asString());
System.out.println("过期时间:" + decodedJWT.getExpiresAt());
}
常见异常:
TokenExpiredException:令牌过期SignatureVerificationException:签名不一致异常AlgorithmMismatchException:算法不一致异常InvalidClaimException:失效的 payload 异常
封装成工具类
public class JWTUtils {
// 密钥
private static final String SIGN = "this_is_secret_key";
// 过期时间 单位为秒
private static final int EXPIRATION = 1800;
// 生成 token
public static String getToken(Map<String, String> map) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, EXPIRATION);
// 创建 JWT Builder
JWTCreator.Builder builder = JWT.create();
// 把传入的内容添加到 payload 里面去
map.forEach(builder::withClaim);
// 等价于
// map.forEach((k,v)->{
// builder.withClaim(k,v);
// });
return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SIGN));
}
// 验证 token 获取信息
public static DecodedJWT verifier(String token) {
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
}
集成到 SpringBoot
JWT 的工具类基本同上不用变,主要是添加一个拦截器
/**
* JWT过滤器,拦截 /secure的请求
*/
@Slf4j
@WebFilter(filterName = "JwtFilter", urlPatterns = "/secure/*")
public class JwtFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
response.setCharacterEncoding("UTF-8");
//获取 header里的token
final String token = request.getHeader("authorization");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
chain.doFilter(request, response);
}
// Except OPTIONS, other request should be checked by JWT
else {
if (token == null) {
response.getWriter().write("没有token!");
return;
}
Map<String, Claim> userData = JwtUtil.verifyToken(token);
if (userData == null) {
response.getWriter().write("token不合法!");
return;
}
Integer id = userData.get("id").asInt();
String name = userData.get("name").asString();
String userName = userData.get("userName").asString();
//拦截器 拿到用户信息,放到request中
request.setAttribute("id", id);
request.setAttribute("name", name);
request.setAttribute("userName", userName);
chain.doFilter(req, res);
}
}
@Override
public void destroy() {
}
}
使用 JJWT
参考资料 JJWT 官方文档 参考资料 Java Web Token 之 JJWT 使用
上面的那个 auth0 好像不是很常用,这里补个 JJWT 的用法
添加依赖
<!-- 配置参数 -->
<properties>
<java.version>1.8</java.version>
<jwt.version>0.10.7</jwt.version>
</properties>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
构建 JWT
@Test
public void getJWTTest() {
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jws = getJwtStr(key);
if (log.isDebugEnabled()) {
log.debug(jws);
}
}
private String getJwtStr(Key key) {
return Jwts.builder()
.setSubject("JDKONG")
.signWith(key)
.compact();
}
在以上代码中,构建的过程如下:
- 构建一个主题为 JDKONG 的 JWT;
- 使用适用于 HMAC-SHA-256 算法的密钥对 JWT 进行签名;
- 最后,将它压缩成最终的 String 形式。 签名的 JWT 称为 JWS。
最终生成的 JWT 如下所示:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKREtPTkcifQ.C-eSTnoK-lryYVerB6SCbgbTRMKpXyWvDJNNPH07g3Q
解析 JWT
现在,通过类似的方式验证 JWT:
@Test
public void parseJwtStr() {
// 得到密钥
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 得到 JWT
String jwtStr = getJwtStr(key);
// 验证 JWT
assert Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody()
.getSubject()
.equals("JDKONG");
}
这里需要注意两件事:
- 之前的密钥用于验证 JWT 的签名。 如果它无法验证 JWT,则抛出 SignatureException(从JwtException扩展)。
- 如果 JWT 已经过验,会接着断言该 claim 设置为 JDKONG。如果都没问题,则验证通过。
如果,在验证的过程中失败了会怎样呢?在做 JWT 解析时,可以捕捉异常 JwtException ,比如:
try {
Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws);
//OK, we can trust this JWT
} catch (JwtException e) {
//don't trust the JWT!
}