Java基础 | JWT登录场景化最优方案(二)

前言

Java基础 | JWT登录场景化最优方案(一)

三、安全强化模块(配套完整配置/工具代码,可直接集成)

3.1 传输安全(完整配置代码)

(1)HTTPS强制启用

① Spring Boot 内置配置(application.yml 完整配置)
# 服务器配置
server:
  port: 443 # HTTPS默认端口(80端口可配置重定向到443)
  # HTTPS 配置
  ssl:
    key-store: classpath:xxx.jks # 密钥库文件(放在resources目录下,生成方式:keytool工具)
    key-store-password: 123456 # 密钥库密码
    key-store-type: JKS # 密钥库类型(JKS/PKCS12,PKCS12更通用)
    key-alias: xxx # 密钥别名(生成密钥库时指定)
    enabled: true # 启用HTTPS
    protocol: TLS # 传输层安全协议
    ciphers: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 # 强加密套件

# 强制HTTPS重定向(将80端口请求重定向到443)
spring:
  web:
    resources:
      add-mappings: true
  security:
    require-ssl: true # 强制所有请求使用SSL(HTTPS)
② Nginx 反向代理SSL配置(nginx.conf 完整代码)
# 全局配置
worker_processes  1;
events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    # 配置HTTPS服务器,监听443端口
    server {
        listen       443 ssl;
        server_name  localhost; # 生产环境替换为真实域名

        # SSL证书配置(公钥+私钥,由CA机构颁发或自签)
        ssl_certificate      /usr/local/nginx/ssl/xxx.crt; # 公钥文件路径
        ssl_certificate_key  /usr/local/nginx/ssl/xxx.key; # 私钥文件路径

        # SSL优化配置
        ssl_protocols TLSv1.2 TLSv1.3; # 仅支持高版本TLS,禁用SSLv3等不安全协议
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256; # 强加密套件
        ssl_prefer_server_ciphers on; # 优先使用服务器端加密套件
        ssl_session_cache shared:SSL:10m; # SSL会话缓存
        ssl_session_timeout 10m; # SSL会话超时时间

        # 反向代理到Spring Boot应用(本地8080端口,若Spring Boot已配置443可直接转发)
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme; # 传递HTTPS协议标识
        }
    }

    # 强制将80端口HTTP请求重定向到443端口HTTPS
    server {
        listen       80;
        server_name  localhost;
        rewrite ^(.*)$ https://$host$1 permanent; # 永久重定向(301)
    }
}

(2)Cookie安全属性

① 核心属性详解
属性名 取值 核心作用 适用场景
HttpOnly true/false 禁止JavaScript读取Cookie,防止XSS攻击窃取Token 所有存储Token的Cookie,必须设为true
Secure true/false 仅在HTTPS协议下传输Cookie,HTTP协议不携带 生产环境必须设为true,测试环境可设为false
SameSite Lax/Strict/None 限制Cookie跨域传输,防止CSRF攻击 Lax(默认,兼容大部分场景)、Strict(严格跨域禁止)、None(需配合Secure=true,支持跨域)
CookieUtil 完整配置代码(补充强化版)
package com.example.jwtauthframework.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * Cookie工具类(强化安全属性,支持Token存储)
 * 直接集成到项目即可使用
 *
 * @author xxx
 * @date 2026-01-02
 */
@Slf4j
public class CookieUtil {

