JWT教程

作者:y1y1z日期:2025/11/28

JWT技术

  • 描述:JWT是用于根据特征值生成Token(凭证)的工具库,常用于身份校验功能
  • JWT特性
  1. JWT天然携带信息,可以快速实现“多设备登录” 管理、登出、重复登录检验等功能
  2. JWT支持签名加密,开发者也可以初步校验特征值,保证了一定的安全性
  • token = Header + Payload + Signature
  1. Header:签名算法 + token类型(固定为JWT),例如{ "alg": "HS256","type": "JWT"}
  2. Signature:密文最后拼接密钥的哈希值(加密钥),防止token被篡改
  3. Payload:存储 Token 的实际数据(如用户信息、过期时间等),以键值对形式储存
  1. <type>:表示认证方案(统一为 Bearer 类型)
  2. <token>:实际的 JWT 字符串
  3. 示例:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

JWT工具类

JWT

  • 轻量级JWT:快速实现JWT基础功能(生成、验证、Claims)
  • Claim 类的作用:实例会实现所有类型映射关系,JWT创建Token时,Claim的其他类型映射实现方法的内容是返回null
  1. asString(): 将 claim 的值转换为 String
  2. asInt(): 将 claim 的值转换为 Integer
  3. asBoolean(): 将 claim 的值转换为 Boolean
  4. asDate(): 将 claim 的值转换为 Date (适用于 exp, iat 等标准声明)
  5. asArray(Class<T> clazz): 将 claim 的值转换为指定类型的数组
  6. asList(Class<T> clazz): 将 claim 的值转换为指定类型的 List
  7. asMap(): 将 claim 的值转换为 Map<String, Object>
  8. isNull(): 判断 claim 的值是否为 null
