Java基础 | JWT登录场景化最优方案(二)
·
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代码)
- 扩容触发条件:当前存储量/预计插入量 ≥ 扩容阈值(默认0.8)
- 扩容核心逻辑:生成新版本布隆过滤器 → 从Redis黑名单批量加载数据 → 切换版本标识 → 旧版本逐步淘汰
- 避免误判率升高:定期重建布隆过滤器(定时任务,默认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 配置中心集成(完整代码)
-
Nacos 控制台配置(命名空间:jwt-auth,配置集ID:jwt-config)
# Nacos 配置内容 jwt.secret.key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx jwt.access.expire=3600 jwt.refresh.expire=604800 -
项目中读取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 # 配置格式 -
项目中注入密钥(与硬编码方式一致,无需修改业务代码)
@Value("${jwt.secret.key}") private String jwtSecretKey;
③ Apollo 配置中心集成(完整代码)
- Apollo 控制台配置(项目:jwt-auth,集群:default,命名空间:application)
- 配置项:
jwt.secret.key,值:生成的HS512密钥
- 配置项:
- 项目集成配置
- 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)密钥轮换流程(完整代码+步骤)
① 轮换核心流程
- 生成新密钥(HS512/RSA),上传到配置中心
- 应用动态读取新密钥(无需重启,配置中心推送)
- 双密钥兼容期(同时支持旧密钥验签、新密钥生成Token)
- 淘汰旧密钥(兼容期结束后,仅支持新密钥)
② 完整代码(双密钥兼容+定时轮换)
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为例)
- 登录接口:
POST http://localhost:8080/api/auth/login- 请求体:
{"username":"user001","password":"123456","deviceId":"PC_Windows_001","deviceType":"PC"} - 预期结果:200 OK,返回双Token
- 请求体:
- 鉴权测试接口:
GET http://localhost:8080/api/auth/test- 请求头:
Authorization: Bearer {accessToken} - 预期结果:200 OK,返回用户信息
- 请求头:
- 无感刷新接口:
POST http://localhost:8080/api/auth/refresh-token- 请求头:
Refresh-Token: Bearer {refreshToken} - 预期结果:200 OK,返回新双Token
- 请求头:
- 退出登录接口:
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 完整配置)
① 测试计划配置
- 新建测试计划,命名为“JWT认证高并发测试”
- 添加线程组:线程数1000,Ramp-Up时间10秒,循环次数100次
- 添加配置元件:HTTP信息头管理器(添加
Authorization: Bearer {accessToken}) - 添加取样器:HTTP请求(按接口配置)
- 添加监听器:聚合报告、查看结果树、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万,仅内部使用)→ 选择「基础方案:JWT + Redis」
- 中大型系统/外部ToC产品(用户量≥10万,多端访问)→ 进入第二步
- 第二步:判断核心需求
- 核心需求:用户体验优先(无感登录)→ 选择「场景3:JWT + 双Token + Redis + 无感刷新」
- 核心需求:高并发/黑名单量大(百万级黑名单,QPS≥2000)→ 选择「场景4:JWT + 双Token + Redis + 布隆过滤器」
- 核心需求:多设备管控/精细化权限(设备数量限制,按设备分配权限)→ 选择「场景5:JWT + 双Token + Redis(用户+设备)」
- 核心需求:微服务/SSO/第三方授权(多系统统一认证,第三方登录)→ 选择「场景6:JWT + Spring Security/Authorization Server」
- 第三步:安全需求补充
- 高安全需求(金融/政务)→ 集成「安全强化模块」(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天) | 常用设备免登 |
更多推荐

所有评论(0)