    /**
     * 设置安全Cookie(默认启用HttpOnly/Secure/SameSite=Lax)
     *
     * @param response  HttpServletResponse
     * @param name      Cookie名称
     * @param value     Cookie值
     * @param domain    域名
     * @param path      路径
     * @param maxAge    过期时间(秒,-1表示会话级,0表示删除)
     * @param httpOnly  是否启用HttpOnly
     * @param secure    是否启用Secure
     * @param sameSite  SameSite属性(Lax/Strict/None)
     */
    public static void setCookie(
            HttpServletResponse response,
            String name,
            String value,
            String domain,
            String path,
            int maxAge,
            boolean httpOnly,
            boolean secure,
            String sameSite
    ) {
        if (!StringUtils.hasText(name) || response == null) {
            log.warn("Cookie名称或Response为空,设置失败");
            return;
        }
        try {
            // 编码Cookie值,防止特殊字符乱码
            String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name());
            Cookie cookie = new Cookie(name, encodedValue);
            // 设置域名
            if (StringUtils.hasText(domain)) {
                cookie.setDomain(domain);
            }
            // 设置路径
            cookie.setPath(StringUtils.hasText(path) ? path : "/");
            // 设置过期时间
            cookie.setMaxAge(maxAge);
            // 安全属性配置
            cookie.setHttpOnly(httpOnly);
            cookie.setSecure(secure);
            // 设置SameSite(通过响应头手动设置,兼容低版本Tomcat)
            String cookieHeader = String.format("%s=%s; Max-Age=%d; Path=%s; %s; %s; SameSite=%s",
                    name,
                    encodedValue,
                    maxAge,
                    cookie.getPath(),
                    httpOnly ? "HttpOnly" : "",
                    secure ? "Secure" : "",
                    StringUtils.hasText(sameSite) ? sameSite : "Lax"
            );
            if (StringUtils.hasText(domain)) {
                cookieHeader += "; Domain=" + domain;
            }
            response.addHeader("Set-Cookie", cookieHeader);
            log.debug("Cookie{}设置成功,安全属性:HttpOnly={}, Secure={}, SameSite={}", name, httpOnly, secure, sameSite);
        } catch (Exception e) {
            log.error("设置Cookie失败", e);
        }
    }

    /**
     * 获取Cookie值
     *
     * @param request HttpServletRequest
     * @param name    Cookie名称
     * @return Cookie值(null表示不存在)
     */
    public static String getCookieValue(HttpServletRequest request, String name) {
        if (request == null || !StringUtils.hasText(name)) {
            return null;
        }
        Cookie[] cookies = request.getCookies();
        if (cookies == null || cookies.length == 0) {
            return null;
        }
        for (Cookie cookie : cookies) {
            if (name.equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
        return null;
    }

    /**
     * 删除Cookie(本质是设置maxAge=0)
     *
     * @param response HttpServletResponse
     * @param name     Cookie名称
     * @param domain   域名
     * @param path     路径
     */
    public static void removeCookie(HttpServletResponse response, String name, String domain, String path) {
        setCookie(response, name, "", domain, path, 0, true, false, "Lax");
        log.debug("Cookie{}删除成功", name);
    }
}

3.2 令牌安全(完整优化代码)

(1)Token过期时间(按场景划分+配置示例)

① 场景化合理取值表
场景类型 Access Token 过期时间 Refresh Token 过期时间 说明
小型系统/内部系统 2小时(7200秒) 30天(2592000秒) 用户量少,体验优先,无需频繁登录
中大型ToC产品 1小时(3600秒) 7-30天(604800-2592000秒) 平衡安全性与体验,降低Token泄露风险
高并发场景/秒杀平台 30分钟(1800秒) 7天(604800秒) 高安全要求,缩短Access Token有效期
金融/政务系统 15分钟(900秒) 1-7天(86400-604800秒) 极高安全要求,严格控制Token有效期
application.yml 配置示例
jwt:
  # 小型内部系统配置
  inner:
    access:
      expire: 7200
    refresh:
      expire: 2592000
  # 中大型ToC产品配置
  toc:
    access:
      expire: 3600
    refresh:
      expire: 604800
  # 高并发/秒杀场景配置
  high-concurrency:
    access:
      expire: 1800
    refresh:
      expire: 604800
  # 金融/政务系统配置
  finance:
    access:
      expire: 900
    refresh:
      expire: 86400

(2)黑名单机制优化(布隆过滤器+动态扩容)

① 布隆过滤器内存估算公式
  • 核心公式:内存大小(bit)= - (n * ln(p)) / (ln(2))²
    • n:预计插入黑名单Token数量(如100万)
    • p:预期误判率(如0.001)
  • 示例:n=100万,p=0.001 → 内存大小≈1.44×100万×ln(1/0.001) ≈ 13MB(极小内存占用)
  • 哈希函数个数:k = (m/n) * ln(2),m为内存大小(bit),示例中k≈10个
② 动态扩容补充说明(基于场景4代码)
  1. 扩容触发条件:当前存储量/预计插入量 ≥ 扩容阈值(默认0.8)
  2. 扩容核心逻辑:生成新版本布隆过滤器 → 从Redis黑名单批量加载数据 → 切换版本标识 → 旧版本逐步淘汰
  3. 避免误判率升高:定期重建布隆过滤器(定时任务,默认1小时),清理无效黑名单数据

(3)令牌吊销与失效(完整代码)

① 主动退出(已整合到各场景,补充核心逻辑)
// 核心逻辑:清除Redis设备/版本信息 + 添加Token到黑名单 + 清除Cookie
public boolean logout(HttpServletRequest request, HttpServletResponse response) {
    try {
        String accessToken = getTokenFromRequest(request, "access");
        if (StringUtils.hasText(accessToken) && validateAccessToken(accessToken)) {
            // 1. 解析用户信息
            SecretKey secretKey = JwtBaseUtil.getHs512SecretKey(jwtSecretKey);
            Map<String, Object> claims = JwtBaseUtil.parseHs512TokenClaims(secretKey, accessToken);
            String userId = claims.get(JwtConstants.JWT_CLAIMS_USER_ID).toString();
            String deviceId = claims.get(JwtConstants.JWT_CLAIMS_DEVICE_ID).toString();
            // 2. 异步添加Token到黑名单(高并发场景)
            asyncAddTokenToBlacklist(accessToken);
            // 3. 删除Redis用户版本号/设备信息
            String versionKey = JwtConstants.REDIS_USER_TOKEN_VERSION_PREFIX + userId;
            String deviceKey = String.format("%s%s:%s", JwtConstants.REDIS_USER_DEVICE_PREFIX, userId, deviceId);
            redisTemplate.delete(versionKey);
            redisTemplate.delete(deviceKey);
        }
        // 4. 清除Token Cookie
        clearTokenCookie(response);
        return true;
    } catch (Exception e) {
        log.error("主动退出失败", e);
        return false;
    }
}
② 被动失效(账号冻结/密码修改,完整代码)
/**
 * 被动失效用户所有Token(账号冻结/密码修改场景)
 *
 * @param userId 用户ID
 * @return true-成功
 */
public boolean passivelyInvalidateAllToken(String userId) {
    if (!StringUtils.hasText(userId)) {
        return false;
    }
    try {
        // 1. 更新Token版本号(使所有旧Token失效)
        generateTokenVersion(userId);
        // 2. 删除用户所有设备绑定信息
        String deviceKeyPattern = String.format("%s%s:*", JwtConstants.REDIS_USER_DEVICE_PREFIX, userId);
        Set<String> deviceKeySet = redisTemplate.keys(deviceKeyPattern);
        if (deviceKeySet != null && !deviceKeySet.isEmpty()) {
            redisTemplate.delete(deviceKeySet);
        }
        // 3. 记录操作日志(生产环境可持久化到数据库)
        log.info("用户{}所有Token被动失效(账号冻结/密码修改)", userId);
        return true;
    } catch (Exception e) {
        log.error("用户{}Token被动失效失败", userId, e);
        return false;
    }
}

3.3 密钥安全(完整工具/存储代码)

(1)密钥存储规范(避免硬编码+配置中心集成)

① 硬编码危害
  • 密钥泄露风险:代码提交到Git仓库、员工离职泄露代码等
  • 无法动态更新:密钥需要修改时,必须重启应用
  • 不符合安全合规:金融/政务系统要求密钥独立存储,不与代码耦合
② Nacos 配置中心集成(完整代码)
  1. Nacos 控制台配置(命名空间:jwt-auth,配置集ID:jwt-config)

    # Nacos 配置内容
    jwt.secret.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    jwt.access.expire=3600
    jwt.refresh.expire=604800
    
  2. 项目中读取Nacos配置(pom.xml 依赖 + application.yml 配置)

    • pom.xml 依赖
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        <version>2021.0.5.0</version>
    </dependency>
    
    • application.yml 配置
    spring:
      cloud:
        nacos:
          config:
            server-addr: 127.0.0.1:8848 # Nacos 地址
            namespace: jwt-auth # 命名空间
            data-id: jwt-config # 配置集ID
            group: DEFAULT_GROUP # 配置分组
            file-extension: properties # 配置格式
    
  3. 项目中注入密钥(与硬编码方式一致,无需修改业务代码)

    @Value("${jwt.secret.key}")
    private String jwtSecretKey;
    
③ Apollo 配置中心集成(完整代码)
  1. Apollo 控制台配置(项目:jwt-auth,集群:default,命名空间:application)
    • 配置项:jwt.secret.key,值:生成的HS512密钥
  2. 项目集成配置
    • pom.xml 依赖
    <dependency>
        <groupId>com.ctrip.framework.apollo</groupId>
        <artifactId>apollo-client</artifactId>
        <version>2.0.1</version>
    </dependency>
    
    • application.yml 配置
    app:
      id: jwt-auth # Apollo 项目ID
    apollo:
      meta: http://127.0.0.1:8080 # Apollo 元数据地址
      bootstrap:
        enabled: true
        namespaces: application # 命名空间
    

(2)密钥轮换流程(完整代码+步骤)

① 轮换核心流程
  1. 生成新密钥(HS512/RSA),上传到配置中心
  2. 应用动态读取新密钥(无需重启,配置中心推送)
  3. 双密钥兼容期(同时支持旧密钥验签、新密钥生成Token)
  4. 淘汰旧密钥(兼容期结束后,仅支持新密钥)
② 完整代码(双密钥兼容+定时轮换)
package com.example.jwtauthframework.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.ArrayList;
import java.util.List;

/**
 * 密钥轮换工具类(支持双密钥兼容+定时轮换)
 *
 * @author xxx
 * @date 2026-01-02
 */
@Slf4j
@Component
@RefreshScope // 支持配置中心动态刷新
public class JwtKeyRotateUtil {

    // 新密钥(配置中心动态配置)
    @Value("${jwt.secret.key.new:}")
    private String newJwtSecretKey;

    // 旧密钥(配置中心动态配置,兼容期使用)
    @Value("${jwt.secret.key.old:}")
    private String oldJwtSecretKey;

    // 密钥列表(存储新密钥+旧密钥,用于验签)
    private List<SecretKey> secretKeyList = new ArrayList<>();

    // 初始化密钥
    @javax.annotation.PostConstruct
    public void initSecretKey() {
        refreshSecretKeyList();
        log.info("密钥轮换工具类初始化成功");
    }

    /**
     * 刷新密钥列表(配置中心推送后自动调用)
     */
    public void refreshSecretKeyList() {
        secretKeyList.clear();
        // 1. 添加新密钥(优先使用)
        if (StringUtils.hasText(newJwtSecretKey)) {
            SecretKey newKey = JwtBaseUtil.getHs512SecretKey(newJwtSecretKey);
            secretKeyList.add(newKey);
            log.info("新密钥已加载到密钥列表");
        }
        // 2. 添加旧密钥(兼容期使用)
        if (StringUtils.hasText(oldJwtSecretKey)) {
            SecretKey oldKey = JwtBaseUtil.getHs512SecretKey(oldJwtSecretKey);
            secretKeyList.add(oldKey);
            log.info("旧密钥已加载到密钥列表(兼容期)");
        }
    }

    /**
     * 获取生成Token的密钥(仅使用新密钥)
     *
     * @return 新密钥
     */
    public SecretKey getGenerateKey() {
        if (secretKeyList.isEmpty()) {
            throw new IllegalStateException("密钥列表为空,无法生成Token");
        }
        // 第一个元素为新密钥
        return secretKeyList.get(0);
    }

    /**
     * 获取验签密钥列表(新密钥+旧密钥,支持兼容期验签)
     *
     * @return 密钥列表
     */
    public List<SecretKey> getVerifyKeyList() {
        return secretKeyList;
    }

    /**
     * 定时清理旧密钥(兼容期结束后,每天凌晨执行)
     * 兼容期默认7天,可配置
     */
    @Scheduled(cron = "0 0 0 * * ?")
    public void scheduledCleanOldKey() {
        // 此处可添加兼容期判断逻辑(如读取配置中心的兼容期结束时间)
        boolean isCompatiblePeriodOver = true; // 模拟兼容期结束
        if (isCompatiblePeriodOver && StringUtils.hasText(oldJwtSecretKey)) {
            oldJwtSecretKey = "";
            refreshSecretKeyList();
            log.info("兼容期结束,旧密钥已清理");
        }
    }
}

四、测试体系模块(完整测试代码,可直接运行验证)

4.1 单元测试示例(完整JUnit 5代码)

(1)JwtBaseUtilTest.java(Token生成/校验/过期测试)

package com.example.jwtauthframework.common.utils;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.crypto.SecretKey;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtBaseUtil 单元测试
 *
 * @author xxx
 * @date 2026-01-02
 */
@SpringBootTest
public class JwtBaseUtilTest {

    private String secretKey;
    private SecretKey hs512Key;
    private Map<String, Object> claims;

    @BeforeEach
    public void init() {
        // 初始化密钥和载荷
        secretKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
        hs512Key = JwtBaseUtil.getHs512SecretKey(secretKey);
        claims = new HashMap<>();
        claims.put("userId", "10001");
        claims.put("username", "testUser");
    }

    @Test
    public void testGenerateHs512Token() {
        // 测试Token生成
        long expireSeconds = 3600;
        String token = JwtBaseUtil.generateHs512Token(hs512Key, claims, expireSeconds);
        Assertions.assertNotNull(token);
        Assertions.assertTrue(token.contains(".")); // JWT格式为header.payload.signature
        log.info("生成的JWT Token:{}", token);
    }

    @Test
    public void testValidateHs512Token() {
        // 测试Token校验
        long expireSeconds = 3600;
        String token = JwtBaseUtil.generateHs512Token(hs512Key, claims, expireSeconds);
        // 有效Token校验
        boolean validResult = JwtBaseUtil.validateHs512Token(hs512Key, token);
        Assertions.assertTrue(validResult);
        // 无效Token(篡改payload)校验
        String invalidToken = token.split("\\.")[0] + ".eyJ1c2VySWQiOiIxMDAwMiIsInVzZXJuYW1lIjoidGVzdFVzZXIyIiwiZXhwIjoxNzM1NjM4NDAwfQ." + token.split("\\.")[2];
        boolean invalidResult = JwtBaseUtil.validateHs512Token(hs512Key, invalidToken);
        Assertions.assertFalse(invalidResult);
    }

    @Test
    public void testParseHs512TokenClaims() {
        // 测试载荷解析
        long expireSeconds = 3600;
        String token = JwtBaseUtil.generateHs512Token(hs512Key, claims, expireSeconds);
        Map<String, Object> parseClaims = JwtBaseUtil.parseHs512TokenClaims(hs512Key, token);
        Assertions.assertNotNull(parseClaims);
        Assertions.assertEquals("10001", parseClaims.get("userId"));
        Assertions.assertEquals("testUser", parseClaims.get("username"));
    }

    @Test
    public void testTokenExpire() {
        // 测试过期Token
        long expireSeconds = 1; // 1秒过期
        String token = JwtBaseUtil.generateHs512Token(hs512Key, claims, expireSeconds);
        // 等待2秒
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        // 校验过期Token
        boolean expireResult = JwtBaseUtil.validateHs512Token(hs512Key, token);
        Assertions.assertFalse(expireResult);
    }
}

(2)JwtRedisUtilTest.java(Redis黑名单拦截测试)

package com.example.jwtauthframework.business.auth.utils;

import com.example.jwtauthframework.common.utils.JwtBaseUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import javax.crypto.SecretKey;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtRedisUtil 单元测试
 *
 * @author xxx
 * @date 2026-01-02
 */
@SpringBootTest
public class JwtRedisUtilTest {

    @Resource
    private JwtRedisUtil jwtRedisUtil;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    private String token;
    private String userId;

    @BeforeEach
    public void init() {
        // 生成测试Token
        userId = "10001";
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("username", "testUser");
        SecretKey secretKey = JwtBaseUtil.getHs512SecretKey(jwtRedisUtil.getJwtSecretKey());
        token = JwtBaseUtil.generateHs512Token(secretKey, claims, 3600);
        // 清空Redis
        redisTemplate.delete(redisTemplate.keys("jwt:*"));
    }

    @Test
    public void testAddTokenToBlacklist() {
        // 测试添加黑名单
        boolean addResult = jwtRedisUtil.addTokenToBlacklist(token, 3600);
        Assertions.assertTrue(addResult);
        // 校验是否存在于Redis
        String pureToken = JwtBaseUtil.removeTokenPrefix(token);
        String blacklistKey = "jwt:user:blacklist:" + pureToken;
        Boolean exists = redisTemplate.hasKey(blacklistKey);
        Assertions.assertTrue(exists);
    }

    @Test
    public void testIsTokenInBlacklist() {
        // 测试黑名单拦截
        // 1. 未添加黑名单,返回false
        boolean notInBlacklist = jwtRedisUtil.isTokenInBlacklist(token);
        Assertions.assertFalse(notInBlacklist);
        // 2. 添加黑名单,返回true
        jwtRedisUtil.addTokenToBlacklist(token, 3600);
        boolean inBlacklist = jwtRedisUtil.isTokenInBlacklist(token);
        Assertions.assertTrue(inBlacklist);
    }
}

(3)JwtRefreshUtilTest.java(双Token无感刷新测试)

package com.example.jwtauthframework.business.auth.utils;

import com.example.jwtauthframework.common.dto.TokenDTO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletResponse;

import javax.annotation.Resource;

/**
 * JwtRefreshUtil 单元测试
 *
 * @author xxx
 * @date 2026-01-02
 */
@SpringBootTest
public class JwtRefreshUtilTest {

    @Resource
    private JwtRefreshUtil jwtRefreshUtil;

    @Test
    public void testGenerateLoginToken() {
        // 测试双Token生成
        MockHttpServletResponse response = new MockHttpServletResponse();
        String userId = "20001";
        String username = "testTocUser";
        TokenDTO tokenDTO = jwtRefreshUtil.generateLoginToken(response, userId, username);
        // 断言结果
        Assertions.assertNotNull(tokenDTO);
        Assertions.assertNotNull(tokenDTO.getAccessToken());
        Assertions.assertNotNull(tokenDTO.getRefreshToken());
        Assertions.assertEquals(3600, tokenDTO.getAccessTokenExpireSeconds());
        Assertions.assertEquals(604800, tokenDTO.getRefreshTokenExpireSeconds());
    }

    @Test
    public void testNeedRefreshAccessToken() {
        // 测试是否需要无感刷新
        MockHttpServletResponse response = new MockHttpServletResponse();
        TokenDTO tokenDTO = jwtRefreshUtil.generateLoginToken(response, "20001", "testTocUser");
        // 1. 刚生成的Token,无需刷新
        boolean needRefresh = jwtRefreshUtil.needRefreshAccessToken(tokenDTO.getAccessToken());
        Assertions.assertFalse(needRefresh);
        // 此处可手动修改Token过期时间,测试需要刷新的场景
    }
}

(4)JwtBloomFilterTest.java(布隆过滤器查询/误判率测试)

package com.example.jwtauthframework.business.auth.utils;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
 * JwtBloomFilter 单元测试
 *
 * @author xxx
 * @date 2026-01-02
 */
@SpringBootTest
public class JwtBloomFilterTest {

    @Resource
    private JwtBloomFilter jwtBloomFilter;

    @Test
    public void testAddAndMightContainToken() {
        // 测试添加并查询
        String token = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOiIxMDAwMSIsInVzZXJuYW1lIjoidGVzdFVzZXIiLCJleHAiOjE3MzU2Mzg0MDB9.xxx";
        // 1. 添加前,不存在
        boolean beforeAdd = jwtBloomFilter.mightContainToken(token);
        Assertions.assertFalse(beforeAdd);
        // 2. 添加后,存在
        jwtBloomFilter.addToken(token);
        boolean afterAdd = jwtBloomFilter.mightContainToken(token);
        Assertions.assertTrue(afterAdd);
    }

    @Test
    public void testFalsePositiveRate() {
        // 测试误判率(是否符合配置的0.001)
        int expectedInsertions = 10000;
        Set<String> tokenSet = new HashSet<>();
        Random random = new Random();
        // 1. 添加10000个随机Token
        for (int i = 0; i < expectedInsertions; i++) {
            String token = "Bearer_" + random.nextLong();
            tokenSet.add(token);
            jwtBloomFilter.addToken(token);
        }
        // 2. 测试1000个未添加的Token,统计误判数
        int falsePositiveCount = 0;
        int testCount = 1000;
        for (int i = 0; i < testCount; i++) {
            String testToken = "Test_Bearer_" + random.nextLong();
            if (!tokenSet.contains(testToken) && jwtBloomFilter.mightContainToken(testToken)) {
                falsePositiveCount++;
            }
        }
        // 3. 断言误判率≤0.001
        double falsePositiveRate = (double) falsePositiveCount / testCount;
        log.info("布隆过滤器实际误判率:{},预期误判率:0.001", falsePositiveRate);
        Assertions.assertTrue(falsePositiveRate <= 0.001);
    }
}

4.2 集成测试流程(完整测试步骤 + 接口调用代码)

(1)各场景集成测试(Postman + HttpClient代码)

① Postman 接口调用顺序(以场景3为例)
  1. 登录接口:POST http://localhost:8080/api/auth/login
    • 请求体:{"username":"user001","password":"123456","deviceId":"PC_Windows_001","deviceType":"PC"}
    • 预期结果:200 OK,返回双Token
  2. 鉴权测试接口:GET http://localhost:8080/api/auth/test
    • 请求头:Authorization: Bearer {accessToken}
    • 预期结果:200 OK,返回用户信息
  3. 无感刷新接口:POST http://localhost:8080/api/auth/refresh-token
    • 请求头:Refresh-Token: Bearer {refreshToken}
    • 预期结果:200 OK,返回新双Token
  4. 退出登录接口:POST http://localhost:8080/api/auth/logout
    • 请求头:Authorization: Bearer {accessToken}
    • 预期结果:200 OK,提示退出成功
② HttpClient 完整代码(Java)
package com.example.jwtauthframework.test;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;

/**
 * 集成测试 HttpClient 代码
 *
 * @author xxx
 * @date 2026-01-02
 */
public class AuthIntegrationTest {

    private static final String BASE_URL = "http://localhost:8080/api/auth";
    private String accessToken;
    private String refreshToken;

    @Test
    public void testAuthFlow() throws Exception {
        // 1. 登录
        loginTest();
        // 2. 鉴权
        authTest();
        // 3. 刷新Token
        refreshTokenTest();
        // 4. 退出登录
        logoutTest();
    }

    private void loginTest() throws Exception {
        CloseableHttpClient client = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(BASE_URL + "/login");
        // 请求体
        String requestBody = "{\"username\":\"user001\",\"password\":\"123456\",\"deviceId\":\"PC_Windows_001\",\"deviceType\":\"PC\"}";
        StringEntity entity = new StringEntity(requestBody, StandardCharsets.UTF_8);
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        // 执行请求
        CloseableHttpResponse response = client.execute(httpPost);
        HttpEntity responseEntity = response.getEntity();
        String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
        System.out.println("登录结果:" + result);
        // 提取Token(实际可通过JSON工具解析,如FastJSON)
        accessToken = "xxx"; // 替换为实际返回的accessToken
        refreshToken = "xxx"; // 替换为实际返回的refreshToken
        client.close();
        response.close();
    }

    private void authTest() throws Exception {
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet(BASE_URL + "/test");
        // 设置请求头
        httpGet.setHeader("Authorization", "Bearer " + accessToken);
        // 执行请求
        CloseableHttpResponse response = client.execute(httpGet);
        HttpEntity responseEntity = response.getEntity();
        String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
        System.out.println("鉴权结果:" + result);
        client.close();
        response.close();
    }

    private void refreshTokenTest() throws Exception {
        CloseableHttpClient client = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(BASE_URL + "/refresh-token");
        // 设置请求头
        httpPost.setHeader("Refresh-Token", "Bearer " + refreshToken);
        // 执行请求
        CloseableHttpResponse response = client.execute(httpPost);
        HttpEntity responseEntity = response.getEntity();
        String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
        System.out.println("刷新Token结果:" + result);
        client.close();
        response.close();
    }

    private void logoutTest() throws Exception {
        CloseableHttpClient client = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(BASE_URL + "/logout");
        // 设置请求头
        httpPost.setHeader("Authorization", "Bearer " + accessToken);
        // 执行请求
        CloseableHttpResponse response = client.execute(httpPost);
        HttpEntity responseEntity = response.getEntity();
        String result = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
        System.out.println("退出登录结果:" + result);
        client.close();
        response.close();
    }
}

(2)高并发压力测试(JMeter 完整配置)

① 测试计划配置
  1. 新建测试计划,命名为“JWT认证高并发测试”
  2. 添加线程组:线程数1000,Ramp-Up时间10秒,循环次数100次
  3. 添加配置元件:HTTP信息头管理器(添加Authorization: Bearer {accessToken}
  4. 添加取样器:HTTP请求(按接口配置)
  5. 添加监听器:聚合报告、查看结果树、Summary Report
② 核心接口配置(鉴权接口为例)
  • 请求方法:GET
  • 请求路径:http://localhost:8080/api/auth/test
  • 请求头:Authorization: Bearer {accessToken}(替换为有效Token)
  • 断言:响应码200,响应内容包含“鉴权成功”
③ 预期结果(参考场景4)
  • 平均响应时间 < 50ms
  • 吞吐量 > 2000 QPS
  • 错误率 < 0.1%
  • Redis QPS < 500

4.3 问题排查指南(配套日志配置完整代码)

(1)日志打印规范(logback.xml 完整配置)

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 日志输出格式 -->
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n" />
    <!-- 日志存储路径 -->
    <property name="LOG_PATH" value="./logs/jwt-auth" />

    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 业务日志文件输出(按天滚动) -->
    <appender name="BUSINESS_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/business.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/business.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory> <!-- 保留30天日志 -->
        </rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 错误日志文件输出(按天滚动) -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- JWT核心日志(单独输出,便于排查) -->
    <logger name="com.example.jwtauthframework.business.auth.utils" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="BUSINESS_FILE" />
    </logger>

    <!-- 根日志配置 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="BUSINESS_FILE" />
        <appender-ref ref="ERROR_FILE" />
    </root>
</configuration>

(2)常见异常排查

异常类型 排查步骤 解决方案
Token解析失败 1. 检查Token格式是否正确(是否包含Bearer前缀)
2. 检查密钥是否一致(客户端/服务端)
3. 检查Token是否被篡改
4. 查看日志中具体的解析错误信息
1. 确保Token格式正确,携带Bearer前缀
2. 统一客户端与服务端密钥
3. 重新生成有效Token
4. 修复Token篡改问题
跨域访问失败 1. 检查服务端是否配置CORS
2. 检查Cookie的SameSite属性
3. 检查HTTPS下Secure属性是否配置正确
1. 配置CORS允许前端域名访问
2. 跨域场景设置SameSite=None + Secure=true
3. 统一前后端协议(HTTP/HTTPS)
Redis连接失败 1. 检查Redis服务是否启动
2. 检查Redis配置(地址/端口/密码/数据库)
3. 检查防火墙是否开放Redis端口
4. 检查Redis连接池配置
1. 启动Redis服务
2. 修正Redis配置与项目一致
3. 开放Redis端口(6379)
4. 优化Redis连接池参数
Token过期 1. 检查Token过期时间配置
2. 检查是否触发无感刷新
3. 检查Refresh Token是否有效
1. 调整合理的过期时间
2. 修复无感刷新逻辑
3. 用有效Refresh Token刷新获取新Token
布隆过滤器误判率过高 1. 检查预计插入量与实际存储量
2. 检查误判率配置
3. 检查是否定期重建布隆过滤器
1. 增大预计插入量配置
2. 降低误判率(需增加内存)
3. 缩短布隆过滤器重建周期

五、生产部署指引(完整部署配置代码)

5.1 部署前置检查清单(配套配置校验代码)

(1)ConfigCheckUtil.java 完整工具类

package com.example.jwtauthframework.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.crypto.SecretKey;

/**
 * 部署前置配置校验工具类
 *
 * @author xxx
 * @date 2026-01-02
 */
@Slf4j
@Component
public class ConfigCheckUtil {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Value("${jwt.secret.key:}")
    private String jwtSecretKey;

    @Value("${jwt.access.expire:0}")
    private long accessTokenExpire;

    @Value("${jwt.refresh.expire:0}")
    private long refreshTokenExpire;

    @Value("${spring.data.redis.host:}")
    private String redisHost;

    @Value("${spring.data.redis.port:0}")
    private int redisPort;

    /**
     * 执行部署前置配置校验
     *
     * @return true-校验通过,false-校验失败
     */
    public boolean executePreDeployCheck() {
        log.info("开始执行部署前置配置校验");
        boolean result = true;
        // 1. JWT密钥校验
        if (!checkJwtSecretKey()) {
            result = false;
        }
        // 2. Token过期时间校验
        if (!checkTokenExpireTime()) {
            result = false;
        }
        // 3. Redis连接校验
        if (!checkRedisConnection()) {
            result = false;
        }
        // 4. 安全配置校验(可扩展)
        if (!checkSecurityConfig()) {
            result = false;
        }
        if (result) {
            log.info("部署前置配置校验全部通过");
        } else {
            log.error("部署前置配置校验存在失败项,请修复后重新部署");
        }
        return result;
    }

    /**
     * 校验JWT密钥
     */
    private boolean checkJwtSecretKey() {
        if (!StringUtils.hasText(jwtSecretKey) || jwtSecretKey.length() < 32) {
            log.error("JWT密钥校验失败:密钥不能为空且长度不小于32位");
            return false;
        }
        try {
            SecretKey secretKey = JwtBaseUtil.getHs512SecretKey(jwtSecretKey);
            log.info("JWT密钥校验通过");
            return true;
        } catch (Exception e) {
            log.error("JWT密钥校验失败:密钥格式无效", e);
            return false;
        }
    }

    /**
     * 校验Token过期时间
     */
    private boolean checkTokenExpireTime() {
        if (accessTokenExpire <= 0 || refreshTokenExpire <= 0) {
            log.error("Token过期时间校验失败:过期时间必须大于0");
            return false;
        }
        if (refreshTokenExpire <= accessTokenExpire) {
            log.error("Token过期时间校验失败:Refresh Token过期时间必须大于Access Token");
            return false;
        }
        log.info("Token过期时间校验通过");
        return true;
    }

    /**
     * 校验Redis连接
     */
    private boolean checkRedisConnection() {
        if (!StringUtils.hasText(redisHost) || redisPort <= 0) {
            log.error("Redis连接校验失败:Redis地址和端口不能为空且有效");
            return false;
        }
        try {
            redisTemplate.opsForValue().set("deploy:check:redis", "success", 60);
            String value = (String) redisTemplate.opsForValue().get("deploy:check:redis");
            if ("success".equals(value)) {
                log.info("Redis连接校验通过");
                return true;
            } else {
                log.error("Redis连接校验失败:Redis写入或读取失败");
                return false;
            }
        } catch (Exception e) {
            log.error("Redis连接校验失败:连接异常", e);
            return false;
        }
    }

    /**
     * 校验安全配置
     */
    private boolean checkSecurityConfig() {
        // 示例:校验Cookie安全属性、HTTPS配置等
        log.info("安全配置校验通过");
        return true;
    }
}

(2)Shell脚本(环境依赖检查)

#!/bin/bash
# 部署前置环境依赖检查脚本
# 执行命令:sh pre-deploy-check.sh

echo "==================== 开始环境依赖检查 ===================="

# 1. 检查JDK版本
JAVA_VERSION=$(java -version 2>&1 | grep -i version | awk -F '"' '{print $2}' | cut -d '.' -f 1-2)
echo "当前JDK版本:$JAVA_VERSION"
if [[ $(echo "$JAVA_VERSION < 1.8" | bc) -eq 1 ]]; then
    echo "错误:JDK版本需不低于1.8,请升级JDK"
    exit 1
else
    echo "JDK版本校验通过"
fi

# 2. 检查Redis是否启动
REDIS_PING=$(redis-cli -h 127.0.0.1 -p 6379 -a 123456 ping 2>/dev/null)
if [[ "$REDIS_PING" == "PONG" ]]; then
    echo "Redis服务已启动,校验通过"
else
    echo "错误:Redis服务未启动或连接失败,请检查Redis配置"
    exit 1
fi

# 3. 检查端口是否占用(以8080为例)
PORT_OCCUPIED=$(netstat -tulpn | grep 8080 | wc -l)
if [[ $PORT_OCCUPIED -gt 0 ]]; then
    echo "警告:8080端口已被占用,建议修改应用端口"
else
    echo "8080端口未被占用,校验通过"
fi

# 4. 检查磁盘空间
DISK_USAGE=$(df -h / | grep / | awk '{print $5}' | sed 's/%//g')
if [[ $DISK_USAGE -gt 80 ]]; then
    echo "警告:根目录磁盘使用率超过80%,请清理磁盘空间"
else
    echo "磁盘空间使用率:$DISK_USAGE%,校验通过"
fi

echo "==================== 环境依赖检查完成 ===================="
exit 0

5.2 高可用部署建议(完整配置代码)

(1)微服务认证服务集群(Nginx 负载均衡配置)

# Nginx 负载均衡配置(认证服务集群)
http {
    # 定义认证服务集群
    upstream jwt-auth-cluster {
        server 192.168.1.100:8080 weight=1; # 节点1
        server 192.168.1.101:8080 weight=1; # 节点2
        server 192.168.1.102:8080 weight=1; # 节点3
        ip_hash; # 会话粘性,确保同一用户请求分发到同一节点(可选)
    }

    # HTTPS服务器配置
    server {
        listen 443 ssl;
        server_name auth.example.com; # 生产环境域名

        # SSL配置
        ssl_certificate      /usr/local/nginx/ssl/auth.crt;
        ssl_certificate_key  /usr/local/nginx/ssl/auth.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256;
        ssl_prefer_server_ciphers on;

        # 反向代理到认证服务集群
        location / {
            proxy_pass http://jwt-auth-cluster;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # 健康检查接口(Spring Boot Actuator)
        location /actuator/health {
            proxy_pass http://jwt-auth-cluster;
            proxy_set_header Host $host;
        }
    }

    # HTTP重定向到HTTPS
    server {
        listen 80;
        server_name auth.example.com;
        rewrite ^(.*)$ https://$host$1 permanent;
    }
}

(2)Redis Cluster 完整配置(3主3从)

① 配置文件(redis-7000.conf 示例,其他端口修改对应参数)
# 端口
port 7000
# 集群启用
cluster-enabled yes
# 集群配置文件
cluster-config-file nodes-7000.conf
# 集群超时时间
cluster-node-timeout 15000
# 数据目录
dir ./data/7000
# 后台运行
daemonize yes
# 日志文件
logfile ./logs/redis-7000.log
# 绑定IP(生产环境替换为内网IP)
bind 0.0.0.0
# 密码
requirepass 123456
# 主从复制密码
masterauth 123456
# 开启AOF持久化
appendonly yes
appendfilename "appendonly-7000.aof"
② 启动命令(批量启动6个节点)
#!/bin/bash
# 启动Redis Cluster节点
redis-server redis-7000.conf
redis-server redis-7001.conf
redis-server redis-7002.conf
redis-server redis-7003.conf
redis-server redis-7004.conf
redis-server redis-7005.conf

# 创建Redis Cluster(3主3从)
redis-cli -a 123456 --cluster create 192.168.1.100:7000 192.168.1.100:7001 192.168.1.100:7002 192.168.1.100:7003 192.168.1.100:7004 192.168.1.100:7005 --cluster-replicas 1
③ 项目中Redis Cluster配置(application.yml
spring:
  data:
    redis:
      cluster:
        nodes: 192.168.1.100:7000,192.168.1.100:7001,192.168.1.100:7002,192.168.1.100:7003,192.168.1.100:7004,192.168.1.100:7005
      password: 123456
      lettuce:
        pool:
          max-active: 32
          max-idle: 32
          min-idle: 8
          max-wait: -1ms
      timeout: 3000ms

5.3 监控告警配置(完整监控代码)

(1)Spring Boot Actuator + Prometheus 配置

pom.xml 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
application.yml 配置
# Actuator 配置
management:
  endpoints:
    web:
      exposure:
        include: prometheus,health,info,metrics # 暴露监控端点
      base-path: /actuator # 基础路径
  endpoint:
    health:
      show-details: always # 显示健康检查详细信息
  metrics:
    tags:
      application: jwt-auth # 应用标识
    export:
      prometheus:
        enabled: true # 启用Prometheus导出
    # 自定义JWT监控指标
    web:
      server:
        request:
          metrics:
            enabled: true
    registry:
      prometheus:
        enabled: true
③ 自定义JWT监控指标(示例)
package com.example.jwtauthframework.common.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * 自定义JWT监控指标
 *
 * @author xxx
 * @date 2026-01-02
 */
@Component
public class JwtMetricsUtil {

    @Resource
    private MeterRegistry meterRegistry;

    // Token生成成功计数器
    private Counter tokenGenerateSuccessCounter;
    // Token生成失败计数器
    private Counter tokenGenerateFailCounter;
    // Token校验失败计数器
    private Counter tokenVerifyFailCounter;

    @PostConstruct
    public void initMetrics() {
        // 初始化计数器
        tokenGenerateSuccessCounter = Counter.builder("jwt.token.generate.success")
                .description("JWT Token生成成功次数")
                .register(meterRegistry);
        tokenGenerateFailCounter = Counter.builder("jwt.token.generate.fail")
                .description("JWT Token生成失败次数")
                .register(meterRegistry);
        tokenVerifyFailCounter = Counter.builder("jwt.token.verify.fail")
                .description("JWT Token校验失败次数")
                .register(meterRegistry);
    }

    /**
     * 记录Token生成成功
     */
    public void recordTokenGenerateSuccess() {
        tokenGenerateSuccessCounter.increment();
    }

    /**
     * 记录Token生成失败
     */
    public void recordTokenGenerateFail() {
        tokenGenerateFailCounter.increment();
    }

    /**
     * 记录Token校验失败
     */
    public void recordTokenVerifyFail() {
        tokenVerifyFailCounter.increment();
    }
}

(2)Grafana 告警规则(JSON 示例)

{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "id": 1,
  "links": [],
  "panels": [
    {
      "collapsed": false,
      "datasource": "Prometheus",
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 0
      },
      "id": 1,
      "panels": [],
      "title": "JWT 监控指标",
      "type": "row"
    },
    {
      "datasource": "Prometheus",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisLabel": "次数/分钟",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 0,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 100
              }
            ]
          },
          "unit": "count"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 1
      },
      "id": 2,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "expr": "rate(jwt_token_verify_fail_total[1m])",
          "interval": "",
          "legendFormat": "Token校验失败次数",
          "refId": "A"
        }
      ],
      "title": "Token校验失败次数",
      "type": "timeseries"
    }
  ],
  "refresh": "5s",
  "schemaVersion": 38,
  "style": "dark",
  "tags": [],
  "templating": {
    "list": []
  },
  "time": {
    "from": "now-1h",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "JWT 认证监控面板",
  "uid": "jwt-auth-monitor",
  "version": 1,
  "weekStart": 0,
  "alerts": [
    {
      "alertRuleTags": {},
      "annotations": {
        "description": "Token校验失败次数超过阈值({{ $value }}次/分钟),请排查问题",
        "summary": "JWT Token校验失败告警"
      },
      "condition": "A > 100",
      "datasourceUid": "prometheus",
      "evaluationInterval": "1m",
      "expr": "rate(jwt_token_verify_fail_total[1m]) > 100",
      "for": "5m",
      "frequency": "1m",
      "grafanaManagedAlert": true,
      "id": 1,
      "labels": {},
      "name": "Token校验失败告警",
      "noDataState": "NoData",
      "notifications": [
        {
          "uid": "email-notification" // 邮件通知UID,需提前配置
        }
      ],
      "severity": "critical",
      "title": "Token校验失败告警"
    }
  ]
}

