5. JWT令牌
5.1 JWT令牌概念
JWT (JSON Web Token) 是一种用于在网络应用程序之间安全传输信息的开放标准(RFC 7519)。JWT 令牌是一种轻量级的令牌,它包含了一些关键信息,比如用户身份、权限等,并使用数字签名进行验证,以确保该信息在传输过程中不被篡改或伪造。
5.2 JWT令牌构成
JWT 令牌的格式是由 RFC 7519 标准规定的,由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。头部包含了令牌类型和加密算法等信息,载荷包含了需要传输的信息,例如用户 ID、角色、权限等,签名则是将头部和载荷进行加密后得到的结果,用于验证令牌是否被篡改。
使用 JWT 令牌的好处是可以方便地在不同的应用程序或服务之间共享用户身份信息,而无需每次都进行身份验证。此外,因为 JWT 令牌是基于标准的 JSON 格式,因此易于使用和传输。
假设一个网站需要验证用户身份并授权用户访问某些受保护的资源。使用 JWT 令牌,用户登录后,服务器会生成一个包含用户信息的 JWT 令牌,然后将该令牌返回给客户端。如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWQiOiIxMjM0NTY3ODkwIiwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl19.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- 头部(Header):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- alg
参数表示使用的加密算法,这里是 HMAC-SHA256 算法。
- typ
参数表示令牌类型,这里是 JWT。
{
"alg": "HS256",
"typ": "JWT"
}
- 载荷(Payload):
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWQiOiIxMjM0NTY3ODkwIiwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl19
-iss
: jwt签发者
-sub
: jwt所面向的用户
-aud
: 接收jwt的一方
-exp
: jwt的过期时间,这个过期时间必须要大于签发时间
-nbf
: 定义在什么时间之前,该jwt都是不可用的.
-iat
: jwt的签发时间
-jti
: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
{
"sub": "1234567890",//ID
"name": "John Doe", //角色
"iat": 1516239022 //权限
}
- 签名(Signature):
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
签名(Signature)部分是对头部和载荷进行数字签名(base64加密)生成的,例如:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
5.3 JWT令牌的基本使用
5.3.1 基本思路
在实际使用中,通常会使用现有的 JWT 库来生成和解析 JWT 令牌,这些库已经实现了 RFC 7519 标准中的格式和规范,因此可以方便地使用这些库来生成和解析 JWT 令牌。本文主要介绍java-jwt,jjwt
等这两种库
基本思路:要分为两种页面,登录页面和其他页面,登录页面用于获取token,只有登录了之后才能访问其他页面。登录之后获取到的token会返回给客服端(或者说前端),下一次(其他页面)发起请求时将token添加到请求头,后端设置拦截器判断请求头中的token是否有值(有值就进一步判断值是否正确),如果没有则说明未登录,即没有权限,有值且正确了就放行。值得一提的是登录页面的请求不用拦截,因为需要获取token。通过解析 JWT 令牌并验证签名,可以获取到载荷部分存储的用户的身份和权限,从而判断是否放行。
5.3.2 java-jwt实现JWT
官网:
https://github.com/auth0/java-jwt
以下是一个使用 Java 编写的基本 JWT 登录示例,该示例使用 java-jwt库来生成和解析 **JWT **令牌
- 首先,需要在项目中添加 java-jwt依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.0.0</version> <!--我使用这个版本时报错了-->
<version>3.4.0</version>
</dependency>
- 然后定义一个简单的用户类 User:
@Data
public class User {
private Long id;
private String username;
private String password;
}
- 将JWT主要操作封装
package com.zhaoxi.test.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Map;
@Component
public class JwtUtil {
/** 盐值*/
private static final String SING="LIUYISHOU@Token666";
//生成令牌
public static String getToken(Map<String,String> map){
//获取日历对象
Calendar calendar=Calendar.getInstance();
//默认7天过期
calendar.add(Calendar.DATE,7);
//新建一个JWT的Builder对象
JWTCreator.Builder builder = JWT.create();
//将map集合中的数据设置进payload载荷
map.forEach((k,v)->{
builder.withClaim(k, v);
});
//设置过期时间和签名
String sign = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SING));
return sign;
}
/**
* 验签并返回DecodedJWT
* @param token 令牌
*/
public static DecodedJWT getToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SING);
DecodedJWT jwt = JWT.require(algorithm).build().verify(token);
System.out.println("解析令牌成功:" + jwt.getClaims());
return jwt;
} catch (Exception ex) {
System.out.println("解析令牌失败:" + ex.getMessage());
}
return null;
}
}
- 定义Controller层接口,其中使用 java-jwt 库生成 JWT 令牌:
package com.zhaoxi.test.controller;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zhaoxi.test.pojo.User;
import com.zhaoxi.test.service.UserService;
import com.zhaoxi.test.utils.JwtUtil;
import com.zhaoxi.test.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@RestController
@Slf4j
public class UserController {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserService userService;
@PostMapping("/user/login")//登录后返回token,拦截器不拦截
public R login(@RequestBody User user){
//打印输入的用户名和密码
log.info("用户名:[{}]",user.getUsername());
log.info("密码:[{}]",user.getPassword());
//创建map,作为返回值
HashMap<String, Object> map = new HashMap<>();
try {
User login = userService.login(user);
//存储载荷声明参数map
HashMap<String, String> plMap = new HashMap<>();
plMap.put("username", login.getUsername());
//生成JWT令牌
String token = jwtUtil.getToken(plMap);
log.info("token={}",token);
//通过验证,将相关用户信息及token等存入R,用于返回
map.put("token",token);
new R(map,"cg");
} catch (Exception e) {
}
return new R(map,"cg");
}
@PostMapping("/user/test")
public R test(HttpServletRequest request){
//处理自己的业务逻辑
//获取请求头中携带的token
String token = request.getHeader("token");
//验证token,获取token中的相关信息,这里其实不用验证,因为拦截器已经验证了
DecodedJWT verify = jwtUtil.getToken(token);
log.info("用户名:[{}]",verify.getClaim("username").asString());
log.info("密码:[{}]",verify.getClaim("password").asString());
return new R("请求成功");
}
}
service和mapper层忽略,
- 定义拦截器
package com.zhaoxi.test.interceptor;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhaoxi.test.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.HashMap;
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HashMap<String, Object> map = new HashMap<>();
//获取存放在请求头中的token
String token = request.getHeader("token");
//这里仅仅只是判断有没有token,并没有判断token是否正确,所以详细判断可以之后添加
log.info("tokenInterceptor={}",token);
try {
JwtUtil.getToken(token); //验证令牌,验证通过则返回true
return true;
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg","无效签名");
}catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token过期!");
}catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","token无效!");
}catch (Exception e){
e.printStackTrace();
map.put("msg","token无效!");
}
//验证令牌不通过,返回false
map.put("state",false);
//将map转化为json,相应给前端
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
- 配置拦截器
package com.zhaoxi.test.config;
import com.zhaoxi.test.interceptor.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
System.out.println("拦截请求");
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/test") //拦截user接口
.excludePathPatterns("/user/login"); //放行login接口,login请求是设置jwt值的,不能拦截
}
}
5.3.2 使用jjwt实现
官网
:https://github.com/jwtk/jjwt
- 导依赖
<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>
- JWT 工具类
package com.zhaoxi.test.utils;
import com.zhaoxi.test.pojo.User;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
private SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
/**
* 获得jwt,token
*/
public String getToken(User user) {
// 标头
Map<String, Object> headMap = new HashMap<>();
headMap.put("type", "JWT");
headMap.put("alg", "HS256");
// 自定义信息,payload载荷
Map<String, Object> cliMap = new HashMap<>();
cliMap.put("username", user.getUsername());
cliMap.put("password", user.getPassword());
// 过期时间
long l = System.currentTimeMillis() + (1000 * 60 * 2);
Date date = new Date(l);
JwtBuilder jwtBuilder = Jwts.builder().setHeader(headMap)
.setClaims(cliMap)
.setExpiration(date);
// 签名,这里有两种方法
// 第一种,此方法要求key长度必须足够长,否则会报错
// SecretKey secretKey = Keys.hmacShaKeyFor("esyrgfdfdfdf".getBytes());
// String jwt = jwtBuilder.signWith(secretKey,SignatureAlgorithm.HS256).compact();
// 第二种,建议采用此法方法
String jwt = jwtBuilder.signWith(secretKey).compact();
return jwt;
}
/**
* 解析jwt,解析token
*/
boolean verifyJwt(String token) {
try{
// 这里的secretKey必须和生成时用的secretKey一样
Jwt parse = Jwts.parserBuilder().setSigningKey(secretKey).build().parse(token);
Claims body = (Claims) parse.getBody(); //获取载荷
Object name = body.get("username");
Header header = parse.getHeader(); //获取头部
Object type = header.get("type"); //加密类型
// 还可以以下面这种方式解析jwt
// Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(jwt);
// Claims body = claimsJws.getBody();
// JwsHeader header = claimsJws.getHeader();
return true; //没有报错就说明解析成功
}catch(Exception e){
System.out.println("解析令牌失败:" + e.getMessage());
}
return false;
}
}
其他部分省略
5.4 JWT创建异常
- 令牌过期异常:ExpiredJwtException
- 签名或jwt错误:SignatureException
- 令牌过期异常:TokenExpiredException
- 签名无效或key错误异常:SignatureVerificationException
- 算法不匹配异常:AlgorithmMismatchException
The Token’s Signature resulted invalid when verified using the Algorithm: HmacSHA256,报此条错误可能是因为java-jwt依赖包与其他依赖冲突
5.5 常见签名算法
如下:
JWS | 算法 | 描述 |
---|---|---|
HS256 | HMAC256 | 带有 SHA-256 的 HMAC |
HS384 | HMAC384 | 带有 SHA-384 的 HMAC |
HS512 | HMAC512 | 带有 SHA-512 的 HMAC |
RS256 | RSA256 | 带有 SHA-256 的 RSASSA-PKCS1-v1_5 |
RS384 | RSA384 | 带有 SHA-384 的 RSASSA-PKCS1-v1_5 |
RS512 | RSA512 | 带有 SHA-512 的 RSASSA-PKCS1-v1_5 |
ES256 | ECDSA256 | 曲线 P-256 和 SHA-256 的 ECDSA |
ES384 | ECDSA384 | 具有曲线 P-384 和 SHA-384 的 ECDSA |
ES512 | ECDSA512 | 曲线 P-521 和 SHA-512 的 ECDSA |