1<dependency>
2    <groupId>com.auth0</groupId>
3    <artifactId>java-jwt</artifactId>
4    <version>4.4.0</version>
5</dependency>
6
1@Slf4j
2public class JWTUtil {
3
4    // 轻量级JWT不严格要求秘钥长度
5    private static final String SECRET_KEY = "7d754ec7-335d-422a-821a-e4cc08877511";
6
7    // 算法实例,默认使用 HMAC-SHA256 算法
8    private static final Algorithm ALGORITHM = Algorithm.HMAC256(SECRET_KEY);
9
10    // Token 的发行者,默认是本应用
11    private static final String ISSUER = "wyh";
12
13    // Token 过期时间,单位:毫秒。例如:3600000ms = 1小时
14    private static final long EXPIRATION_TIME = 3600000;
15
16    /**
17     * 生成 JWT Token
18     *
19     * @param subject 主题(例如用户ID)
20     * @return 生成的 JWT Token 字符串
21     * @throws IllegalArgumentException 如果参数无效
22     * @throws JWTCreationException     如果生成 Token 失败(例如,密钥无效)
23     */
24    public static String generateToken(String subject) throws JWTCreationException {
25        // 虽然subject可以为null,但统一规范
26        if (!StringUtils.hasText(subject)) {
27            throw new IllegalArgumentException("Subject cannot be null or blank");
28        }
29        Date now = new Date();
30        Date expiration = new Date(now.getTime() + EXPIRATION_TIME);
31        return JWT.create()
32                .withIssuer(ISSUER) // (可选) Token 的发行者
33                .withSubject(subject)             // (推荐) Token 的主体,通常是用户名或用户ID
34                .withIssuedAt(now)                 // (推荐) Token 的签发时间
35                .withExpiresAt(expiration)         // (推荐) Token 的过期时间
36                .sign(ALGORITHM);                  // 使用算法进行签名
37    }
38
39
40    /**
41     * 生成 JWT Token
42     *
43     * @param subject 主题(例如用户ID)
44     * @param map 自定义参数,项目统一泛型均为String
45     * @return 生成的 JWT Token 字符串
46     * @throws IllegalArgumentException 如果参数无效
47     * @throws JWTCreationException     如果生成 Token 失败(例如,密钥无效)
48     */
49    public static String generateToken(String subject, Map<String, String> map) throws JWTCreationException {
50        if (!StringUtils.hasText(subject)) {
51            throw new IllegalArgumentException("Subject cannot be null or blank");
52        }
53        Date now = new Date();
54        Date expiration = new Date(now.getTime() + EXPIRATION_TIME);
55        JWTCreator.Builder builder = JWT.create();
56        Optional.ofNullable(map).orElse(new HashMap<>()).forEach((name, value) ->{
57            if (StringUtils.hasText(name) && StringUtils.hasText(value)) {
58                builder.withClaim(name, value); // 先声明自定义Claim,防止属性被覆盖
59            }
60        });
61        return builder.withIssuer(ISSUER)     // (可选) Token 的发行者
62                .withSubject(subject)   // (推荐) Token 的主体,通常是用户名或用户ID
63                .withIssuedAt(now)      // (推荐) Token 的签发时间
64                .withExpiresAt(expiration)
65                .sign(ALGORITHM);// (推荐) Token 的过期时间
66    }
67
68    /**
69     * 验证 JWT Token 的有效性
70     *
71     * @param token 需要验证的 Token 字符串
72     * @return 如果 Token 有效,则返回 JWT实例,否则返回 null
73     */
74    public static DecodedJWT validateToken(String token) {
75        try {
76            if (!StringUtils.hasText(token)){
77                return null;
78            }
79            // 创建验证器
80            JWTVerifier verifier = JWT.require(ALGORITHM)
81                    .withIssuer(ISSUER) // 验证发行者是否匹配
82                    .build(); // 构建验证器
83            // verify方法内置了验证逻辑
84            return verifier.verify(token);
85        } catch (JWTVerificationException e) {
86            // 验证失败的原因有很多,例如:签名不匹配、Token为空、Token 过期、发行者不匹配等
87            log.error("JWT verification failed: {}", e.getMessage());
88            return null;
89        }
90    }
91
92    /**
93     *  Token 中提取Subject
94     *
95     * @param token JWT Token 字符串
96     * @return 提取出的用户名,如果 Token 无效或没有 subject,则返回 null
97     */
98    public static String getSubject(String token) {
99        DecodedJWT jwt = validateToken(token);
100        if (ObjectUtils.isEmpty(jwt)) {
101            return null;
102        }
103        return jwt.getSubject();
104    }
105
106    /**
107     *  Token 中提取 Claims
108     *
109     * @param token JWT Token 字符串
110     * @return 包含 Claims  Map,如果 Token 无效则返回 null
111     */
112    public static Map<String, String> getClaims(String token) {
113        DecodedJWT jwt = validateToken(token);
114        if (ObjectUtils.isEmpty(jwt)) {
115            return Collections.emptyMap();
116        }
117        Map<String, Claim> claimMap = jwt.getClaims();
118        return convertClaim2Map(claimMap);
119    }
120
121    /**
122     * 刷新Token(重置有效期)
123     *
124     * @param oldToken 旧的、但仍然有效的JWT Token
125     * @return 如果旧Token有效,则返回一个具有新过期时间的新Token;否则返回 null
126     */
127    public static String refreshToken(String oldToken) {
128        DecodedJWT jwt = validateToken(oldToken);
129        if (ObjectUtils.isEmpty(jwt)) {
130            return null;
131        }
132        String subject = jwt.getSubject();
133        if (!StringUtils.hasText(subject)){
134            return null;
135        }
136        return generateToken(subject,convertClaim2Map(jwt.getClaims()));
137    }
138
139    private static Map<String, String> convertClaim2Map(Map<String, Claim> claimMap) {
140        HashMap<String, String> resultMap = new HashMap<>();
141        claimMap.forEach((name, claim) -> {
142            resultMap.put(name, claim.asString()); // 约定claim类型是字符串,其他格式默认换为null
143        });
144        return resultMap;
145    }
146}
147

JJWT

  • 企业级首选JWT:API可读性高、支持高级特性,如灵活的密钥管理、与其他库的深度集成
