资讯动态

JWT认证实战:前后端分离下的无状态登录方案设计与落地

发布时间:2026/10/15 19:15:35 来源:尧图企业网站定制
1. 项目概述为什么前后端分离时代JWT成了认证方案的“默认选项”你打开一个现代网页点登录输完账号密码页面没刷新就跳转到了个人中心——背后大概率跑着一套基于JWT的认证流程。这不是玄学而是过去五年里几乎所有新启动的Web项目在做用户认证时几乎都会把JWT列为首选方案。它不依赖服务端session存储天然适配前后端分离架构它把用户身份信息打包进一个字符串前端存localStorage、发请求时带在Authorization头里后端只校验签名、不查数据库性能和扩展性都立竿见影。我参与过的7个中型以上项目里有6个在初期技术选型会上JWT都是被第一个写上白板的方案。它不是银弹但确实是当前工程实践中在安全性、开发效率、部署灵活性三者之间找到的最务实平衡点。如果你正在用VueSpring Boot、ReactNode.js、或者FlutterDjango搭新系统又卡在“怎么让登录态跨页面、跨请求、跨域名稳定存在”这个问题上那这篇内容就是为你写的。它不讲抽象理论只拆解真实项目里从生成token、存哪里、怎么传、怎么验、怎么续期、怎么注销到踩过哪些坑、为什么这么设计——全部来自一线落地经验。2. 整体设计思路与方案选型逻辑为什么不是Session为什么不是OAuth2为什么是JWT2.1 前后端分离带来的根本性挑战传统单体应用里用户登录成功服务端在内存或Redis里存一个session ID浏览器自动带cookie服务端靠这个ID查用户信息。但前后端分离后前端是独立部署的静态资源比如Nginx托管的Vue打包文件后端是另一套API服务比如部署在K8s里的Java微服务。这时候问题就来了跨域问题前端域名是app.example.com后端API是api.example.com浏览器默认禁止跨域携带cookie除非双方都显式配置withCredentials: true和Access-Control-Allow-Origin精确匹配稍有疏忽就401状态耦合每个后端实例都要能访问共享session存储如Redis集群扩容时session同步成本高且一旦Redis挂了整个登录态就崩移动端不友好App、小程序、桌面客户端无法像浏览器那样自动管理cookie每次请求都得手动拼header开发体验差。JWT的出现本质上是对“无状态认证”的一次工程化落地。它把“谁登录了、权限是什么、有效期到几点”这些信息用加密签名的方式打包成一个字符串前端自己保管、自己传递后端只负责验证签名是否有效、内容是否过期完全不依赖外部存储。这就彻底解耦了认证状态与服务端实例。2.2 为什么不是直接用OAuth2OAuth2是授权框架不是认证协议。它解决的是“A应用想访问B应用的用户数据B如何安全地把访问权限给A”这个问题。比如微信登录、GitHub第三方授权。而我们日常做的“用户输入账号密码系统确认身份并允许访问个人数据”这叫Authentication认证不是Authorization授权。强行用OAuth2做内部认证等于拿扳手当螺丝刀——能拧但多绕三道弯要建授权服务器、要处理code exchange、要维护client_secret、要区分implicit/code flow……我见过有团队为实现一个内部员工系统登录硬上了Spring Security OAuth2结果光调试redirect_uri就花了两天最后发现90%的代码都在处理OAuth2的样板逻辑真正跟业务相关的认证逻辑不到50行。JWT是轻量级认证载体OAuth2是重型授权协议目标不同不该混用。2.3 JWT结构解析Header.Payload.Signature到底装了什么一个典型的JWT长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c它由三段Base64Url编码字符串用点号连接分别对应Header头部声明签名算法如HS256、RS256和token类型JWT。例如{alg:HS256,typ:JWT}。这里的关键是算法选择HS256是HMAC对称加密密钥由前后端共用RS256是RSA非对称加密后端用私钥签名前端或另一个服务用公钥验签。中小项目用HS256足够简单高效金融级系统必须用RS256避免密钥泄露导致所有token可伪造。Payload载荷存放实际数据分为注册声明如iss签发者、exp过期时间、公共声明自定义字段如userId、role和私有声明业务字段如departmentId。注意Payload是Base64编码不是加密任何人拿到token都能解码看到内容。所以绝对不能放密码、身份证号、银行卡号等敏感信息。我曾在一个医疗项目里看到有人把患者手机号明文塞进payload后来被安全审计直接打回重做——正确做法是只放不可逆的标识符如用户ID哈希值敏感数据留存在服务端关联查询。Signature签名对base64UrlEncode(header) . base64UrlEncode(payload)这个字符串用指定算法和密钥进行签名。这是JWT防篡改的核心。后端验签时用同样密钥重新计算签名比对是否一致。只要密钥不泄露攻击者就无法伪造合法token。提示JWT不是用来“存储用户数据”的而是用来“证明用户身份已通过验证”的凭证。它的Payload应该尽可能精简只放认证必需字段。一个超过1KB的token不仅增加网络传输负担还可能触发某些网关的header大小限制如Nginx默认limit为4KB。2.4 方案对比JWT vs Session vs Cookie-only对比维度JWT方案传统Session纯Cookie方案状态管理无状态token自包含所有认证信息有状态依赖服务端session存储有状态依赖服务端sessioncookie绑定跨域支持天然支持前端控制header发送需复杂CORScookie配置易出错同Session且cookie默认不跨域扩展性极佳后端任意节点均可独立验签中等需共享session存储如Redis差session绑定具体服务器时无法水平扩展安全性风险token泄露即失权需防范XSS窃取localStoragesession ID泄露风险类似但可服务端主动失效同Session且cookie易受CSRF攻击移动端适配极佳所有HTTP客户端均可手动设置Authorization头差需模拟cookie机制差同Session调试难度低token可在线解码查看内容中需查日志或Redis中需查cookie和session存储从表中能清晰看出JWT在前后端分离场景下是综合得分最高的方案。它的代价是“无法主动让token失效”——因为服务端不存token状态只能等它自然过期。但这不是死局后面会讲如何用Redis黑名单、短生命周期刷新机制来弥补。3. 核心细节解析与实操要点从生成到销毁的全链路关键控制点3.1 Token生成密钥管理、过期策略与载荷设计生成JWT不是一行代码的事每个参数都影响系统安全水位。密钥管理绝不能写死在代码里我见过最危险的操作是把secretKey my-super-secret-key硬编码在Java的application.properties里还提交到了Git仓库。正确做法是生产环境从环境变量读取String secret System.getenv(JWT_SECRET);或使用配置中心如Apollo、Nacos动态下发密钥长度至少32字节HS256要求推荐用openssl rand -base64 32生成定期轮换密钥如每季度轮换期间需支持新旧密钥并存验签平滑过渡。过期时间exp设定这是安全与体验的博弈点。过短如5分钟用户频繁被登出体验差且刷新token请求暴增过长如7天token一旦泄露攻击者有超长窗口期实践建议访问tokenaccess_token设为15-30分钟刷新tokenrefresh_token设为7天。前者用于常规API调用后者仅用于换取新的access_token且必须严格保护存httpOnly cookie不暴露给JS。载荷Payload字段设计必填标准字段subsubject通常为用户ID、iatissued at签发时间、expexpiration time推荐业务字段role角色用于RBAC权限判断、perms权限列表如[user:read,order:write]避免每次请求查DB绝对禁用字段password、idCard、phone等明文敏感信息sessionId等冗余字段JWT本身已是会话凭证。// Java示例使用jjwt库生成token String jwt Jwts.builder() .setSubject(String.valueOf(userId)) // sub: 用户唯一标识 .claim(role, admin) // 自定义角色 .claim(perms, Arrays.asList(user:read, user:write)) .setIssuedAt(new Date()) // iat: 签发时间 .setExpiration(new Date(System.currentTimeMillis() 30 * 60 * 1000)) // exp: 30分钟 .signWith(SignatureAlgorithm.HS256, secretKey) // 使用HS256算法和密钥签名 .compact();3.2 前端存储与传输localStorage、sessionStorage还是HttpOnly Cookie这是前端同学最容易踩坑的地方。三种方式各有生死线localStorage/sessionStorage✅ 优点JS可读写方便前端控制token生命周期sessionStorage关闭标签页即失效更安全❌ 缺点完全暴露在XSS攻击下。只要页面有任意一处未过滤的script注入攻击者就能localStorage.getItem(token)盗走 结论仅适用于非敏感场景如内部工具系统且必须配合严格CSP策略和XSS防护。生产环境面向公众的系统不推荐。HttpOnly Cookie✅ 优点JS无法读取彻底免疫XSS窃取浏览器自动携带减少前端代码量❌ 缺点需处理跨域CORS配置credentials: includeAccess-Control-Allow-Credentials: true无法像localStorage那样灵活控制过期依赖cookie max-age 结论生产环境首选方案。将access_token存localStorage风险太高而refresh_token必须存HttpOnly Cookie——这是行业共识。混合方案推荐access_token存localStorage用于常规API请求因需JS读取拼接Authorization头refresh_token存HttpOnly、Secure、SameSiteStrict的Cookie仅用于/auth/refresh接口换取新access_token登录成功后后端Set-Cookie设置refresh_token同时返回access_token给前端存localStorage前端拦截401响应自动调用刷新接口成功后更新localStorage中的access_token。注意SameSite属性必须设为Strict或Lax防止CSRF攻击。Strict最安全跨站请求不带cookie但可能影响用户体验如从微信内嵌页跳转Lax是折中方案仅GET请求带cookie目前主流框架默认采用。3.3 后端验签与权限拦截不只是“解码一下”很多新手以为验签就是Jwts.parser().setSigningKey(secret).parseClaimsJws(token)一行代码。实际上完整的验签流程包含至少5层校验格式校验检查token是否为三段式每段是否Base64Url编码签名算法校验Header中alg字段是否为预期算法如HS256防止算法混淆攻击如将HS256改为none密钥匹配校验用正确的密钥而非空密钥验签时间有效性校验检查nbfnot before、iat、exp是否在有效窗口内黑名单校验检查该token是否已被主动注销需额外Redis存储。以Spring Boot为例完整的Filter逻辑public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); // 从Authorization头提取Bearer token if (token ! null validateToken(token)) { // 1. 解析token获取用户信息 Claims claims Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody(); String username claims.getSubject(); ListString perms (ListString) claims.get(perms); // 2. 构建Spring Security Authentication对象 UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(username, null, perms.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())); // 3. 检查是否在Redis黑名单中主动注销 if (redisTemplate.hasKey(jwt:blacklist: token)) { throw new AccessDeniedException(Token has been revoked); } SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } }3.4 主动注销与token失效JWT“无状态”的补丁方案JWT的“无状态”是双刃剑。好处是扩展性好坏处是服务端无法像session那样session.invalidate()一键作废。必须引入外部机制方案一Redis黑名单Blacklist用户点击“退出登录”时后端将当前token或其jti存入Redis设置过期时间token剩余有效期每次验签前先查Redis是否存在该token优点实现简单精准控制单个token缺点每次请求多一次Redis查询增加延迟大量token会占用Redis内存。方案二Redis白名单Whitelist 短生命周期登录成功时生成token的同时将{userId: token_jti}存入Redis设置TTL30分钟验签时先根据userId查Redis中最新jti再比对当前token的jti是否匹配优点可强制用户“单设备登录”新登录踢掉旧设备缺点需要维护jti映射逻辑稍复杂。方案三时间戳版本号Versioning用户表加token_version字段每次登录1token payload中存入当前token_version验签时查数据库用户当前version与token中version比对优点无需Redis数据一致性好缺点每次验签都要查DB违背JWT初衷。实测结论中小项目用方案一Redis黑名单最稳妥。我们压测过单节点Redis QPS 5万而JWT验签本身耗时1ms增加一次Redis查询平均0.5ms对整体性能影响可忽略。关键是它让“退出登录”功能真正可用而不是让用户干等30分钟token过期。4. 实操过程与核心环节实现从零搭建一个可运行的JWT认证流程4.1 后端实现Spring Boot 3.x Java 17步骤1添加依赖!-- pom.xml -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency步骤2配置JWT参数# application.yml jwt: secret: ${JWT_SECRET:default-secret-key-change-in-prod} # 从环境变量读取 expiration: 1800000 # 30分钟毫秒 refresh-expiration: 604800000 # 7天毫秒 header: Authorization prefix: Bearer步骤3创建JWT工具类Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expiration}) private long expiration; public String generateAccessToken(Long userId, String role, ListString perms) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .claim(perms, perms) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expiration)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public String generateRefreshToken(Long userId) { return Jwts.builder() .setSubject(String.valueOf(userId)) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (ExpiredJwtException e) { log.warn(JWT expired: {}, token); } catch (UnsupportedJwtException | MalformedJwtException | SignatureException e) { log.warn(Invalid JWT: {}, token); } catch (IllegalArgumentException e) { log.warn(JWT claims string is empty: {}, token); } return false; } public Long getUserIdFromToken(String token) { Claims claims Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }步骤4登录接口实现PostMapping(/login) public ResponseEntityMapString, Object login(RequestBody LoginRequest request) { // 1. 查询用户此处省略密码校验逻辑 User user userService.findByUsername(request.getUsername()); if (user null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BadCredentialsException(Invalid username or password); } // 2. 生成access_token和refresh_token String accessToken jwtUtil.generateAccessToken(user.getId(), user.getRole(), user.getPermissions()); String refreshToken jwtUtil.generateRefreshToken(user.getId()); // 3. 将refresh_token存入Redis黑名单初始为空仅占位 String refreshTokenKey jwt:refresh: user.getId(); redisTemplate.opsForValue().set(refreshTokenKey, refreshToken, Duration.ofDays(7)); // 4. 设置HttpOnly Cookie返回refresh_token ResponseCookie cookie ResponseCookie.from(refresh_token, refreshToken) .httpOnly(true) .secure(true) // 生产环境必须true .path(/auth/refresh) .maxAge(Duration.ofDays(7)) .sameSite(Strict) .build(); response.addHeader(Set-Cookie, cookie.toString()); // 5. 返回access_token给前端 MapString, Object result new HashMap(); result.put(access_token, accessToken); result.put(expires_in, jwtUtil.getExpiration()); return ResponseEntity.ok(result); }步骤5刷新token接口PostMapping(/refresh) public ResponseEntityMapString, Object refresh(CookieValue(refresh_token) String refreshToken, HttpServletRequest request) { // 1. 从Cookie获取refresh_token if (refreshToken null || !jwtUtil.validateToken(refreshToken)) { throw new AccessDeniedException(Invalid refresh token); } // 2. 校验refresh_token是否属于当前用户防越权 Long userId jwtUtil.getUserIdFromToken(refreshToken); String storedRefresh (String) redisTemplate.opsForValue().get(jwt:refresh: userId); if (!Objects.equals(storedRefresh, refreshToken)) { throw new AccessDeniedException(Refresh token mismatch); } // 3. 生成新的access_token String newAccessToken jwtUtil.generateAccessToken(userId, user, Arrays.asList(user:read)); // 4. 返回新access_token MapString, Object result new HashMap(); result.put(access_token, newAccessToken); result.put(expires_in, jwtUtil.getExpiration()); return ResponseEntity.ok(result); }步骤6全局JWT过滤器Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtUtil jwtUtil; Autowired private RedisTemplateString, Object redisTemplate; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null jwtUtil.validateToken(token)) { Long userId jwtUtil.getUserIdFromToken(token); // 检查是否在黑名单 String blacklistKey jwt:blacklist: token; if (redisTemplate.hasKey(blacklistKey)) { throw new AccessDeniedException(Token revoked); } // 构建Authentication并放入SecurityContext UsernamePasswordAuthenticationToken auth buildAuthentication(userId); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } private UsernamePasswordAuthenticationToken buildAuthentication(Long userId) { // 此处应查询用户权限简化为固定权限 ListGrantedAuthority authorities Arrays.asList( new SimpleGrantedAuthority(ROLE_USER), new SimpleGrantedAuthority(user:read) ); return new UsernamePasswordAuthenticationToken(userId, null, authorities); } }4.2 前端实现Vue 3 Composition API步骤1封装API请求拦截器// api/request.js import axios from axios import { useUserStore } from /stores/user const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截自动添加access_token request.interceptors.request.use(config { const userStore useUserStore() const token userStore.accessToken if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截统一处理401 request.interceptors.response.use( response response, async error { const originalRequest error.config if (error.response?.status 401 !originalRequest._retry) { originalRequest._retry true try { // 调用刷新接口 const res await axios.post(/auth/refresh, {}, { withCredentials: true // 关键携带refresh_token Cookie }) const newToken res.data.access_token // 更新store中的token const userStore useUserStore() userStore.setAccessToken(newToken) // 重试原请求 originalRequest.headers.Authorization Bearer ${newToken} return request(originalRequest) } catch (refreshError) { // 刷新失败清空登录态 const userStore useUserStore() userStore.logout() window.location.href /login return Promise.reject(refreshError) } } return Promise.reject(error) } ) export default request步骤2用户Store管理token// stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ accessToken: localStorage.getItem(access_token) || , userInfo: null }), actions: { setAccessToken(token) { this.accessToken token localStorage.setItem(access_token, token) }, logout() { this.accessToken this.userInfo null localStorage.removeItem(access_token) // 清除refresh_token Cookie需后端配合提供logout接口 document.cookie refresh_token; expiresThu, 01 Jan 1970 00:00:00 GMT; path/auth/refresh; Secure; SameSiteStrict } } })步骤3登录组件调用!-- views/Login.vue -- script setup import { ref } from vue import { useUserStore } from /stores/user import request from /api/request const username ref() const password ref() const loading ref(false) const userStore useUserStore() const handleLogin async () { loading.value true try { const res await request.post(/auth/login, { username: username.value, password: password.value }) // 后端已通过Set-Cookie设置refresh_token // 前端只需保存access_token userStore.setAccessToken(res.data.access_token) // 跳转首页 window.location.href / } catch (error) { alert(登录失败 error.response?.data?.message || 未知错误) } finally { loading.value false } } /script4.3 关键配置与部署注意事项Nginx反向代理配置确保传递Authorization头和Cookielocation /api/ { proxy_pass http://backend/; 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; # 必须透传Authorization头 proxy_set_header Authorization $http_authorization; # 必须透传Cookie且允许跨域携带 proxy_pass_request_headers on; proxy_cookie_path / /; Secure; HttpOnly; SameSiteStrict; }HTTPS强制所有生产环境必须启用HTTPS。因为HttpOnly Cookie的Secure属性要求只有HTTPS连接才发送否则浏览器会拒绝发送。CORS配置后端需明确设置允许的源避免宽泛的*Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList(https://app.example.com)); // 精确域名 configuration.setAllowedOrigins(Arrays.asList(http://localhost:5173)); // 开发环境 configuration.setAllowCredentials(true); // 允许携带cookie configuration.addAllowedMethod(GET); configuration.addAllowedMethod(POST); configuration.addAllowedMethod(PUT); configuration.addAllowedMethod(DELETE); configuration.addExposedHeader(Authorization); // 暴露Authorization头供前端读取 UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, configuration); return source; }5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “401 Unauthorized”但token明明有效的10种可能这是JWT项目中最高频的问题。别急着怀疑代码先按顺序排查排查顺序可能原因快速验证方法解决方案1token过期在 https://jwt.io 粘贴token看exp时间是否早于当前时间前端检查Date.now()是否准确后端确认服务器时间是否同步NTP2密钥不一致后端打印secret值与生成token时用的密钥比对确保application.yml、环境变量、Dockerfile中密钥完全一致重启服务确认生效3算法不匹配查看token Header中alg字段如HS256确认后端parser用的算法相同在Jwts.parser().setSigningKey(...)前加.requireAlgorithm(HS256)强制校验4Authorization头格式错误浏览器Network面板看请求Headers确认是Authorization: Bearer xxx而非Bearerxxx少空格或Basic xxx前端拦截器中严格拼接config.headers.Authorization Bearer token5跨域Cookie未携带Network面板看请求Cookies是否为空检查Response Headers中Set-Cookie的Domain、Path、SameSite是否匹配确认前端fetch或axios配置credentials: include后端CORS配置allowCredentialstrue6Nginx未透传Authorization头在后端Controller里打印request.getHeader(Authorization)看是否为nullNginx配置中添加proxy_set_header Authorization $http_authorization;7token被截断token长度是否异常正常JWT约200-400字符检查是否有特殊字符被URL编码前端用encodeURIComponent编码后再拼接URL参数如果走GET优先用POST8Redis黑名单误判检查Redis中jwt:blacklist:*key是否存在且value正确临时注释黑名单校验代码确认是否恢复正常9时区问题服务器时间和浏览器时间相差大如服务器在UTC前端在CST统一用System.currentTimeMillis()生成时间戳避免new Date()时区干扰10Spring Security默认拦截/auth/login等接口被WebSecurityConfigurerAdapter的antMatchers拦截在configure(HttpSecurity http)中添加.requestMatchers(/auth/**).permitAll()实操心得我处理过一个线上事故现象是iOS用户登录后立即401安卓正常。最终发现是iOS Safari对SameSiteLax的兼容性问题将SameSite改为Strict后解决。这提醒我们永远不要假设浏览器行为一致真机测试必不可少。5.2 XSS与CSRF的双重防御实战JWT方案常被质疑“不防XSS”这是事实但不意味着放弃防御XSS防御三板斧输入过滤所有用户输入表单、URL参数、富文本必须用DOMPurify.sanitize()清洗移除script、onerror等危险标签输出编码服务端渲染时对、、、、进行HTML实体编码CSP策略在Nginx或后端添加Content-Security-Policy: default-src self; script-src self unsafe-inline禁止外链脚本执行。CSRF防御虽然JWT不用cookie存access_token但refresh_token是HttpOnly Cookie仍需防CSRFSameSiteStrict最严策略跨站请求不带cookie双重提交Cookie登录时后端生成随机csrf_token存入Cookie和响应体前端后续请求如/auth/refresh需在header中带上此token后端比对Referer校验后端检查Referer头是否为可信域名需注意Referer可被伪造仅作辅助。5.3 性能压测与瓶颈定位JWT本身验签很快1ms但组合方案可能成为瓶颈Redis连接池耗尽黑名单校验频繁访问Redis若连接池配置过小如默认8个高并发时线程阻塞。解决方案# application.yml spring: redis: lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4 max-wait: 10000JWT解析GC压力高QPS下频繁创建Claims对象触发Minor GC。优化方案使用Jwts.parserBuilder().setSigningKey(...).build().parseClaimsJws(token)复用parser实例对于只校验签名和过期时间的场景用Jwts.parserBuilder().setSigningKey(...).build().parseClaimsJws(token).getBody()避免解析完整Payload。压测结果参考阿里云ECS 4C8G Redis 6.2单节点Spring BootJWT验签Redis黑名单校验QPS可达3200平均延迟12ms瓶颈在Redis网络延迟平均0.8ms而非JWT解析0.3ms当QPS 5000时建议将黑名单校验改为异步如

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