Java JWT 两种实现方案实战(auth0 java-jwt + jjwt)
【摘要】 前后端分离项目常用 JWT 做无状态登录鉴权,本文整理两套主流 JWT 工具库完整实现,包含工具封装、本地内存黑名单、Redis 集群黑名单方案、SpringBoot 拦截器接入、统一异常处理,同时记录开发高频踩坑点,可直接用于项目。
标签:
JavaJWTTokenSpringBoot鉴权hutool前置说明
JWT 本身是无状态的,一旦签发,在有效期内都能正常访问。想要实现主动注销,主流两种方案:
- 短时效 AccessToken + RefreshToken 刷新机制(推荐)
- Token 黑名单:注销时把 token 存入缓存,鉴权时优先校验黑名单(兜底方案)
一、依赖汇总(pom.xml)
<!-- 方案一:auth0 java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>
<!-- 方案二:jjwt 0.11.x 新版 -->
<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>
<!-- hutool 本地定时缓存(单机黑名单) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<!-- SpringBoot Redis(集群黑名单) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- SpringBoot Web、commons-lang3 辅助 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
application.yml 配置(JWT密钥、过期时间、Redis)
jwt:
# auth0 方案密钥 HS256,不少于32位
auth0-secret: abcdefghijklmnopqrstuvwxyz123456
issuer: zzuedu
# token 默认有效期 1小时,单位毫秒
expire: 3600000
# jjwt HS512 密钥,不少于64位
jjwt-secret: abcdefghijklmnopqrstuvwxyz123456abcdefghijklmnopqrstuvwxyz123456
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
二、方案一:auth0 java-jwt 工具类
package com.lxx.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* auth0 java-jwt 工具类
*/
@Component
public class JwtAuth0Util {
@Value("${jwt.auth0-secret}")
private String secretKey;
@Value("${jwt.issuer}")
private String issuer;
/**
* 生成JWT
* @param subject 一般存放 userId
* @param expireMs 有效期毫秒
* @return token
*/
public String generateToken(String subject, long expireMs) {
long expireTs = System.currentTimeMillis() + expireMs;
return JWT.create()
.withSubject(subject)
.withIssuer(issuer)
.withExpiresAt(new Date(expireTs))
.sign(Algorithm.HMAC256(secretKey));
}
/**
* 校验token,成功返回解析对象,失败返回null
*/
public DecodedJWT verifyToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(issuer)
.build();
return verifier.verify(token);
} catch (JWTVerificationException e) {
// 过期、签名错误、签发者不匹配都会进入
return null;
}
}
/**
* 重载:使用默认过期时间
*/
public String generateToken(String subject) {
return generateToken(subject, 3600 * 1000L);
}
}
三、方案二:jjwt 新版工具类
package com.lxx.utils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* jjwt 工具类,统一使用jjwt解析+校验,不再混用auth0包校验
*/
@Component
public class JwtJjwtUtil {
@Value("${jwt.jjwt-secret}")
private String signKeyRaw;
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(signKeyRaw.getBytes(StandardCharsets.UTF_8));
}
/**
* 创建token,默认有效期
*/
public String createToken(String userId, String username) {
return createToken(userId, username, 3600 * 1000L);
}
/**
* 创建token,自定义有效期
*/
public String createToken(String userId, String username, long expiration) {
return Jwts.builder()
.setSubject("AUTH-USER")
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.claim("userId", userId)
.claim("username", username)
.signWith(getSecretKey(), Jwts.SIG.HS512)
// 不建议开启GZIP压缩,兼容性差
// .compressWith(CompressionCodecs.GZIP)
.compact();
}
/**
* 校验token
*/
public boolean verifyToken(String token) {
if (!StringUtils.hasText(token)) {
return false;
}
try {
Jwts.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException e) {
return false;
}
}
/**
* 获取userId
*/
public String getUserId(String token) {
try {
if (!StringUtils.hasText(token)) return null;
Claims claims = Jwts.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.get("userId", String.class);
} catch (Exception e) {
return null;
}
}
/**
* 获取username
*/
public String getUsername(String token) {
try {
if (!StringUtils.hasText(token)) return null;
Claims claims = Jwts.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.get("username", String.class);
} catch (Exception e) {
return null;
}
}
}
四、Token黑名单两套实现(单机本地缓存 + 集群Redis)
4.1 Hutool TimedCache 本地黑名单(仅单机)
package com.lxx.utils;
import cn.hutool.cache.impl.TimedCache;
/**
* 本地定时缓存黑名单
* 注意:集群多节点部署失效!
* key:token字符串;value:占位标记;过期时间和JWT保持一致
*/
public class TimedCacheBlackListUtil {
// 3小时过期
public static final TimedCache<String, String> TOKEN_BLACK_CACHE = new TimedCache<>(3L * 60L * 60L * 1000L);
/**
* 添加token到黑名单
*/
public static void addBlack(String token) {
TOKEN_BLACK_CACHE.put(token, "invalid");
}
/**
* 判断是否在黑名单
*/
public static boolean isBlack(String token) {
return TOKEN_BLACK_CACHE.containsKey(token);
}
}
4.2 Redis 黑名单工具(集群环境推荐)
package com.lxx.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
public class RedisTokenBlackUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String BLACK_TOKEN_PREFIX = "jwt:black:";
/**
* 加入黑名单,ttl 和 jwt 有效期一致
*/
public void addBlack(String token, long expireMs) {
String key = BLACK_TOKEN_PREFIX + token;
stringRedisTemplate.opsForValue().set(key, "1", expireMs, TimeUnit.MILLISECONDS);
}
/**
* 判断是否黑名单
*/
public boolean isBlack(String token) {
String key = BLACK_TOKEN_PREFIX + token;
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
}
}
五、统一返回结果 + 自定义鉴权异常
package com.lxx.common;
import lombok.Data;
@Data
public class R<T> {
private Integer code;
private String msg;
private T data;
public static <T> R<T> ok(T data) {
R<T> r = new R<>();
r.setCode(200);
r.setMsg("success");
r.setData(data);
return r;
}
public static <T> R<T> fail(Integer code, String msg) {
R<T> r = new R<>();
r.setCode(code);
r.setMsg(msg);
return r;
}
}
package com.lxx.exception;
/**
* 鉴权异常:token为空、黑名单、校验失败、过期
*/
public class AuthException extends RuntimeException{
private Integer code;
public AuthException(Integer code,String msg){
super(msg);
this.code = code;
}
public Integer getCode(){
return code;
}
}
全局异常处理器
package com.lxx.exception;
import com.lxx.common.R;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AuthException.class)
@ResponseBody
public R<?> authExceptionHandler(AuthException e){
return R.fail(e.getCode(), e.getMessage());
}
}
六、登录拦截器(核心鉴权逻辑)
鉴权顺序:
- 取出Header中
Authorization: Bearer xxx- 判断token是否为空
- 判断是否在黑名单
- 校验JWT签名与有效期
- 解析用户信息放入请求上下文
- 放行;失败抛出鉴权异常
package com.lxx.interceptor;
import com.lxx.exception.AuthException;
import com.lxx.utils.JwtJjwtUtil;
import com.lxx.utils.RedisTokenBlackUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class JwtAuthInterceptor implements HandlerInterceptor {
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
@Resource
private JwtJjwtUtil jwtJjwtUtil;
@Resource
private RedisTokenBlackUtil redisTokenBlackUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String header = request.getHeader(AUTHORIZATION);
if (StringUtils.isBlank(header) || !header.startsWith(BEARER_PREFIX)) {
throw new AuthException(401, "未携带token");
}
String token = header.substring(BEARER_PREFIX.length());
// 第一步:黑名单校验
if (redisTokenBlackUtil.isBlack(token)) {
throw new AuthException(401, "token已注销,请重新登录");
}
// 第二步:校验jwt本身
if (!jwtJjwtUtil.verifyToken(token)) {
throw new AuthException(401, "token非法或已过期");
}
// 解析用户信息,放入request,controller中获取
String userId = jwtJjwtUtil.getUserId(token);
String username = jwtJjwtUtil.getUsername(token);
request.setAttribute("userId", userId);
request.setAttribute("username", username);
return true;
}
}
注册拦截器
package com.lxx.config;
import com.lxx.interceptor.JwtAuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private JwtAuthInterceptor jwtAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtAuthInterceptor)
.addPathPatterns("/**")
// 放行登录接口、swagger、验证码等
.excludePathPatterns("/user/login","/user/register","/doc.html");
}
}
七、Controller 示例:登录 + 注销 + 需要鉴权接口
package com.lxx.controller;
import com.lxx.common.R;
import com.lxx.utils.JwtJjwtUtil;
import com.lxx.utils.RedisTokenBlackUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private JwtJjwtUtil jwtJjwtUtil;
@Resource
private RedisTokenBlackUtil redisTokenBlackUtil;
/**
* 登录接口,返回token
*/
@PostMapping("/login")
public R<String> login(){
// 模拟账号密码校验成功
String userId = "2";
String username = "admin";
String token = jwtJjwtUtil.createToken(userId, username, 3600 * 1000L);
return R.ok(token);
}
/**
* 退出登录,token加入黑名单
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
String header = request.getHeader("Authorization");
String token = header.substring("Bearer ".length());
// 黑名单有效期和token保持一致 1小时
redisTokenBlackUtil.addBlack(token, 3600 * 1000L);
return R.ok("退出成功");
}
/**
* 需要鉴权的接口
*/
@GetMapping("/info")
public R<Object> info(HttpServletRequest request){
Object userId = request.getAttribute("userId");
Object username = request.getAttribute("username");
return R.ok("当前用户:" + userId + " - " + username);
}
}
八、两套方案对比 & 踩坑清单
方案对比
| 方案 | SDK包 | 优点 | 缺点 |
|---|---|---|---|
| auth0 java-jwt | com.auth0:java-jwt | API简洁,异常细分清晰,无内置压缩,上手友好 | claim自定义取值稍微麻烦 |
| jjwt | io.jsonwebtoken | 国内老项目普及,支持GZIP压缩,claim存取方便 | 0.9.x 和 0.11.x API改动巨大,版本坑多 |
⚠️ 重点坑点
- 禁止两套JWT库交叉校验:jjwt开启GZIP压缩后的token,auth0无法解析,必校验失败;
- 密钥长度要求:HS256 ≥ 32字节,HS512 ≥ 64字节,密钥太短会报错;
- 黑名单选型:单机开发测试用 Hutool TimedCache,生产集群必须Redis;
- GZIP压缩不推荐开启:部分网关、反向代理、前端组件对压缩JWT兼容差;
- claim类型匹配:存入String类型userId,读取时不要强转Integer/Long,避免类型转换异常;
- JWT黑名单只是兜底方案,最优方案是短时效AccessToken + RefreshToken;
- 密钥严禁硬编码代码中,统一放到配置中心(Nacos/Apollo)。
九、扩展方向(博客末尾)
- RefreshToken 刷新机制实现
- JWT 载荷加密(JWE),敏感信息不要放在JWT的payload
- 结合 Spring Security 替换自定义拦截器方案
- JWT 颁发记录日志、异常告警
- 基于注解实现接口免鉴权(@IgnoreAuth)
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)