1<dependency>
2    <groupId>io.jsonwebtoken</groupId>
3    <artifactId>jjwt-api</artifactId>
4    <version>0.11.5</version>
5</dependency>
6<dependency>
7    <groupId>io.jsonwebtoken</groupId>
8    <artifactId>jjwt-impl</artifactId>
9    <version>0.11.5</version>
10    <scope>runtime</scope>
11</dependency>
12<dependency>
13    <groupId>io.jsonwebtoken</groupId>
14    <artifactId>jjwt-jackson</artifactId>
15    <version>0.11.5</version>
16    <scope>runtime</scope>
17</dependency>
18
1@Slf4j
2public class JJWTUtil {
3
4    // JJWT严格要求秘钥长度不少于32位,实际应该将密钥配置在配置文件中或使用密钥管理系统
5    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(
6        "7d754ec7-335d-422a-821a-e4cc08877511".getBytes());
7
8    // 统一算法HS256
9    private static final SignatureAlgorithm SIGNATUREALGORITHM = SignatureAlgorithm.HS256;
10
11    // Token 过期时间,单位:毫秒。例如:3600000ms = 1小时
12    private static final long EXPIRATION_TIME = 3600000;
13
14    // Token 的发行者,默认是本应用
15    private static final String ISSUER = "wyh";
16
17    /**
18     * 生成token
19     *
20     * @param subject 主体信息(如用户ID等)
21     * @return token密文
22     */
23    public static String generateToken(String subject) throws IllegalStateException, JwtException {
24        // 虽然subject可以为null,但统一规范
25        if (!StringUtils.hasText(subject)) {
26            throw new IllegalArgumentException("Subject cannot be null or blank");
27        }
28        Date createTime = new Date();
29        Date expireDate = new Date(createTime.getTime() + EXPIRATION_TIME);
30        return Jwts.builder()
31                .setSubject(subject) // 标识 Token 的主体(如用户ID等)
32                .setIssuer(ISSUER) // (可选) Token 的发行者
33                .setIssuedAt(createTime) // token生成时间
34                .setExpiration(expireDate) // token过期时间
35                .signWith(SECRET_KEY, SIGNATUREALGORITHM) // 开始加密
36                .compact();
37    }
38
39    /**
40     * 生成token
41     *
42     * @param subject 主体信息(如用户ID等)
43     * @param map     自定义参数,项目统一泛型均为String
44     * @return token密文
45     */
46    public static String generateToken(String subject, Map<String, String> map) throws IllegalStateException, JwtException {
47        if (!StringUtils.hasText(subject)) {
48            throw new IllegalArgumentException("Subject cannot be null or blank");
49        }
50        Date createTime = new Date();
51        Date expireDate = new Date(createTime.getTime() + EXPIRATION_TIME); // 默认有效期1h
52        return Jwts.builder()
53                .setClaims(Optional.ofNullable(map).orElse(new HashMap<>())) // 先声明自定义Claim,防止属性被覆盖
54                .setIssuer(ISSUER) // (可选) Token 的发行者
55                .setSubject(subject) // 标识 Token 的主体(如用户ID等)
56                .setIssuedAt(createTime) // token生成时间
57                .setExpiration(expireDate) // token过期时间
58                .signWith(SECRET_KEY, SignatureAlgorithm.HS256) // 默认HS256加密
59                .compact();
60    }
61
62
63    /**
64     * 验证 Token 有效性
65     *
66     * @param token 待验证的 Token 字符串
67     * @return 验证通过返回 true,否则返回 false
68     */
69    public static Claims validateToken(String token) {
70        if (!StringUtils.hasText(token)) {
71            return Jwts.claims(); // Jwts.claims() 本身就会返回空的 Claims 对象(底层是DefaultClaims)
72        }
73        try {
74            return Jwts.parserBuilder()
75                    .setSigningKey(SECRET_KEY) // 验证签名
76                    .requireIssuer(ISSUER)     // 验证发行者
77                    .build()
78                    .parseClaimsJws(token)    // 解析 Token
79                    .getBody();
80        } catch (IllegalArgumentException | JwtException e) { // 验证不通过、token过期等
81            log.error("Invalid JWT claims: {}", e.getMessage());
82            return Jwts.claims();
83        }
84    }
85
86    /**
87     *  Token 中提取Subject
88     *
89     * @param token token字符串
90     * @return 提取出的Subject,如果 Token 无效或没有 subject,则返回 null
91     */
92    public static String getSubject(String token) throws JwtException {
93        Claims claims = validateToken(token);
94        if (CollectionUtils.isEmpty(claims)) {
95            return null;
96        }
97        return claims.getSubject();
98    }
99
100    /**
101     *  Token 中提取 完整Claims
102     *
103     * @param token JWT Token 字符串
104     * @return 自定义信息的Map,如果 Token 无效则返回 null
105     */
106    public static Map<String, Object> getClaims(String token) throws JwtException {
107        Claims claims = validateToken(token);
108        if (CollectionUtils.isEmpty(claims)) {
109            return Collections.emptyMap();
110        }
111        return new HashMap<>(claims);
112    }
113
114    /**
115     * 刷新token有效期
116     *
117     * @param oldToken 待刷新token
118     * @return 新的token
119     */
120    public static String refreshToken(String oldToken) {
121        Claims claims = validateToken(oldToken);
122        if (CollectionUtils.isEmpty(claims)) {
123            return null;
124        }
125        Date now = new Date();
126        Date expireDate = new Date(now.getTime() + EXPIRATION_TIME);
127        return Jwts.builder()
128                .setClaims(claims) // 先声明自定义Claim,防止属性被覆盖
129                .setIssuer(ISSUER) // (可选) Token 的发行者
130                .setIssuedAt(now) // token生成时间
131                .setExpiration(expireDate) // token过期时间
132                .signWith(SECRET_KEY, SignatureAlgorithm.HS256) // 默认HS256加密
133                .compact();
134    }
135}
136