六、快速选型与参考

6.1 场景选型决策树(文字流程图)

  1. 第一步:判断系统规模与用户类型
    • 小型系统/内部系统(用户量<1万,仅内部使用)→ 选择「基础方案:JWT + Redis」
    • 中大型系统/外部ToC产品(用户量≥10万,多端访问)→ 进入第二步
  2. 第二步:判断核心需求
    • 核心需求:用户体验优先(无感登录)→ 选择「场景3:JWT + 双Token + Redis + 无感刷新」
    • 核心需求:高并发/黑名单量大(百万级黑名单,QPS≥2000)→ 选择「场景4:JWT + 双Token + Redis + 布隆过滤器」
    • 核心需求:多设备管控/精细化权限(设备数量限制,按设备分配权限)→ 选择「场景5:JWT + 双Token + Redis(用户+设备)」
    • 核心需求:微服务/SSO/第三方授权(多系统统一认证,第三方登录)→ 选择「场景6:JWT + Spring Security/Authorization Server」
  3. 第三步:安全需求补充
    • 高安全需求(金融/政务)→ 集成「安全强化模块」(HTTPS、密钥轮换、令牌吊销等)
    • 普通安全需求 → 基础配置即可满足

6.2 核心配置参数对照表

场景类型 核心参数 建议值 说明
小型内部系统 jwt.access.expire 7200秒(2小时) 体验优先,无需频繁登录
小型内部系统 jwt.refresh.expire 2592000秒(30天) 长期有效,减少登录次数
中大型ToC产品 jwt.access.expire 3600秒(1小时) 平衡安全与体验
中大型ToC产品 jwt.refresh.expire 604800秒(7天) 常用设备免登
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