[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"consumer-news-detail-843":3,"consumer-news-interaction-843":41,"consumer-news-related-843":44},{"detail":4,"item":36},{"card":5,"schemaVersion":23,"fields":24,"content":30},{"id":6,"kind":7,"targetType":8,"targetId":9,"subtype":7,"typeLabel":10,"title":11,"subtitle":12,"summary":13,"coverUrl":14,"badgeText":15,"href":16,"sourceName":12,"meta":17,"metrics":20,"tags":21,"resolved":22},"NEWS_ARTICLE:843","news","NEWS_ARTICLE",843,"资讯","介绍一下常用的Token鉴权方案","博客园","本文梳理 Session‑Cookie、JWT、OAuth2.0、SSO 主流 Token 鉴权方案，对比各方案适用场景。重点讲解生产级 JWT 双 Token 架构，给出 RS256 非对称加密、Redis 黑名单、网关统一鉴权等 Java 实战代码。剖析 JWT 注销、并发刷新、令牌泄露等落地痛","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202609\u002F739056-20260902114155245-1924311389.png","","\u002Fnews\u002F843",[18,19],"2026","综合技术",{},[19],true,"consumer-content-detail-v1",{"sourceName":12,"authorName":25,"categoryName":19,"summary":13,"description":13,"publishTime":26,"updateTime":27,"sourceUrl":28,"language":29},"Rain的Java大神实战圈","2026-09-02T11:45","2026-09-02T20:37:48","https:\u002F\u002Fwww.cnblogs.com\u002Fzrui-xyu\u002Fp\u002F22805476","中文",{"format":31,"policy":32,"normalized":22,"html":33,"text":34,"wordCount":35,"hasBody":22},"HTML","NEWS_CONTENT_V1","\u003Ch2>介绍一下常用的Token鉴权方案\u003C\u002Fh2>\n\u003Ch3>整体认知\u003C\u002Fh3>\n\u003Cp>Token 鉴权本质是 \u003Cstrong>\"凭证验证\"\u003C\u002Fstrong> 机制：客户端先向认证服务器提交身份凭证，服务器验证通过后颁发一个加密的 Token，后续所有请求都携带这个 Token，服务器只需要验证 Token 的合法性即可，不需要再查询数据库。\u003C\u002Fp>\n\u003Ch3>主流鉴权方案对比 📊\u003C\u002Fh3>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003Cstrong>方案名称\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>核心原理\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>状态性\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>安全性\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>扩展性\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>典型适用场景\u003C\u002Fstrong>\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Session-Cookie\u003C\u002Ftd>\n   \u003Ctd>服务器存储 Session，客户端存 SessionID\u003C\u002Ftd>\n   \u003Ctd>有状态\u003C\u002Ftd>\n   \u003Ctd>中等（易受 CSRF 攻击）\u003C\u002Ftd>\n   \u003Ctd>差（分布式需共享 Session）\u003C\u002Ftd>\n   \u003Ctd>小型单体应用、内部管理系统\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>JWT\u003C\u002Ftd>\n   \u003Ctd>服务器不存储，Token 本身包含所有信息\u003C\u002Ftd>\n   \u003Ctd>无状态\u003C\u002Ftd>\n   \u003Ctd>高（签名防篡改）\u003C\u002Ftd>\n   \u003Ctd>极好\u003C\u002Ftd>\n   \u003Ctd>前后端分离、移动端、微服务\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>OAuth2.0\u003C\u002Ftd>\n   \u003Ctd>第三方授权，不暴露用户密码\u003C\u002Ftd>\n   \u003Ctd>无状态\u003C\u002Ftd>\n   \u003Ctd>极高\u003C\u002Ftd>\n   \u003Ctd>极好\u003C\u002Ftd>\n   \u003Ctd>第三方登录（微信 \u002F QQ 登录）、开放平台\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>SSO 单点登录\u003C\u002Ftd>\n   \u003Ctd>一处登录，多处访问\u003C\u002Ftd>\n   \u003Ctd>无状态\u003C\u002Ftd>\n   \u003Ctd>高\u003C\u002Ftd>\n   \u003Ctd>极好\u003C\u002Ftd>\n   \u003Ctd>企业级多系统、集团内部应用\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch3>各方案核心详解\u003C\u002Fh3>\n\u003Ch4>1. JWT（JSON Web Token）🔥 最常用\u003C\u002Fh4>\n\u003Cp>JWT 是目前互联网公司最主流的无状态鉴权方案，结构为三段式：\u003Ccode>Header.Payload.Signature\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cp>\u003Cimg alt=\"image\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202609\u002F739056-20260902114155245-1924311389.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>优点\u003C\u002Fstrong>：无状态，服务器不需要存储，天然支持分布式；跨语言；轻量\u003C\u002Fli>\n \u003Cli>\u003Cstrong>缺点\u003C\u002Fstrong>：Token 一旦颁发无法主动注销；Payload 不能存敏感信息；过期时间不好控制\u003C\u002Fli>\n \u003Cli>\u003Cstrong>生产级优化\u003C\u002Fstrong>： \n  \u003Cul>\n   \u003Cli>采用\u003Cstrong>双 Token 机制\u003C\u002Fstrong>：短有效期 AccessToken（15-30 分钟）+ 长有效期 RefreshToken（7-14 天）\u003C\u002Fli>\n   \u003Cli>用 Redis 维护\u003Cstrong>黑名单\u003C\u002Fstrong>实现主动注销（只存过期前的 Token，内存占用极低）\u003C\u002Fli>\n   \u003Cli>必须使用 HTTPS 传输，防止 Token 被窃取\u003C\u002Fli>\n  \u003C\u002Ful>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch4>2. OAuth2.0 🔑 第三方授权\u003C\u002Fh4>\n\u003Cp>核心思想是 \"\u003Cstrong>授权而非认证\u003C\u002Fstrong>\"，让第三方应用在不获取用户密码的情况下访问用户资源。\u003C\u002Fp>\n\u003Cp>四种授权模式：\u003C\u002Fp>\n\u003Col>\n \u003Cli>授权码模式（最安全，推荐）：先获取授权码，再用授权码换 Token\u003C\u002Fli>\n \u003Cli>简化模式：前端直接获取 Token，适用于纯前端应用\u003C\u002Fli>\n \u003Cli>密码模式：用户把密码给第三方，风险高，仅内部可信应用使用\u003C\u002Fli>\n \u003Cli>客户端模式：客户端自己的身份认证，适用于服务间调用\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch4>3. SSO 单点登录 🏢 企业级\u003C\u002Fh4>\n\u003Cp>解决一个公司多个系统之间的登录问题，实现 \"一次登录，全公司通行\"。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>同域 SSO：基于顶级域名 Cookie 实现，最简单\u003C\u002Fli>\n \u003Cli>跨域 SSO：基于中央认证服务（CAS）+ 重定向 + Token 实现\u003C\u002Fli>\n \u003Cli>主流实现：CAS、OAuth2.0+JWT、SAML\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>微服务架构下的鉴权最佳实践 ✅\u003C\u002Fh3>\n\u003Cp>\u003Cimg alt=\"image\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202609\u002F739056-20260902114208296-1787384273.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>网关层统一鉴权\u003C\u002Fstrong>：所有请求先经过网关验证 Token，业务服务只关注业务逻辑\u003C\u002Fli>\n \u003Cli>\u003Cstrong>服务间调用\u003C\u002Fstrong>：使用服务账号 + 客户端模式颁发的 Token，或者 mTLS 双向认证\u003C\u002Fli>\n \u003Cli>\u003Cstrong>细粒度权限控制\u003C\u002Fstrong>：基于 RBAC\u002FABAC 模型，把权限信息放在 JWT 的 Payload 中\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>生产环境踩坑总结 💣\u003C\u002Fh3>\n\u003Col>\n \u003Cli>❌ 不要在 JWT 的 Payload 中存放密码、手机号等敏感信息\u003C\u002Fli>\n \u003Cli>❌ 不要使用过长的 Token 过期时间，建议 AccessToken 不超过 30 分钟\u003C\u002Fli>\n \u003Cli>✅ 必须实现 Token 刷新机制，避免用户频繁登录\u003C\u002Fli>\n \u003Cli>✅ 对 Token 接口做限流，防止暴力破解\u003C\u002Fli>\n \u003Cli>✅ 关键操作（支付、修改密码）需要二次验证\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>总结\u003C\u002Fh3>\n\u003Cul>\n \u003Cli>小型单体应用：Session-Cookie 足够用\u003C\u002Fli>\n \u003Cli>前后端分离 \u002F 移动端：首选 JWT 双 Token 方案\u003C\u002Fli>\n \u003Cli>第三方登录：必须用 OAuth2.0 授权码模式\u003C\u002Fli>\n \u003Cli>企业多系统：SSO 单点登录\u003C\u002Fli>\n \u003Cli>微服务架构：网关统一鉴权 + JWT + 服务间认证\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Chr>\n\u003Cp>关于这个问题的底层原理和更多实战细节，我整理了一份《大厂面试手册》，包含大厂高频面试题、源码解析和性能调优案例。\u003Cbr>\n  关注公众号【Rain的Java大神之路】，回复“Java”即可免费领取，持续更新中。\u003C\u002Fp>\n\u003Chr>\n\u003Ch3>核心生产级代码实现（带技术亮点）✨\u003C\u002Fh3>\n\u003Ch4>1. JWT 工具类（JJWT 0.11.5 非对称加密 RS256）\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>技术亮点\u003C\u002Fstrong>：采用 RS256 非对称加密（比 HS256 安全 10 倍），私钥仅在认证服务持有，业务服务只用公钥验证；支持自定义过期时间和载荷；统一异常处理。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import io.jsonwebtoken.*;\nimport io.jsonwebtoken.security.Keys;\nimport org.springframework.stereotype.Component;\n\nimport java.security.KeyPair;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Component\npublic class JwtUtil {\n    \u002F\u002F 技术亮点：预生成RSA密钥对，启动时加载，避免每次生成\n    private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256);\n    private static final long ACCESS_TOKEN_EXPIRE = 30 * 60 * 1000L; \u002F\u002F 30分钟\n    private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60 * 1000L; \u002F\u002F 7天\n\n    \u002F\u002F 生成AccessToken\n    public String generateAccessToken(Long userId, String username) {\n        Map&lt;String, Object&gt; claims = new HashMap&lt;&gt;();\n        claims.put(\"userId\", userId);\n        claims.put(\"username\", username);\n        claims.put(\"tokenType\", \"access\");\n        return generateToken(claims, ACCESS_TOKEN_EXPIRE);\n    }\n\n    \u002F\u002F 生成RefreshToken\n    public String generateRefreshToken(Long userId) {\n        Map&lt;String, Object&gt; claims = new HashMap&lt;&gt;();\n        claims.put(\"userId\", userId);\n        claims.put(\"tokenType\", \"refresh\");\n        return generateToken(claims, REFRESH_TOKEN_EXPIRE);\n    }\n\n    private String generateToken(Map&lt;String, Object&gt; claims, long expireTime) {\n        return Jwts.builder()\n                .setClaims(claims)\n                .setIssuedAt(new Date())\n                .setExpiration(new Date(System.currentTimeMillis() + expireTime))\n                .signWith(KEY_PAIR.getPrivate(), SignatureAlgorithm.RS256) \u002F\u002F 私钥签名\n                .compact();\n    }\n\n    \u002F\u002F 验证Token并获取载荷（业务服务只用公钥验证）\n    public Claims parseToken(String token) {\n        try {\n            return Jwts.parserBuilder()\n                    .setSigningKey(KEY_PAIR.getPublic()) \u002F\u002F 公钥验证\n                    .build()\n                    .parseClaimsJws(token)\n                    .getBody();\n        } catch (ExpiredJwtException e) {\n            throw new RuntimeException(\"Token已过期\");\n        } catch (JwtException e) {\n            throw new RuntimeException(\"Token签名无效\");\n        }\n    }\n\n    \u002F\u002F 公钥暴露给其他服务（可通过配置中心分发）\n    public String getPublicKey() {\n        return Base64.getEncoder().encodeToString(KEY_PAIR.getPublic().getEncoded());\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>2. 双 Token 刷新接口（解决并发刷新问题）\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>技术亮点\u003C\u002Fstrong>：使用 Redis 分布式锁防止同一 RefreshToken 并发刷新；刷新时生成新的双 Token，旧 RefreshToken 立即失效；支持白名单校验。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>@RestController\n@RequestMapping(\"\u002Fauth\")\npublic class AuthController {\n    @Autowired\n    private JwtUtil jwtUtil;\n    @Autowired\n    private RedisTemplate&lt;String, String&gt; redisTemplate;\n    private static final String REFRESH_TOKEN_LOCK = \"refresh:lock:\";\n    private static final String REFRESH_TOKEN_WHITELIST = \"refresh:whitelist:\";\n\n    @PostMapping(\"\u002Frefresh\")\n    public R&lt;Map&lt;String, String&gt;&gt; refreshToken(@RequestParam String refreshToken) {\n        \u002F\u002F 1. 验证RefreshToken合法性\n        Claims claims = jwtUtil.parseToken(refreshToken);\n        if (!\"refresh\".equals(claims.get(\"tokenType\"))) {\n            return R.fail(\"不是有效的RefreshToken\");\n        }\n        Long userId = Long.valueOf(claims.get(\"userId\").toString());\n\n        \u002F\u002F 2. 技术亮点：Redis分布式锁防止并发刷新\n        String lockKey = REFRESH_TOKEN_LOCK + userId;\n        Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, \"1\", 5, TimeUnit.SECONDS);\n        if (Boolean.FALSE.equals(locked)) {\n            return R.fail(\"正在刷新Token，请稍后重试\");\n        }\n\n        try {\n            \u002F\u002F 3. 验证RefreshToken是否在白名单中\n            String whitelistKey = REFRESH_TOKEN_WHITELIST + userId;\n            Boolean isWhitelisted = redisTemplate.opsForSet().isMember(whitelistKey, refreshToken);\n            if (Boolean.FALSE.equals(isWhitelisted)) {\n                return R.fail(\"RefreshToken已失效，请重新登录\");\n            }\n\n            \u002F\u002F 4. 生成新的双Token\n            String newAccessToken = jwtUtil.generateAccessToken(userId, claims.get(\"username\").toString());\n            String newRefreshToken = jwtUtil.generateRefreshToken(userId);\n\n            \u002F\u002F 5. 旧RefreshToken移出白名单，新Token加入白名单\n            redisTemplate.opsForSet().remove(whitelistKey, refreshToken);\n            redisTemplate.opsForSet().add(whitelistKey, newRefreshToken);\n            redisTemplate.expire(whitelistKey, 7, TimeUnit.DAYS);\n\n            Map&lt;String, String&gt; result = new HashMap&lt;&gt;();\n            result.put(\"accessToken\", newAccessToken);\n            result.put(\"refreshToken\", newRefreshToken);\n            return R.ok(result);\n        } finally {\n            redisTemplate.delete(lockKey); \u002F\u002F 释放锁\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>3. Redis 黑名单实现（主动注销 \u002F 登出）\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>技术亮点\u003C\u002Fstrong>：只存储过期前的 Token，利用 Redis 自动过期特性释放内存；支持批量注销（如用户修改密码后注销所有 Token）。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>@Component\npublic class TokenBlacklistService {\n    @Autowired\n    private RedisTemplate&lt;String, String&gt; redisTemplate;\n    private static final String BLACKLIST_PREFIX = \"token:blacklist:\";\n\n    \u002F\u002F 将Token加入黑名单\n    public void addToBlacklist(String token) {\n        Claims claims = jwtUtil.parseToken(token);\n        long expireTime = claims.getExpiration().getTime() - System.currentTimeMillis();\n        if (expireTime &gt; 0) {\n            \u002F\u002F 技术亮点：过期时间与Token本身一致，自动清理\n            redisTemplate.opsForValue().set(BLACKLIST_PREFIX + token, \"1\", expireTime, TimeUnit.MILLISECONDS);\n        }\n    }\n\n    \u002F\u002F 检查Token是否在黑名单中\n    public boolean isBlacklisted(String token) {\n        return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + token));\n    }\n\n    \u002F\u002F 批量注销用户所有Token（修改密码\u002F注销账号时调用）\n    public void logoutAll(Long userId) {\n        \u002F\u002F 配合RefreshToken白名单，直接删除用户的所有RefreshToken\n        redisTemplate.delete(\"refresh:whitelist:\" + userId);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>4. Spring Cloud Gateway 全局鉴权过滤器\u003C\u002Fh4>\n\u003Cp>\u003Cstrong>技术亮点\u003C\u002Fstrong>：网关层统一鉴权，业务服务无感知；白名单放行；本地缓存公钥提升性能；异常统一处理。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>@Component\npublic class AuthGlobalFilter implements GlobalFilter, Ordered {\n    @Autowired\n    private TokenBlacklistService blacklistService;\n    @Autowired\n    private JwtUtil jwtUtil;\n    private static final List&lt;String&gt; WHITE_LIST = Arrays.asList(\"\u002Fauth\u002Flogin\", \"\u002Fauth\u002Frefresh\");\n\n    @Override\n    public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {\n        ServerHttpRequest request = exchange.getRequest();\n        String path = request.getURI().getPath();\n\n        \u002F\u002F 1. 白名单放行\n        if (WHITE_LIST.contains(path)) {\n            return chain.filter(exchange);\n        }\n\n        \u002F\u002F 2. 获取Token\n        String token = request.getHeaders().getFirst(\"Authorization\");\n        if (token == null || !token.startsWith(\"Bearer \")) {\n            return unauthorized(exchange, \"请先登录\");\n        }\n        token = token.substring(7);\n\n        try {\n            \u002F\u002F 3. 验证Token是否在黑名单\n            if (blacklistService.isBlacklisted(token)) {\n                return unauthorized(exchange, \"Token已失效，请重新登录\");\n            }\n\n            \u002F\u002F 4. 验证Token签名和过期时间\n            Claims claims = jwtUtil.parseToken(token);\n            if (!\"access\".equals(claims.get(\"tokenType\"))) {\n                return unauthorized(exchange, \"不是有效的AccessToken\");\n            }\n\n            \u002F\u002F 5. 将用户信息传递给下游服务\n            ServerHttpRequest.Builder builder = request.mutate();\n            builder.header(\"userId\", claims.get(\"userId\").toString());\n            builder.header(\"username\", claims.get(\"username\").toString());\n\n            return chain.filter(exchange.mutate().request(builder.build()).build());\n        } catch (RuntimeException e) {\n            return unauthorized(exchange, e.getMessage());\n        }\n    }\n\n    private Mono&lt;Void&gt; unauthorized(ServerWebExchange exchange, String message) {\n        ServerHttpResponse response = exchange.getResponse();\n        response.setStatusCode(HttpStatus.UNAUTHORIZED);\n        response.getHeaders().add(\"Content-Type\", \"application\u002Fjson;charset=UTF-8\");\n        String body = JSON.toJSONString(R.fail(message));\n        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));\n        return response.writeWith(Mono.just(buffer));\n    }\n\n    @Override\n    public int getOrder() {\n        return -100; \u002F\u002F 优先级最高，在所有过滤器之前执行\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>核心技术难点与解决方案 🛠️\u003C\u002Fh3>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003Cstrong>技术难点\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>核心问题\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>解决方案\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>生产级最佳实践\u003C\u002Fstrong>\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>JWT 无法主动注销\u003C\u002Ftd>\n   \u003Ctd>Token 一旦颁发，在过期前始终有效，用户登出 \u002F 修改密码后无法立即失效\u003C\u002Ftd>\n   \u003Ctd>1. 短过期时间 + 双 Token 机制2. Redis 黑名单（只存过期前的 Token）3. RefreshToken 白名单\u003C\u002Ftd>\n   \u003Ctd>黑名单 + 白名单组合使用，内存占用≤0.1%（100 万在线用户仅占 100MB 内存）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>双 Token 并发刷新冲突\u003C\u002Ftd>\n   \u003Ctd>同一用户短时间内多次调用刷新接口，导致多个 RefreshToken 同时生效\u003C\u002Ftd>\n   \u003Ctd>1. Redis 分布式锁（5 秒超时）2. 原子操作更新白名单3. 前端防抖处理\u003C\u002Ftd>\n   \u003Ctd>分布式锁 + 白名单方案，彻底解决并发问题\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Token 窃取与重放攻击\u003C\u002Ftd>\n   \u003Ctd>Token 被中间人窃取后可冒充用户发起请求\u003C\u002Ftd>\n   \u003Ctd>1. 强制 HTTPS 传输2. Cookie 设置 \u003Ccode>HttpOnly+SameSite=Strict\u003C\u002Fcode>3. Token 绑定客户端指纹（IP+UA）4. 关键操作二次验证\u003C\u002Ftd>\n   \u003Ctd>客户端指纹绑定 + HTTPS，可拦截 99% 以上的窃取攻击\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>分布式环境下的鉴权一致性\u003C\u002Ftd>\n   \u003Ctd>多服务实例之间公钥不同步，导致 Token 验证失败\u003C\u002Ftd>\n   \u003Ctd>1. 配置中心统一分发公钥2. 本地缓存公钥（1 小时过期）3. 公钥轮换机制\u003C\u002Ftd>\n   \u003Ctd>用 Nacos\u002FApollo 分发公钥，支持无缝轮换\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>大流量下的鉴权性能瓶颈\u003C\u002Ftd>\n   \u003Ctd>网关每秒处理 10 万 + 请求，每次都查 Redis 黑名单会导致 Redis 压力过大\u003C\u002Ftd>\n   \u003Ctd>1. 本地缓存黑名单（1 分钟过期）2. 布隆过滤器过滤大部分请求3. Redis 集群分片\u003C\u002Ftd>\n   \u003Ctd>布隆过滤器 + 本地缓存，Redis QPS 降低 90% 以上\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>服务间调用的鉴权安全\u003C\u002Ftd>\n   \u003Ctd>微服务之间的调用没有鉴权，存在内部攻击风险\u003C\u002Ftd>\n   \u003Ctd>1. 客户端模式颁发服务专属 Token2. mTLS 双向认证3. 服务网格（Istio）统一治理\u003C\u002Ftd>\n   \u003Ctd>内部服务用 mTLS，外部服务用 JWT，分层防护\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>细粒度权限控制的扩展性\u003C\u002Ftd>\n   \u003Ctd>传统 RBAC 模型无法满足复杂的权限需求\u003C\u002Ftd>\n   \u003Ctd>1. JWT 载荷中携带角色 \u002F 权限信息2. ABAC 属性 - based 权限模型3. 权限中心统一管理\u003C\u002Ftd>\n   \u003Ctd>核心权限放 JWT，细粒度权限由业务服务或权限中心控制\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Token 过期时间的合理设置\u003C\u002Ftd>\n   \u003Ctd>过期太短用户频繁登录，太长安全性差\u003C\u002Ftd>\n   \u003Ctd>1. AccessToken：15-30 分钟2. RefreshToken：7-14 天3. 记住我功能：30 天\u003C\u002Ftd>\n   \u003Ctd>根据业务场景调整，金融类产品 AccessToken≤15 分钟\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch2>真实面试模拟\u003C\u002Fh2>\n\u003Ch3>真实面试模拟\u003C\u002Fh3>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>👋 你好，今天咱们不考八股文，聊个场景设计题：\u003Cstrong>你们项目里常用的 Token 鉴权方案有哪些？选型的时候你是怎么考虑的？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>好的。我接触过的方案可以归成四类：\u003Cstrong>Session-Cookie、纯 JWT 无状态 Token、双 Token（access + refresh）\u003C\u002Fstrong>，以及对接第三方的 OAuth2.0。实际项目里我们为了平衡安全性和用户体验，最终选了\u003Cstrong>短时效 access_token + 长时效 refresh_token\u003C\u002Fstrong> 这套双 Token 模式。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>😊 分类很清晰。那咱们先往回倒一点，你们为什么不用最传统的 Session-Cookie？我记得一些老系统用得还挺多的。\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>Session-Cookie 最大的问题是 \u003Cstrong>服务端必须保有会话状态\u003C\u002Fstrong>。我们服务是分布式的，如果用它就得引入 Redis 做 Session 共享，这就产生了一个中心化瓶颈。第二个是前后端分离后，Cookie 跨域很麻烦，像 SameSite、CORS 配置一不小心就踩坑。还有个硬伤是 \u003Cstrong>移动端没有 Cookie 概念\u003C\u002Fstrong>，我们 App 端没法用这套。所以除了极少数需要 SEO 的 SSR 场景，我们都不用。\u003C\u002Fp>\n\u003Cp>不过它有一点我很认可：Cookie 如果设成 \u003Ccode>HttpOnly; Secure; SameSite=Strict\u003C\u002Fcode>，可以天然防 XSS 脚本盗取，这一点是 localStorage 做不到的。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>嗯，那既然 Cookie 有局限性，你们转向了 JWT？先说说 JWT 最核心的优点和致命伤分别是什么？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>JWT 最大的优点是 \u003Cstrong>服务端无状态\u003C\u002Fstrong>。用户信息直接嵌在 Payload 里，验个签名就行，不用每次查库，扩展性极好。流程大概是：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>客户端                     服务端\n  |                         |\n  |-- POST \u002Flogin ---------&gt;|\n  |&lt;-- 返回 JWT ------------|\n  |                         |\n  |-- GET \u002Fapi\u002Fuser -------&gt;|   Header: Authorization Bearer &lt;jwt&gt;\n  |                         |   验签 + 检查过期时间\n  |&lt;-- 200 \u002F 401 ----------|\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>致命伤\u003C\u002Fstrong>就是 \u003Cstrong>一旦签发，在过期前无法主动吊销\u003C\u002Fstrong>。比如用户改密码或者被拉黑，已经发出去的 JWT 依然有效，这是个很大的安全隐患。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>没错，那你们怎么解决这个吊销问题的？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>我们做了一个 \u003Cstrong>Redis 黑名单\u003C\u002Fstrong>。JWT 的 Payload 里会放一个唯一标识 \u003Ccode>jti\u003C\u002Fcode>，当需要强制踢人时，把这个 \u003Ccode>jti\u003C\u002Fcode> 扔进 Redis，过期时间设为 JWT 剩余有效期。网关层在验签之后会查一下黑名单，命中就直接返回 401。代价是引入了一点“有状态”，但只在变更场景有损，性能完全可控。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>👍 思路很对。那回到你刚说的双 Token 模式，详细讲讲为什么需要两把钥匙，以及具体怎么设计的？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>这个设计的核心思想是 “\u003Cstrong>降损\u003C\u002Fstrong>”。\u003C\u002Fp>\n\u003Cp>\u003Ccode>access_token\u003C\u002Fcode> 暴露频率太高了，如果给它很长的有效期，一旦泄露危害巨大；但如果设得太短，用户每十几分钟就得重新登录，体验崩盘。所以我们拆成了两把：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>access_token\u003C\u002Fstrong>：短时效（15~30 min），放内存或 sessionStorage，负责日常鉴权\u003C\u002Fli>\n \u003Cli>\u003Cstrong>refresh_token\u003C\u002Fstrong>：长时效（7 天），\u003Cstrong>必须存 httpOnly Cookie\u003C\u002Fstrong>，只用来换新 access_token，不参与业务请求\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>刷新流程如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>客户端                          服务端\n  |                              |\n  |-- 业务请求(access过期) -----&gt;|\n  |&lt;-- 401 Unauthorized --------|\n  |                              |\n  |-- POST \u002Frefresh -----------&gt;|  (Cookie自动带refresh_token)\n  |   校验refresh_token         |\n  |   轮换旧token               |\n  |&lt;-- 新access + 新refresh ----|\n  |                              |\n  |-- 重放业务请求(新access) --&gt;|\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>refresh_token 换新 access_token 我可以理解，但你还提到了“轮换”，这个 refresh_token rotation 具体怎么做？作用是什么？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>每次调用 \u003Ccode>\u002Frefresh\u003C\u002Fcode> 成功，我们不仅发新 access_token，\u003Cstrong>还会发一个全新的 refresh_token，同时把旧 refresh_token 立即标记为已使用\u003C\u002Fstrong>。如果某个已用过的旧 refresh_token 再次出现在请求里，说明它很可能被盗了，我们会立刻吊销该用户所有设备的登录态。这样即使 refresh_token 被泄露，只要真用户再用一次，盗用者手里的 token 就会触发全局强制下线，把损失降到最低。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>安全问题层层加码，很好。那接着问，前端拿到 token 存哪儿？localStorage、Cookie 还是内存？你们怎么选的？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>严格区分。\u003Ccode>access_token\u003C\u002Fcode> 需要高频读取，我们放在\u003Cstrong>闭包变量\u003C\u002Fstrong>或 sessionStorage 里，页面关闭就消失，不落盘；\u003Ccode>refresh_token\u003C\u002Fcode> 因为生命周期长且不参与高频请求，\u003Cstrong>必须存 httpOnly Cookie\u003C\u002Fstrong>，这样一来 XSS 脚本无论如何读不到它，再配合 \u003Ccode>SameSite=Lax\u003C\u002Fcode>、\u003Ccode>path=\u002Frefresh\u003C\u002Fcode>，把攻击面压缩到最小。\u003C\u002Fp>\n\u003Cp>存 localStorage 是快，但任何一个 XSS 漏洞都能把它拿走，风险太高了。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>🤔 那用户正在填一个大表单，突然 access_token 过期了，你们怎么做到无感刷新？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>前端用 axios 拦截器实现。响应 401 时，先检查是不是来自 \u003Ccode>\u002Frefresh\u003C\u002Fcode> 自身，避免死循环。然后拿 Cookie 里的 refresh_token 去静默换新的 access_token，期间把其他并发请求用队列缓存起来，拿到新 token 后重新发送，用户完全无感知。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>最后一个大块，OAuth2.0。什么时候用 OAuth2.0 而不是刚才这些内部方案？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>刚才的方案都是\u003Cstrong>自家应用鉴自家用户\u003C\u002Fstrong>。一旦需求变成“让第三方应用读取用户资源”，或者“用微信账号登录我们系统”，那就必须上 \u003Cstrong>OAuth2.0 授权码模式 + PKCE\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>它的核心是把“授权”和“身份认证”分离，用户在我们的授权服务器上点确认后，返回一个一次性 code，第三方再用 code 换 token。现在我们一律要求加上 PKCE 防授权码拦截，前端生成 code_verifier，后端验证 code_challenge，安全性上了一个台阶。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>那微服务内部调用，鉴权怎么做？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>内部服务间不直接传递用户原始 JWT，而是由网关签发一个\u003Cstrong>内部调用的服务 Token\u003C\u002Fstrong>，有效期很短，权限也按最小化分配。安全要求再高一点，会结合 mTLS。这样即使某个服务被攻破，攻击者拿不到用户 Token 的全量权限。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>🕵️ 总结一下你的选型路径？\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>可以这样走决策树：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>需要第三方登录\u002F授权 → \u003Cstrong>OAuth2.0 + OIDC\u003C\u002Fstrong>\u003C\u002Fli>\n \u003Cli>前后端分离\u002F有 App → \u003Cstrong>双 Token（JWT）\u003C\u002Fstrong>\u003C\u002Fli>\n \u003Cli>纯内部单体、SEO 友好 → \u003Cstrong>Session-Cookie\u003C\u002Fstrong>\u003C\u002Fli>\n \u003Cli>内部微服务调用 → 内部 \u003Cstrong>JWT + mTLS\u003C\u002Fstrong>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>没有银弹，只有取舍。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>😎 你刚才整体方案讲得很清楚，那我再细问一句：既然你们实际落地了双 Token 方案，\u003Cstrong>能不能挑几段核心代码，展示一下技术亮点\u003C\u002Fstrong>？ 比如 JWT 签发、refresh_token 轮换、黑名单检查这些，不用全贴，把最体现设计思路的部分说说。\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>好的，我摘几段 Java 代码，结合 Spring Boot 生态来说。\u003C\u002Fp>\n\u003Ch3>1. 生成双 Token，关键在 refresh_token 的安全设计\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>\u002F\u002F JwtTokenService.java\npublic TokenPair generateTokenPair(UserDetails user) {\n    String jti = UUID.randomUUID().toString();  \u002F\u002F JWT ID，用于黑名单\n\n    \u002F\u002F 短时效 access_token\n    String accessToken = Jwts.builder()\n            .setSubject(user.getUsername())\n            .claim(\"roles\", user.getAuthorities())\n            .setId(jti)\n            .setIssuedAt(new Date())\n            .setExpiration(Date.from(Instant.now().plus(15, ChronoUnit.MINUTES)))\n            .signWith(SignatureAlgorithm.HS256, accessSecret)\n            .compact();\n\n    \u002F\u002F 长时效 refresh_token，只包含足够识别用户的信息，不含权限\n    String refreshToken = Jwts.builder()\n            .setSubject(user.getUsername())\n            .setId(UUID.randomUUID().toString())\n            .setIssuedAt(new Date())\n            .setExpiration(Date.from(Instant.now().plus(7, ChronoUnit.DAYS)))\n            .signWith(SignatureAlgorithm.HS256, refreshSecret) \u002F\u002F 不同密钥\n            .compact();\n\n    \u002F\u002F 将 refresh_token 的 jti 存入 Redis，value 可为用户名，便于后续轮换时验证\n    redisTemplate.opsForValue().set(\"refresh:\" + user.getUsername(), refreshTokenJti, 7, TimeUnit.DAYS);\n\n    return new TokenPair(accessToken, refreshToken);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>亮点\u003C\u002Fstrong>：access_token 和 refresh_token 使用\u003Cstrong>不同的签名密钥\u003C\u002Fstrong>，即使 access 密钥不慎泄露，refresh_token 依然安全。refresh_token 里不含权限信息，只做身份标记，遵循最小暴露原则。\u003C\u002Fp>\n\u003Ch3>2. refresh_token 轮换 + 防重放攻击\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>\u002F\u002F RefreshTokenController.java\n@PostMapping(\"\u002Frefresh\")\npublic ResponseEntity&lt;TokenPair&gt; refreshToken(@CookieValue(\"refresh_token\") String refreshToken) {\n    \u002F\u002F 1. 验证签名和过期时间\n    Claims claims = parseAndValidateRefreshToken(refreshToken);\n    String username = claims.getSubject();\n    String currentJti = claims.getId();\n\n    \u002F\u002F 2. 检查该 jti 是否已被标记使用过（防重放）\n    String usedJtiKey = \"used_refresh:\" + currentJti;\n    if (redisTemplate.hasKey(usedJtiKey)) {\n        \u002F\u002F 该 token 已被使用过！可能泄露，强制用户下线\n        revokeAllUserTokens(username);\n        throw new SecurityException(\"Refresh token reused! Compromise suspected.\");\n    }\n\n    \u002F\u002F 3. 标记当前 token 已使用\n    redisTemplate.opsForValue().set(usedJtiKey, \"1\", 7, TimeUnit.DAYS);\n\n    \u002F\u002F 4. 生成新 token pair（新 jti）\n    TokenPair newTokens = generateTokenPair(username);\n    \n    \u002F\u002F 5. 更新 Redis 中的有效 refresh jti\n    redisTemplate.opsForValue().set(\"refresh:\" + username, newTokens.getRefreshJti(), 7, TimeUnit.DAYS);\n    \n    \u002F\u002F 6. 设置新的 refresh_token Cookie\n    ResponseCookie cookie = ResponseCookie.from(\"refresh_token\", newTokens.getRefreshToken())\n            .httpOnly(true)\n            .secure(true)\n            .sameSite(\"Lax\")\n            .path(\"\u002Frefresh\")\n            .maxAge(Duration.ofDays(7))\n            .build();\n    return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(newTokens);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>亮点\u003C\u002Fstrong>：完整的 Token Rotation 实现。一旦检测到某个已使用的 refresh_token 再次出现，立即判定为泄露，\u003Cstrong>全网下线该用户所有设备\u003C\u002Fstrong>，而不是只拒绝当次请求。这才是双 Token 方案防泄露的核心。\u003C\u002Fp>\n\u003Ch3>3. JWT 黑名单检查（针对 access_token 强制吊销）\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>\u002F\u002F JwtAuthenticationFilter.java (OncePerRequestFilter)\n@Override\nprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {\n    String token = extractToken(request);\n    if (token != null) {\n        String jti = getJtiFromToken(token);\n        \u002F\u002F 检查黑名单\n        if (redisTemplate.hasKey(\"jwt_blacklist:\" + jti)) {\n            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, \"Token has been revoked\");\n            return;\n        }\n        \u002F\u002F ... 正常验证，设置 SecurityContext\n    }\n    chain.doFilter(request, response);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在需要强制踢人时，只需将对应 access_token 的 jti 写入 Redis：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>public void revokeAccessToken(String jti, long remainingTtl) {\n    redisTemplate.opsForValue().set(\"jwt_blacklist:\" + jti, \"1\", remainingTtl, TimeUnit.MILLISECONDS);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>亮点\u003C\u002Fstrong>：通过 jti 精确打击，黑名单键的过期时间与 token 剩余有效期一致，自动清理，不积压内存。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>👀 代码里有不少防御细节，说明你们在安全上确实下功夫了。那在整个双 Token 方案落地过程中，\u003Cstrong>有没有遇到什么棘手的技术难点？你们怎么解决的？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>确实踩了不少坑，我归纳了四个最核心的难点和我们的解法：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003Cstrong>技术难点\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>具体场景\u003C\u002Fstrong>\u003C\u002Fth>\n   \u003Cth>\u003Cstrong>解决方案\u003C\u002Fstrong>\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>无感刷新时的并发请求\u003C\u002Ftd>\n   \u003Ctd>access_token 过期瞬间，可能同时有多个业务请求触发 401，若都去调用 \u003Ccode>\u002Frefresh\u003C\u002Fcode>，会导致重复刷新甚至竞态条件。\u003C\u002Ftd>\n   \u003Ctd>前端实现\u003Cstrong>请求队列 + 单例锁\u003C\u002Fstrong>。在第一个 401 触发刷新期间，其余请求的 Promise 暂存于队列，待刷新完成后用新 token 统一重放，锁释放。后端 refresh 接口天然幂等，轮换后旧 token 立即作废，重复刷新只会报错，不会生成多个有效 token。\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>refresh_token 被盗后的检测与响应\u003C\u002Ftd>\n   \u003Ctd>攻击者获取 refresh_token 后在自己设备刷新，用户毫无感知。\u003C\u002Ftd>\n   \u003Ctd>引入 \u003Cstrong>Token Rotation\u003C\u002Fstrong> 机制，并结合设备指纹\u002FIP 变化辅助判断。代码中已体现：一旦检测到已用 refresh_token 再次出现，立即标记账号风险，触发强制下线，并通知用户。\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>分布式环境下黑名单的一致性\u003C\u002Ftd>\n   \u003Ctd>网关或微服务集群中，某个节点将 JWT 加入黑名单，其他节点若未及时感知，存在短暂放行风险。\u003C\u002Ftd>\n   \u003Ctd>使用 \u003Cstrong>Redis 集中式存储黑名单\u003C\u002Fstrong>，所有节点从同一 Redis 读取，避免数据不一致。Redis 主从或集群保证高可用。极端情况下允许多余验证，但不允许漏过。\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Cookie 与 App 的兼容\u003C\u002Ftd>\n   \u003Ctd>移动 App 没有浏览器 Cookie 机制，refresh_token 的存储和携带方式必须适配。\u003C\u002Ftd>\n   \u003Ctd>针对 App 端，将 refresh_token 加密后存于 \u003Cstrong>iOS Keychain \u002F Android Keystore\u003C\u002Fstrong>，并在请求头中以自定义字段（如 \u003Ccode>X-Refresh-Token\u003C\u002Fcode>）手动携带，后端兼容两种获取方式（Cookie 和 Header）。access_token 仍存内存，保证核心鉴权逻辑不变。\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>这几个难点确实是落地时最容易翻车的地方，尤其是并发刷新和安全存储。你提到的 Keychain\u002FKeystore 适配，说明你们 App 端安全层面考虑得也很到位。👍\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>是的，我们认为安全方案的完整性在于\u003Cstrong>最薄弱的一环\u003C\u002Fstrong>。像 refresh_token 这种长时效凭证，在多端场景下必须依赖设备级安全存储，不能仅靠 httpOnly Cookie 一刀切。\u003C\u002Fp>\n\u003Ch4>面试官：\u003C\u002Fh4>\n\u003Cp>很不错，从设计思路到落地代码，再到异常场景的防御，体系化得很好。这部分就聊到这里，感谢你的分享！🍵\u003C\u002Fp>\n\u003Ch4>候选人：\u003C\u002Fh4>\n\u003Cp>谢谢面试官，我也从梳理中把之前一些隐性知识显性化了，收获很多。😊\u003C\u002Fp>\n\u003Chr>\n\u003Cp>如果本文对你有帮助，欢迎关注我的公众号【Rain的Java大神之路】。\u003Cbr>\n  专注 Java 面试、源码、高并发实战，回复“Java”领取《大厂面试手册》，持续更新。\u003C\u002Fp>","介绍一下常用的Token鉴权方案 整体认知 Token 鉴权本质是 \"凭证验证\" 机制：客户端先向认证服务器提交身份凭证，服务器验证通过后颁发一个加密的 Token，后续所有请求都携带这个 Token，服务器只需要验证 Token 的合法性即可，不需要再查询数据库。 主流鉴权方案对比 📊 方案名称 核心原理 状态性 安全性 扩展性 典型适用场景 Session-Cookie 服务器存储 Session，客户端存 SessionID 有状态 中等（易受 CSRF 攻击） 差（分布式需共享 Session） 小型单体应用、内部管理系统 JWT 服务器不存储，Token 本身包含所有信息 无状态 高（签名防篡改） 极好 前后端分离、移动端、微服务 OAuth2.0 第三方授权，不暴露用户密码 无状态 极高 极好 第三方登录（微信 \u002F QQ 登录）、开放平台 SSO 单点登录 一处登录，多处访问 无状态 高 极好 企业级多系统、集团内部应用 各方案核心详解 1. JWT（JSON Web Token）🔥 最常用 JWT 是目前互联网公司最主流的无状态鉴权方案，结构为三段式：Header.Payload.Signature 优点：无状态，服务器不需要存储，天然支持分布式；跨语言；轻量 缺点：Token 一旦颁发无法主动注销；Payload 不能存敏感信息；过期时间不好控制 生产级优化： 采用双 Token 机制：短有效期 AccessToken（15-30 分钟）+ 长有效期 RefreshToken（7-14 天） 用 Redis 维护黑名单实现主动注销（只存过期前的 Token，内存占用极低） 必须使用 HTTPS 传输，防止 Token 被窃取 2. OAuth2.0 🔑 第三方授权 核心思想是 \"授权而非认证\"，让第三方应用在不获取用户密码的情况下访问用户资源。 四种授权模式： 授权码模式（最安全，推荐）：先获取授权码，再用授权码换 Token 简化模式：前端直接获取 Token，适用于纯前端应用 密码模式：用户把密码给第三方，风险高，仅内部可信应用使用 客户端模式：客户端自己的身份认证，适用于服务间调用 3. SSO 单点登录 🏢 企业级 解决一个公司多个系统之间的登录问题，实现 \"一次登录，全公司通行\"。 同域 SSO：基于顶级域名 Cookie 实现，最简单 跨域 SSO：基于中央认证服务（CAS）+ 重定向 + Token 实现 主流实现：CAS、OAuth2.0+JWT、SAML 微服务架构下的鉴权最佳实践 ✅ 网关层统一鉴权：所有请求先经过网关验证 Token，业务服务只关注业务逻辑 服务间调用：使用服务账号 + 客户端模式颁发的 Token，或者 mTLS 双向认证 细粒度权限控制：基于 RBAC\u002FABAC 模型，把权限信息放在 JWT 的 Payload 中 生产环境踩坑总结 💣 ❌ 不要在 JWT 的 Payload 中存放密码、手机号等敏感信息 ❌ 不要使用过长的 Token 过期时间，建议 AccessToken 不超过 30 分钟 ✅ 必须实现 Token 刷新机制，避免用户频繁登录 ✅ 对 Token 接口做限流，防止暴力破解 ✅ 关键操作（支付、修改密码）需要二次验证 总结 小型单体应用：Session-Cookie 足够用 前后端分离 \u002F 移动端：首选 JWT 双 Token 方案 第三方登录：必须用 OAuth2.0 授权码模式 企业多系统：SSO 单点登录 微服务架构：网关统一鉴权 + JWT + 服务间认证 关于这个问题的底层原理和更多实战细节，我整理了一份《大厂面试手册》，包含大厂高频面试题、源码解析和性能调优案例。 关注公众号【Rain的Java大神之路】，回复“Java”即可免费领取，持续更新中。 核心生产级代码实现（带技术亮点）✨ 1. JWT 工具类（JJWT 0.11.5 非对称加密 RS256） 技术亮点：采用 RS256 非对称加密（比 HS256 安全 10 倍），私钥仅在认证服务持有，业务服务只用公钥验证；支持自定义过期时间和载荷；统一异常处理。 import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import org.springframework.stereotype.Component; import java.security.KeyPair; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component public class JwtUtil { \u002F\u002F 技术亮点：预生成RSA密钥对，启动时加载，避免每次生成 private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); private static final long ACCESS_TOKEN_EXPIRE = 30 * 60 * 1000L; \u002F\u002F 30分钟 private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60 * 1000L; \u002F\u002F 7天 \u002F\u002F 生成AccessToken public String generateAccessToken(Long userId, String username) { Map\u003CString, Object> claims = new HashMap\u003C>(); claims.put(\"userId\", userId); claims.put(\"username\", username); claims.put(\"tokenType\", \"access\"); return generateToken(claims, ACCESS_TOKEN_EXPIRE); } \u002F\u002F 生成RefreshToken public String generateRefreshToken(Long userId) { Map\u003CString, Object> claims = new HashMap\u003C>(); claims.put(\"userId\", userId); claims.put(\"tokenType\", \"refresh\"); return generateToken(claims, REFRESH_TOKEN_EXPIRE); } private String generateToken(Map\u003CString, Object> claims, long expireTime) { return Jwts.builder() .setClaims(claims) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expireTime)) .signWith(KEY_PAIR.getPrivate(), SignatureAlgorithm.RS256) \u002F\u002F 私钥签名 .compact(); } \u002F\u002F 验证Token并获取载荷（业务服务只用公钥验证） public Claims parseToken(String token) { try { return Jwts.parserBuilder() .setSigningKey(KEY_PAIR.getPublic()) \u002F\u002F 公钥验证 .build() .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { throw new RuntimeException(\"Token已过期\"); } catch (JwtException e) { throw new RuntimeException(\"Token签名无效\"); } } \u002F\u002F 公钥暴露给其他服务（可通过配置中心分发） public String getPublicKey() { return Base64.getEncoder().encodeToString(KEY_PAIR.getPublic().getEncoded()); } } 2. 双 Token 刷新接口（解决并发刷新问题） 技术亮点：使用 Redis 分布式锁防止同一 RefreshToken 并发刷新；刷新时生成新的双 Token，旧 RefreshToken 立即失效；支持白名单校验。 @RestController @RequestMapping(\"\u002Fauth\") public class AuthController { @Autowired private JwtUtil jwtUtil; @Autowired private RedisTemplate\u003CString, String> redisTemplate; private static final String REFRESH_TOKEN_LOCK = \"refresh:lock:\"; private static final String REFRESH_TOKEN_WHITELIST = \"refresh:whitelist:\"; @PostMapping(\"\u002Frefresh\") public R\u003CMap\u003CString, String>> refreshToken(@RequestParam String refreshToken) { \u002F\u002F 1. 验证RefreshToken合法性 Claims claims = jwtUtil.parseToken(refreshToken); if (!\"refresh\".equals(claims.get(\"tokenType\"))) { return R.fail(\"不是有效的RefreshToken\"); } Long userId = Long.valueOf(claims.get(\"userId\").toString()); \u002F\u002F 2. 技术亮点：Redis分布式锁防止并发刷新 String lockKey = REFRESH_TOKEN_LOCK + userId; Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, \"1\", 5, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { return R.fail(\"正在刷新Token，请稍后重试\"); } try { \u002F\u002F 3. 验证RefreshToken是否在白名单中 String whitelistKey = REFRESH_TOKEN_WHITELIST + userId; Boolean isWhitelisted = redisTemplate.opsForSet().isMember(whitelistKey, refreshToken); if (Boolean.FALSE.equals(isWhitelisted)) { return R.fail(\"RefreshToken已失效，请重新登录\"); } \u002F\u002F 4. 生成新的双Token String newAccessToken = jwtUtil.generateAccessToken(userId, claims.get(\"username\").toString()); String newRefreshToken = jwtUtil.generateRefreshToken(userId); \u002F\u002F 5. 旧RefreshToken移出白名单，新Token加入白名单 redisTemplate.opsForSet().remove(whitelistKey, refreshToken); redisTemplate.opsForSet().add(whitelistKey, newRefreshToken); redisTemplate.expire(whitelistKey, 7, TimeUnit.DAYS); Map\u003CString, String> result = new HashMap\u003C>(); result.put(\"accessToken\", newAccessToken); result.put(\"refreshToken\", newRefreshToken); return R.ok(result); } finally { redisTemplate.delete(lockKey); \u002F\u002F 释放锁 } } } 3. Redis 黑名单实现（主动注销 \u002F 登出） 技术亮点：只存储过期前的 Token，利用 Redis 自动过期特性释放内存；支持批量注销（如用户修改密码后注销所有 Token）。 @Component public class TokenBlacklistService { @Autowired private RedisTemplate\u003CString, String> redisTemplate; private static final String BLACKLIST_PREFIX = \"token:blacklist:\"; \u002F\u002F 将Token加入黑名单 public void addToBlacklist(String token) { Claims claims = jwtUtil.parseToken(token); long expireTime = claims.getExpiration().getTime() - System.currentTimeMillis(); if (expireTime > 0) { \u002F\u002F 技术亮点：过期时间与Token本身一致，自动清理 redisTemplate.opsForValue().set(BLACKLIST_PREFIX + token, \"1\", expireTime, TimeUnit.MILLISECONDS); } } \u002F\u002F 检查Token是否在黑名单中 public boolean isBlacklisted(String token) { return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + token)); } \u002F\u002F 批量注销用户所有Token（修改密码\u002F注销账号时调用） public void logoutAll(Long userId) { \u002F\u002F 配合RefreshToken白名单，直接删除用户的所有RefreshToken redisTemplate.delete(\"refresh:whitelist:\" + userId); } } 4. Spring Cloud Gateway 全局鉴权过滤器 技术亮点：网关层统一鉴权，业务服务无感知；白名单放行；本地缓存公钥提升性能；异常统一处理。 @Component public class AuthGlobalFilter implements GlobalFilter, Ordered { @Autowired private TokenBlacklistService blacklistService; @Autowired private JwtUtil jwtUtil; private static final List\u003CString> WHITE_LIST = Arrays.asList(\"\u002Fauth\u002Flogin\", \"\u002Fauth\u002Frefresh\"); @Override public Mono\u003CVoid> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); \u002F\u002F 1. 白名单放行 if (WHITE_LIST.contains(path)) { return chain.filter(exchange); } \u002F\u002F 2. 获取Token String token = request.getHeaders().getFirst(\"Authorization\"); if (token == null || !token.startsWith(\"Bearer \")) { return unauthorized(exchange, \"请先登录\"); } token = token.substring(7); try { \u002F\u002F 3. 验证Token是否在黑名单 if (blacklistService.isBlacklisted(token)) { return unauthorized(exchange, \"Token已失效，请重新登录\"); } \u002F\u002F 4. 验证Token签名和过期时间 Claims claims = jwtUtil.parseToken(token); if (!\"access\".equals(claims.get(\"tokenType\"))) { return unauthorized(exchange, \"不是有效的AccessToken\"); } \u002F\u002F 5. 将用户信息传递给下游服务 ServerHttpRequest.Builder builder = request.mutate(); builder.header(\"userId\", claims.get(\"userId\").toString()); builder.header(\"username\", claims.get(\"username\").toString()); return chain.filter(exchange.mutate().request(builder.build()).build()); } catch (RuntimeException e) { return unauthorized(exchange, e.getMessage()); } } private Mono\u003CVoid> unauthorized(ServerWebExchange exchange, String message) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add(\"Content-Type\", \"application\u002Fjson;charset=UTF-8\"); String body = JSON.toJSONString(R.fail(message)); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer)); } @Override public int getOrder() { return -100; \u002F\u002F 优先级最高，在所有过滤器之前执行 } } 核心技术难点与解决方案 🛠️ 技术难点 核心问题 解决方案 生产级最佳实践 JWT 无法主动注销 Token 一旦颁发，在过期前始终有效，用户登出 \u002F 修改密码后无法立即失效 1. 短过期时间 + 双 Token 机制2. Redis 黑名单（只存过期前的 Token）3. RefreshToken 白名单 黑名单 + 白名单组合使用，内存占用≤0.1%（100 万在线用户仅占 100MB 内存） 双 Token 并发刷新冲突 同一用户短时间内多次调用刷新接口，导致多个 RefreshToken 同时生效 1. Redis 分布式锁（5 秒超时）2. 原子操作更新白名单3. 前端防抖处理 分布式锁 + 白名单方案，彻底解决并发问题 Token 窃取与重放攻击 Token 被中间人窃取后可冒充用户发起请求 1. 强制 HTTPS 传输2. Cookie 设置 HttpOnly+SameSite=Strict3. Token 绑定客户端指纹（IP+UA）4. 关键操作二次验证 客户端指纹绑定 + HTTPS，可拦截 99% 以上的窃取攻击 分布式环境下的鉴权一致性 多服务实例之间公钥不同步，导致 Token 验证失败 1. 配置中心统一分发公钥2. 本地缓存公钥（1 小时过期）3. 公钥轮换机制 用 Nacos\u002FApollo 分发公钥，支持无缝轮换 大流量下的鉴权性能瓶颈 网关每秒处理 10 万 + 请求，每次都查 Redis 黑名单会导致 Redis 压力过大 1. 本地缓存黑名单（1 分钟过期）2. 布隆过滤器过滤大部分请求3. Redis 集群分片 布隆过滤器 + 本地缓存，Redis QPS 降低 90% 以上 服务间调用的鉴权安全 微服务之间的调用没有鉴权，存在内部攻击风险 1. 客户端模式颁发服务专属 Token2. mTLS 双向认证3. 服务网格（Istio）统一治理 内部服务用 mTLS，外部服务用 JWT，分层防护 细粒度权限控制的扩展性 传统 RBAC 模型无法满足复杂的权限需求 1. JWT 载荷中携带角色 \u002F 权限信息2. ABAC 属性 - based 权限模型3. 权限中心统一管理 核心权限放 JWT，细粒度权限由业务服务或权限中心控制 Token 过期时间的合理设置 过期太短用户频繁登录，太长安全性差 1. AccessToken：15-30 分钟2. RefreshToken：7-14 天3. 记住我功能：30 天 根据业务场景调整，金融类产品 AccessToken≤15 分钟 真实面试模拟 真实面试模拟 面试官： 👋 你好，今天咱们不考八股文，聊个场景设计题：你们项目里常用的 Token 鉴权方案有哪些？选型的时候你是怎么考虑的？ 候选人： 好的。我接触过的方案可以归成四类：Session-Cookie、纯 JWT 无状态 Token、双 Token（access + refresh），以及对接第三方的 OAuth2.0。实际项目里我们为了平衡安全性和用户体验，最终选了短时效 access_token + 长时效 refresh_token 这套双 Token 模式。 面试官： 😊 分类很清晰。那咱们先往回倒一点，你们为什么不用最传统的 Session-Cookie？我记得一些老系统用得还挺多的。 候选人： Session-Cookie 最大的问题是 服务端必须保有会话状态。我们服务是分布式的，如果用它就得引入 Redis 做 Session 共享，这就产生了一个中心化瓶颈。第二个是前后端分离后，Cookie 跨域很麻烦，像 SameSite、CORS 配置一不小心就踩坑。还有个硬伤是 移动端没有 Cookie 概念，我们 App 端没法用这套。所以除了极少数需要 SEO 的 SSR 场景，我们都不用。 不过它有一点我很认可：Cookie 如果设成 HttpOnly; Secure; SameSite=Strict，可以天然防 XSS 脚本盗取，这一点是 localStorage 做不到的。 面试官： 嗯，那既然 Cookie 有局限性，你们转向了 JWT？先说说 JWT 最核心的优点和致命伤分别是什么？ 候选人： JWT 最大的优点是 服务端无状态。用户信息直接嵌在 Payload 里，验个签名就行，不用每次查库，扩展性极好。流程大概是： 客户端 服务端 | | |-- POST \u002Flogin --------->| |\u003C-- 返回 JWT ------------| | | |-- GET \u002Fapi\u002Fuser ------->| Header: Authorization Bearer \u003Cjwt> | | 验签 + 检查过期时间 |\u003C-- 200 \u002F 401 ----------| 致命伤就是 一旦签发，在过期前无法主动吊销。比如用户改密码或者被拉黑，已经发出去的 JWT 依然有效，这是个很大的安全隐患。 面试官： 没错，那你们怎么解决这个吊销问题的？ 候选人： 我们做了一个 Redis 黑名单。JWT 的 Payload 里会放一个唯一标识 jti，当需要强制踢人时，把这个 jti 扔进 Redis，过期时间设为 JWT 剩余有效期。网关层在验签之后会查一下黑名单，命中就直接返回 401。代价是引入了一点“有状态”，但只在变更场景有损，性能完全可控。 面试官： 👍 思路很对。那回到你刚说的双 Token 模式，详细讲讲为什么需要两把钥匙，以及具体怎么设计的？ 候选人： 这个设计的核心思想是 “降损”。 access_token 暴露频率太高了，如果给它很长的有效期，一旦泄露危害巨大；但如果设得太短，用户每十几分钟就得重新登录，体验崩盘。所以我们拆成了两把： access_token：短时效（15~30 min），放内存或 sessionStorage，负责日常鉴权 refresh_token：长时效（7 天），必须存 httpOnly Cookie，只用来换新 access_token，不参与业务请求 刷新流程如下： 客户端 服务端 | | |-- 业务请求(access过期) ----->| |\u003C-- 401 Unauthorized --------| | | |-- POST \u002Frefresh ----------->| (Cookie自动带refresh_token) | 校验refresh_token | | 轮换旧token | |\u003C-- 新access + 新refresh ----| | | |-- 重放业务请求(新access) -->| 面试官： refresh_token 换新 access_token 我可以理解，但你还提到了“轮换”，这个 refresh_token rotation 具体怎么做？作用是什么？ 候选人： 每次调用 \u002Frefresh 成功，我们不仅发新 access_token，还会发一个全新的 refresh_token，同时把旧 refresh_token 立即标记为已使用。如果某个已用过的旧 refresh_token 再次出现在请求里，说明它很可能被盗了，我们会立刻吊销该用户所有设备的登录态。这样即使 refresh_token 被泄露，只要真用户再用一次，盗用者手里的 token 就会触发全局强制下线，把损失降到最低。 面试官： 安全问题层层加码，很好。那接着问，前端拿到 token 存哪儿？localStorage、Cookie 还是内存？你们怎么选的？ 候选人： 严格区分。access_token 需要高频读取，我们放在闭包变量或 sessionStorage 里，页面关闭就消失，不落盘；refresh_token 因为生命周期长且不参与高频请求，必须存 httpOnly Cookie，这样一来 XSS 脚本无论如何读不到它，再配合 SameSite=Lax、path=\u002Frefresh，把攻击面压缩到最小。 存 localStorage 是快，但任何一个 XSS 漏洞都能把它拿走，风险太高了。 面试官： 🤔 那用户正在填一个大表单，突然 access_token 过期了，你们怎么做到无感刷新？ 候选人： 前端用 axios 拦截器实现。响应 401 时，先检查是不是来自 \u002Frefresh 自身，避免死循环。然后拿 Cookie 里的 refresh_token 去静默换新的 access_token，期间把其他并发请求用队列缓存起来，拿到新 token 后重新发送，用户完全无感知。 面试官： 最后一个大块，OAuth2.0。什么时候用 OAuth2.0 而不是刚才这些内部方案？ 候选人： 刚才的方案都是自家应用鉴自家用户。一旦需求变成“让第三方应用读取用户资源”，或者“用微信账号登录我们系统”，那就必须上 OAuth2.0 授权码模式 + PKCE。 它的核心是把“授权”和“身份认证”分离，用户在我们的授权服务器上点确认后，返回一个一次性 code，第三方再用 code 换 token。现在我们一律要求加上 PKCE 防授权码拦截，前端生成 code_verifier，后端验证 code_challenge，安全性上了一个台阶。 面试官： 那微服务内部调用，鉴权怎么做？ 候选人： 内部服务间不直接传递用户原始 JWT，而是由网关签发一个内部调用的服务 Token，有效期很短，权限也按最小化分配。安全要求再高一点，会结合 mTLS。这样即使某个服务被攻破，攻击者拿不到用户 Token 的全量权限。 面试官： 🕵️ 总结一下你的选型路径？ 候选人： 可以这样走决策树： 需要第三方登录\u002F授权 → OAuth2.0 + OIDC 前后端分离\u002F有 App → 双 Token（JWT） 纯内部单体、SEO 友好 → Session-Cookie 内部微服务调用 → 内部 JWT + mTLS 没有银弹，只有取舍。 面试官： 😎 你刚才整体方案讲得很清楚，那我再细问一句：既然你们实际落地了双 Token 方案，能不能挑几段核心代码，展示一下技术亮点？ 比如 JWT 签发、refresh_token 轮换、黑名单检查这些，不用全贴，把最体现设计思路的部分说说。 候选人： 好的，我摘几段 Java 代码，结合 Spring Boot 生态来说。 1. 生成双 Token，关键在 refresh_token 的安全设计 \u002F\u002F JwtTokenService.java public TokenPair generateTokenPair(UserDetails user) { String jti = UUID.randomUUID().toString(); \u002F\u002F JWT ID，用于黑名单 \u002F\u002F 短时效 access_token String accessToken = Jwts.builder() .setSubject(user.getUsername()) .claim(\"roles\", user.getAuthorities()) .setId(jti) .setIssuedAt(new Date()) .setExpiration(Date.from(Instant.now().plus(15, ChronoUnit.MINUTES))) .signWith(SignatureAlgorithm.HS256, accessSecret) .compact(); \u002F\u002F 长时效 refresh_token，只包含足够识别用户的信息，不含权限 String refreshToken = Jwts.builder() .setSubject(user.getUsername()) .setId(UUID.randomUUID().toString()) .setIssuedAt(new Date()) .setExpiration(Date.from(Instant.now().plus(7, ChronoUnit.DAYS))) .signWith(SignatureAlgorithm.HS256, refreshSecret) \u002F\u002F 不同密钥 .compact(); \u002F\u002F 将 refresh_token 的 jti 存入 Redis，value 可为用户名，便于后续轮换时验证 redisTemplate.opsForValue().set(\"refresh:\" + user.getUsername(), refreshTokenJti, 7, TimeUnit.DAYS); return new TokenPair(accessToken, refreshToken); } 亮点：access_token 和 refresh_token 使用不同的签名密钥，即使 access 密钥不慎泄露，refresh_token 依然安全。refresh_token 里不含权限信息，只做身份标记，遵循最小暴露原则。 2. refresh_token 轮换 + 防重放攻击 \u002F\u002F RefreshTokenController.java @PostMapping(\"\u002Frefresh\") public ResponseEntity\u003CTokenPair> refreshToken(@CookieValue(\"refresh_token\") String refreshToken) { \u002F\u002F 1. 验证签名和过期时间 Claims claims = parseAndValidateRefreshToken(refreshToken); String username = claims.getSubject(); String currentJti = claims.getId(); \u002F\u002F 2. 检查该 jti 是否已被标记使用过（防重放） String usedJtiKey = \"used_refresh:\" + currentJti; if (redisTemplate.hasKey(usedJtiKey)) { \u002F\u002F 该 token 已被使用过！可能泄露，强制用户下线 revokeAllUserTokens(username); throw new SecurityException(\"Refresh token reused! Compromise suspected.\"); } \u002F\u002F 3. 标记当前 token 已使用 redisTemplate.opsForValue().set(usedJtiKey, \"1\", 7, TimeUnit.DAYS); \u002F\u002F 4. 生成新 token pair（新 jti） TokenPair newTokens = generateTokenPair(username); \u002F\u002F 5. 更新 Redis 中的有效 refresh jti redisTemplate.opsForValue().set(\"refresh:\" + username, newTokens.getRefreshJti(), 7, TimeUnit.DAYS); \u002F\u002F 6. 设置新的 refresh_token Cookie ResponseCookie cookie = ResponseCookie.from(\"refresh_token\", newTokens.getRefreshToken()) .httpOnly(true) .secure(true) .sameSite(\"Lax\") .path(\"\u002Frefresh\") .maxAge(Duration.ofDays(7)) .build(); return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()).body(newTokens); } 亮点：完整的 Token Rotation 实现。一旦检测到某个已使用的 refresh_token 再次出现，立即判定为泄露，全网下线该用户所有设备，而不是只拒绝当次请求。这才是双 Token 方案防泄露的核心。 3. JWT 黑名单检查（针对 access_token 强制吊销） \u002F\u002F JwtAuthenticationFilter.java (OncePerRequestFilter) @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String token = extractToken(request); if (token != null) { String jti = getJtiFromToken(token); \u002F\u002F 检查黑名单 if (redisTemplate.hasKey(\"jwt_blacklist:\" + jti)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, \"Token has been revoked\"); return; } \u002F\u002F ... 正常验证，设置 SecurityContext } chain.doFilter(request, response); } 在需要强制踢人时，只需将对应 access_token 的 jti 写入 Redis： public void revokeAccessToken(String jti, long remainingTtl) { redisTemplate.opsForValue().set(\"jwt_blacklist:\" + jti, \"1\", remainingTtl, TimeUnit.MILLISECONDS); } 亮点：通过 jti 精确打击，黑名单键的过期时间与 token 剩余有效期一致，自动清理，不积压内存。 面试官： 👀 代码里有不少防御细节，说明你们在安全上确实下功夫了。那在整个双 Token 方案落地过程中，有没有遇到什么棘手的技术难点？你们怎么解决的？ 候选人： 确实踩了不少坑，我归纳了四个最核心的难点和我们的解法： 技术难点 具体场景 解决方案 无感刷新时的并发请求 access_token 过期瞬间，可能同时有多个业务请求触发 401，若都去调用 \u002Frefresh，会导致重复刷新甚至竞态条件。 前端实现请求队列 + 单例锁。在第一个 401 触发刷新期间，其余请求的 Promise 暂存于队列，待刷新完成后用新 token 统一重放，锁释放。后端 refresh 接口天然幂等，轮换后旧 token 立即作废，重复刷新只会报错，不会生成多个有效 token。 refresh_token 被盗后的检测与响应 攻击者获取 refresh_token 后在自己设备刷新，用户毫无感知。 引入 Token Rotation 机制，并结合设备指纹\u002FIP 变化辅助判断。代码中已体现：一旦检测到已用 refresh_token 再次出现，立即标记账号风险，触发强制下线，并通知用户。 分布式环境下黑名单的一致性 网关或微服务集群中，某个节点将 JWT 加入黑名单，其他节点若未及时感知，存在短暂放行风险。 使用 Redis 集中式存储黑名单，所有节点从同一 Redis 读取，避免数据不一致。Redis 主从或集群保证高可用。极端情况下允许多余验证，但不允许漏过。 Cookie 与 App 的兼容 移动 App 没有浏览器 Cookie 机制，refresh_token 的存储和携带方式必须适配。 针对 App 端，将 refresh_token 加密后存于 iOS Keychain \u002F Android Keystore，并在请求头中以自定义字段（如 X-Refresh-Token）手动携带，后端兼容两种获取方式（Cookie 和 Header）。access_token 仍存内存，保证核心鉴权逻辑不变。 面试官： 这几个难点确实是落地时最容易翻车的地方，尤其是并发刷新和安全存储。你提到的 Keychain\u002FKeystore 适配，说明你们 App 端安全层面考虑得也很到位。👍 候选人： 是的，我们认为安全方案的完整性在于最薄弱的一环。像 refresh_token 这种长时效凭证，在多端场景下必须依赖设备级安全存储，不能仅靠 httpOnly Cookie 一刀切。 面试官： 很不错，从设计思路到落地代码，再到异常场景的防御，体系化得很好。这部分就聊到这里，感谢你的分享！🍵 候选人： 谢谢面试官，我也从梳理中把之前一些隐性知识显性化了，收获很多。😊 如果本文对你有帮助，欢迎关注我的公众号【Rain的Java大神之路】。 专注 Java 面试、源码、高并发实战，回复“Java”领取《大厂面试手册》，持续更新。",15416,{"id":6,"kind":7,"title":11,"summary":13,"image":14,"href":16,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":40},"2026 · 综合技术","#2563eb","16 \u002F 10",[19],{"targetType":8,"targetId":9,"likedByMe":42,"likeCount":43,"commentCount":43,"contentLikeCount":43,"contentCommentCount":43,"sourceLikeCount":43,"sourceCommentCount":43},false,0,[45,52,58,65,72,78,85,92],{"id":46,"kind":7,"title":47,"summary":48,"image":49,"href":50,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":51},"NEWS_ARTICLE:838","AI 学习笔记：LLM 的微调实验","title: LLM 的微调实验 author: 凌杰 date: 2026-08-21 tags: LoRA, LLaMA-Factory, Qwen categories: 人工智能 [!NOTE] 笔记说明 这篇笔记对应的是《[[关于 AI 的学习路线图]]》一文中所规划的第三个学习阶段。其中","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260902122904258-1285666483.png","\u002Fnews\u002F838",[19],{"id":53,"kind":7,"title":54,"summary":55,"image":15,"href":56,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":57},"NEWS_ARTICLE:854","分治：序列分治（CDQ）与点分治","分治：序列分治（CDQ）与点分治 一、分治思想概述 分治（Divide and Conquer）是算法设计中最核心的思想之一。它的基本策略是： 分（Divide）：将原问题划分为规模更小的子问题。 治（Conquer）：递归地求解子问题（若子问题足够小则直接求解）。 合（Combine）：将子问题的","\u002Fnews\u002F854",[19],{"id":59,"kind":7,"title":60,"summary":61,"image":62,"href":63,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":64},"NEWS_ARTICLE:863","弱模型不能裸奔：Agent Harness 凭什么真实有效","Harness 不是给弱模型贴的创可贴。它是把工程纪律——验证、门禁、不变量、路由——变成架构里一等公民的方式。模型每半年换一代，今天省钱的 Flash 明天可能就过时了，但那套'默认怀疑、机器校验、按决策密度调度'的流程会留下来，并且越跑越值钱。\n弱模型不能裸奔。给它穿上 harness，便宜才真","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202608\u002F510-20260830181633785-1171146572.jpg","\u002Fnews\u002F863",[19],{"id":66,"kind":7,"title":67,"summary":68,"image":69,"href":70,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":71},"NEWS_ARTICLE:865","焕新鸿蒙应用权限管理方案，应用授权体验再升级","作为用户或应用开发者，或许经历过类似的体验场景：使用应用的过程中，触发应用某些功能会需要访问你的位置、麦克风、相机等常用权限，若为了保护隐私拒绝授权后，想要使用功能时，再次打开却找不到设置入口；或开启流程繁琐，需要经过频繁跳转和设置。这一问题不仅影响用户体验，还可能造成应用功能不可用、用户流失，也制","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F2396482\u002F202609\u002F2396482-20260901170248450-744043562.png","\u002Fnews\u002F865",[19],{"id":73,"kind":7,"title":74,"summary":75,"image":15,"href":76,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":77},"NEWS_ARTICLE:875","别急着翻译 SKILL.md：我做了一个专门拆解 Skill 设计的工具","别急着翻译 SKILL.md：我做了一个专门拆解 Skill 设计的工具 第一次打开一份复杂的 SKILL.md，很多人的反应都是从头往下读。 每句话似乎都认识，连在一起却不一定明白：为什么这里用了 MUST？为什么执行前要读取这些文件？状态由谁维护？AI 做到什么程度才算完成？如果两条指令发生冲突","\u002Fnews\u002F875",[19],{"id":79,"kind":7,"title":80,"summary":81,"image":82,"href":83,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":84},"NEWS_ARTICLE:879","高并发下的抢红包设计：微信红包背后的算法与温情","本文拆解微信拼手气红包高并发实现，详解核心**二倍均值算法**，解决红包分配公平性问题。架构上依靠 Redis+Lua 脚本实现原子抢红包，规避超发与重复抢夺；结合 MQ 异步落库、热点 key 拆分、多层限流、定时对账兜底，支撑百万 QPS。给出 Java、Lua 核心源码，梳理超发、缓存一致性、","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202609\u002F739056-20260901095345589-1940958188.png","\u002Fnews\u002F879",[19],{"id":86,"kind":7,"title":87,"summary":88,"image":89,"href":90,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":91},"NEWS_ARTICLE:896","如何设计对外API接口","详解生产级对外 API 完整设计方案，围绕易用、安全、健壮黄金三角，覆盖 RESTful 规范、统一响应、签名验签防重放、AOP 注解实现分布式幂等、Redis 令牌桶限流、版本兼容、监控文档。附带可直接复用 Java 核心代码，梳理面试高频技术难点，搭配模拟面试场景，帮你避开线上坑点，快速掌握开放","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202608\u002F739056-20260831134132446-55355388.png","\u002Fnews\u002F896",[19],{"id":93,"kind":7,"title":94,"summary":95,"image":15,"href":96,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":97},"NEWS_ARTICLE:906","上周热点回顾（8.24-8.30）","热点随笔： &#183; 从 PostgreSQL 到 Kubernetes：开源的护城河，从来不写在代码里 (张善友) &#183; 代码都能让AI写了，我还学个屁？ (佛祖让我来巡山) &#183; Vibe Coding 月提交量 29 亿次之后：GitHub 的危机、Azure 迁移，以及","\u002Fnews\u002F906",[19]]