简单测试

1class JJWTUtilTest {
2
3    private String token;
4
5    @BeforeEach
6    void setUp() {
7        // 这个方法会在所有执行前各跑一次
8        token = JJWTUtil.generateToken("wyh");
9    }
10
11    @Test
12    void generateToken() {
13        System.out.println(token);
14        assertNotNull(token);
15    }
16
17    @Test
18    void getSubject() {
19        String subject = JJWTUtil.getSubject(token);
20        System.out.println(subject);
21        assertNotNull(subject);
22    }
23
24    @Test
25    void refreshToken() {
26        String newToken = JJWTUtil.refreshToken(token);
27        System.out.println(newToken);
28        String subject = JJWTUtil.getSubject(newToken);
29        System.out.println(subject);
30        assertNotNull(newToken);
31        assertNotNull(subject);
32    }
33}
34
1class JWTUtilTest {
2
3    private String token;
4
5    @BeforeEach
6    void setUp() {
7        // 这个方法会在所有执行前各跑一次
8        token = JWTUtil.generateToken("wyh");
9    }
10
11    @Test
12    void generateToken() {
13        System.out.println(token);
14        assertNotNull(token);
15    }
16
17    @Test
18    void getSubject() {
19        String subject = JWTUtil.getSubject(token);
20        System.out.println(subject);
21        assertNotNull(subject);
22    }
23
24    @Test
25    void refreshToken() {
26        String refreshToken = JWTUtil.refreshToken(token);
27        System.out.println(refreshToken);
28        String subject = JWTUtil.getSubject(refreshToken);
29        System.out.println(subject);
30        assertNotNull(refreshToken);
31        assertNotNull(subject);
32    }
33}
34

JWT身份验证

  • JWT+Redis实现身份校验流程
  1. 用户发送请求时,如果没有携带token或者token非法就返回登录功能
  2. 用户登录时,后端会生成一个token返回给前端,后端前端均缓存这个token
  3. 用户再次发送请求时,后端会查询缓存校验token,如果一致就认为用户已登录,直接复用缓存的用户信息即可
  • 注意事项
  1. token和Redis相关数据有效期要保持一致,考虑网络延迟等原因,Redis有效期可以适当延长
  2. 重置有效期前可以先检验是否临近过期(例如还剩5分钟),再刷新有效期
  3. 后端缓存可以设计两个映射关系:{token,用户信息}{用户特征值, 最新token}
  4. 设计两个映射关系优势:1.无需解密token得subje即可快速验证 2.后续请求时token若被异地更新会强制下线

