Java JWT 两种实现方案实战(auth0 java-jwt + jjwt)

举报
yd_272031863 发表于 2026/11/02 23:42:58 2026/11/02
【摘要】 前后端分离项目常用 JWT 做无状态登录鉴权,本文整理两套主流 JWT 工具库完整实现,包含工具封装、本地内存黑名单、Redis 集群黑名单方案、SpringBoot 拦截器接入、统一异常处理,同时记录开发高频踩坑点,可直接用于项目。

标签:Java JWT Token SpringBoot 鉴权 hutool

前置说明
JWT 本身是无状态的,一旦签发,在有效期内都能正常访问。想要实现主动注销,主流两种方案:

  1. 短时效 AccessToken + RefreshToken 刷新机制(推荐)
  2. 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());
    }
}

六、登录拦截器(核心鉴权逻辑)

鉴权顺序:

  1. 取出Header中 Authorization: Bearer xxx
  2. 判断token是否为空
  3. 判断是否在黑名单
  4. 校验JWT签名与有效期
  5. 解析用户信息放入请求上下文
  6. 放行;失败抛出鉴权异常
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改动巨大,版本坑多

⚠️ 重点坑点

  1. 禁止两套JWT库交叉校验:jjwt开启GZIP压缩后的token,auth0无法解析,必校验失败;
  2. 密钥长度要求:HS256 ≥ 32字节,HS512 ≥ 64字节,密钥太短会报错;
  3. 黑名单选型:单机开发测试用 Hutool TimedCache,生产集群必须Redis;
  4. GZIP压缩不推荐开启:部分网关、反向代理、前端组件对压缩JWT兼容差;
  5. claim类型匹配:存入String类型userId,读取时不要强转Integer/Long,避免类型转换异常;
  6. JWT黑名单只是兜底方案,最优方案是短时效AccessToken + RefreshToken;
  7. 密钥严禁硬编码代码中,统一放到配置中心(Nacos/Apollo)。

九、扩展方向(博客末尾)

  1. RefreshToken 刷新机制实现
  2. JWT 载荷加密(JWE),敏感信息不要放在JWT的payload
  3. 结合 Spring Security 替换自定义拦截器方案
  4. JWT 颁发记录日志、异常告警
  5. 基于注解实现接口免鉴权(@IgnoreAuth)

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。