登录功能

  • 登录业务逻辑
  1. 先检查用户身份,可以使用【用户名密码】,也可以使用【验证码】方式
  2. 如果用户未注册,则自动注册用户
  3. 请求未带token,就认为是第一次登录,生成token,存入后端缓存
  4. 请求带有合法token,判断是否重复登录:查询redis中最新token是否一致,不一致说明异地登录了,返回重新登陆
1@Service
2@Slf4j
3public class LoginService {
4
5    @Autowired
6    private UserDao userDao;
7
8    @Autowired
9    private RedisClient redisClient;
10
11    @Autowired
12    private BasicPasswordEncryptor passwordEncryptor;
13
14    @Autowired
15    private UserConverter userConverter;
16
17    private static final String USERINFO_PREFIX = "login:userinfo:";
18
19    private static final String TOKEN_PREFIX = "login:latest_token:";
20    
21    private static final Integer TOKEN_EXPIRE_IN = 300; // second
22
23    private static final Integer TOKEN_EXPIRE_TIME = 1; // hour
24
25    // 用户登录
26    public Result<String> login(UserVo userVo, String token) {
27        UserPo userPo = userDao.getUserByPhone(userVo.getPhone());
28        if (ObjectUtils.isEmpty(userPo)) {
29            return register(userVo);
30        }
31        // VO和PO的密码都是以密文方式存在
32        if (!passwordEncryptor.checkPassword(userVo.getPassword(), userPo.getPassword())) {
33            return Result.fail("身份验证失败");
34        }
35        String phone = userVo.getPhone();
36        UserDto userDto = userConverter.toUserDto(userVo);
37        // 用户携带token,检查是否重复登录
38        String latestToken = redisClient.get(TOKEN_PREFIX + phone);
39        if (StringUtils.hasText(token) && StringUtils.hasText(latestToken) && latestToken.equals(token)) {
40            // 重复登录
41            return Result.ok(refreshExpire(phone, token, userDto));
42        }
43        // 请求未携带token或者token已过期,直接以新会话进行登录
44        return Result.ok(saveToken2Redis(userDto, userVo.getPhone()));
45    }
46
47    // 用户注册
48    public Result<String> register(UserVo userVo) {
49        try {
50            UserPo userPo = userConverter.toUserPo(userVo);
51            // 数据库存入密码不能是明文
52            String password = passwordEncryptor.encryptPassword(userVo.getPassword());
53            userPo.setPassword(password);
54            userDao.insertUser(userPo);
55        } catch (Exception e) {
56            // 记录异常日志,包含关键信息如手机号,以及完整的异常堆栈
57            log.error("用户注册失败,手机号: {}", userVo.getPhone(), e);
58            return Result.fail("用户注册失败,稍后再试");
59        }
60        // 注册成功后,重新查询用户信息,防止极端情况下注册成功但查询不到
61        if (ObjectUtils.isEmpty(userDao.getUserByPhone(userVo.getPhone()))) {
62            return Result.fail("注册成功,但用户信息获取失败");
63        }
64        UserDto userDto = userConverter.toUserDto(userVo);
65        return Result.ok(saveToken2Redis(userDto, userVo.getPhone()));
66    }
67
68    private String refreshExpire(String phone, String token, UserDto userDto) {
69        if (redisClient.getExpire(TOKEN_PREFIX + phone) < TOKEN_EXPIRE_IN) {
70            // 有效期不足五分钟,重置有效期
71            String newToken = JWTUtil.refreshToken(token);
72            redisClient.set(TOKEN_PREFIX + phone, newToken, TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
73            redisClient.set(USERINFO_PREFIX + newToken, JSON.toJSONString(userDto), TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
74            redisClient.delete(USERINFO_PREFIX + token);
75            return newToken;
76        }
77        // 直接复用token
78        return token;
79    }
80
81    // 存入token信息到redis中,redis不需要存密码,入参为DTO
82    private String saveToken2Redis(UserDto userDto, String phone) {
83        String newToken = JWTUtil.generateToken(phone);
84        // redis不需要存密码,转换为DTO
85        String userInfo = JSON.toJSONString(userDto);
86        // token: 用户信息
87        redisClient.set(TOKEN_PREFIX + phone, newToken, TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
88        // 用户手机: 最新token,此设计可用于实现单设备登录,后续请求发现token不是最新的了强制下线
89        redisClient.set(USERINFO_PREFIX + newToken, userInfo, TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
90        return newToken;
91    }
92}
93

会话保持

  • 使用拦截器拦截需要验证请求的Web接口
  1. 先验证token有效性
  2. 再根据剩余有效期判断是否延长有效期
  3. 存入ThreadLocal,方便后续业务复用
1@Component
2public class AuthTokenInterceptor implements HandlerInterceptor {
3
4    @Autowired
5    private RedisClient redisClient;
6
7    @Override
8    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
9        String token = req.getHeader(REQUEST_TOKEN_HEADER);
10        // 未携带token拦截
11        if (!StringUtils.hasText(token)) {
12            Result<String> fail = Result.fail("用户未登录");
13            res.setContentType(HTTP_JSON_TYPE);
14            res.getWriter().write(JSON.toJSONString(fail));
15            return false;
16        }
17        // 非法token拦截
18        DecodedJWT decodedJWT = JWTUtil.validateToken(token);
19        if (ObjectUtils.isEmpty(decodedJWT)) {
20            Result<String> fail = Result.fail("token非法或过期");
21            res.setContentType(HTTP_JSON_TYPE);
22            res.getWriter().write(JSON.toJSONString(fail));
23            return false;
24        }
25        // 必须同时校验「JWT 有效性」和「Redis 状态」,且Redis 状态是最终判据(因为 Redis 可动态管控)
26        String phone = JWTUtil.getSubject(token);
27        String latestToken = redisClient.get(TOKEN_PREFIX + phone);
28        String userInfo = redisClient.get(USERINFO_PREFIX + token);
29        if (!StringUtils.hasText(latestToken) || !StringUtils.hasText(userInfo)) {
30            Result<String> fail = Result.fail("登录失效");
31            res.setContentType(HTTP_JSON_TYPE);
32            res.getWriter().write(JSON.toJSONString(fail));
33            return false;
34        }
35        // 实现异地登录功能
36        if (!latestToken.equals(token)){
37            Result<String> fail = Result.fail("异地登录风险拦截");
38            res.setContentType(HTTP_JSON_TYPE);
39            res.getWriter().write(JSON.toJSONString(fail));
40            return false;
41        }
42        // 验证通过,如果临近过期,重置token和redis有效期
43        UserVo userVo = JSON.parseObject(userInfo, UserVo.class);
44        Long expire = redisClient.getExpire(TOKEN_PREFIX + phone);
45        if (expire != -1 && expire < TOKEN_EXPIRE_IN) {
46            // 有效期不足五分钟,重置有效期
47            String newToken = JWTUtil.refreshToken(token);
48            redisClient.set(TOKEN_PREFIX + phone, newToken, TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
49            redisClient.set(USERINFO_PREFIX + newToken, userInfo, TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
50            redisClient.delete(USERINFO_PREFIX + token);
51            res.setHeader(REQUEST_TOKEN_HEADER, newToken);
52        }
53        // 无需重置token,直接用户信息存入ThreadLocal
54        res.setHeader(REQUEST_TOKEN_HEADER, token);
55        UserThreadLocal.USER_THREAD_LOCAL.set(userVo);
56        return true;
57    }
58
59    @Override
60    public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object o, Exception ex) throws Exception {
61        // 请求执行完毕后置处理,移除用户信息,避免内存泄露
62        UserThreadLocal.USER_THREAD_LOCAL.remove();
63    }
64}
65

单点登录(SSO)

  • 描述:一个功能可能涉及多个服务,只要登录任一服务,其他服务可以自动登录
  • 流程:单体JWT区别是SSO将JWT验证API单独置于一个服务中,且前端可以共享token
  1. 共享令牌:前端在访问所有子系统时,携带同一个 JWT 令牌(跨域 / 跨服务共享)
  2. 统一认证中心:所有子系统的登录请求都转发到认证中心,认证通过后生成 JWT 令牌
  3. 跨系统验证:子系统接收到请求时,提取 JWT 并调用认证中心的验证接口,验证通过则识别用户身份,无需登录
1@RestController
2public class AuthTokenController {
3
4    @Autowired
5    private RedisClient redisClient; // redis客户端已自定义封装
6
7    // 常量实际应该以单独类封装
8    private static final String USERINFO_PREFIX = "login:userinfo:";
9
10    private static final String TOKEN_PREFIX = "login:latest_token:";
11    
12    private static final Integer TOKEN_EXPIRE_IN = 300; // second
13
14    private static final Integer TOKEN_EXPIRE_TIME = 1; // hour
15    
16    private static final String HTTP_JSON_TYPE = "application/json;charset=UTF-8";
17
18    private static final String REQUEST_TOKEN_HEADER = "token";
19
20    @PostMapping("/auth")
21	public void login(HttpServletRequest request, HttpServletResponse response) throws IOException{
22        String token = request.getHeader(REQUEST_TOKEN_HEADER);
23        // 未携带token拦截
24        if (!StringUtils.hasText(token)) {
25            Result<String> fail = Result.fail("用户未登录");
26            response.setContentType(HTTP_JSON_TYPE);
27            response.getWriter().write(JSON.toJSONString(fail));
28            return;
29        }
30        // 非法token拦截
31        DecodedJWT decodedJWT = JWTUtil.validateToken(token);
32        if (ObjectUtils.isEmpty(decodedJWT)) {
33            Result<String> fail = Result.fail("token非法或过期");
34            response.setContentType(HTTP_JSON_TYPE);
35            response.getWriter().write(JSON.toJSONString(fail));
36            return;
37        }
38        // 必须同时校验「JWT 有效性」和「Redis 状态」,且Redis 状态是最终判据(因为 Redis 可动态管控)
39        String phone = JWTUtil.getSubject(token);
40        String latestToken = redisClient.get(TOKEN_PREFIX + phone);
41        String userInfo = redisClient.get(USERINFO_PREFIX + token);
42        if (!StringUtils.hasText(latestToken) || !StringUtils.hasText(userInfo)) {
43            Result<String> fail = Result.fail("登录失效");
44            response.setContentType(HTTP_JSON_TYPE);
45            response.getWriter().write(JSON.toJSONString(fail));
46            return;
47        }
48        // 实现异地登录功能
49        if (!latestToken.equals(token)){
50            Result<String> fail = Result.fail("异地登录风险拦截");
51            response.setContentType(HTTP_JSON_TYPE);
52            response.getWriter().write(JSON.toJSONString(fail));
53            return;
54        }
55        // 验证通过,如果临近过期,重置token和redis有效期
56        UserVo userVo = JSON.parseObject(userInfo, UserVo.class);
57        Long expire = redisClient.getExpire(TOKEN_PREFIX + phone);
58        if (expire != -1 && expire < TOKEN_EXPIRE_IN) {
59            // 有效期不足五分钟,重置有效期
60            String newToken = JWTUtil.refreshToken(token);
61            redisClient.set(TOKEN_PREFIX + phone, newToken, TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
62            redisClient.set(USERINFO_PREFIX + newToken, userInfo, TOKEN_EXPIRE_TIME, TimeUnit.HOURS);
63            redisClient.delete(USERINFO_PREFIX + token);
64            response.setHeader(REQUEST_TOKEN_HEADER, newToken);
65            return;
66        }
67        // 无需重置token
68        response.setHeader(REQUEST_TOKEN_HEADER, token);
69    }
70}
71

JWT教程》 是转载文章,点击查看原文


相关推荐


首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2025 XYZ博客

Powered by 聚合阅读