资讯动态

微服务单点登录实战:OAuth2 + JWT 从理论到落地

发布时间:2026/10/23 1:37:18 来源:尧图企业网站定制
最近在给团队做微服务架构的统一登录改造前后折腾了两周把 Spring Security OAuth2 和 JWT 这套组合从理论到落地完整地过了一遍。过程中踩的坑比想象中多很多问题翻官方文档根本找不到答案只能靠断点调试、翻源码、看 GitHub issue 一点点磨。这篇就从头到尾聊聊微服务环境下的单点登录到底怎么设计、怎么实现以及那些文档里很少会写的注意事项。先说结论如果你的系统只是单体应用加个登录别用 OAuth2 JWT太重了。但如果你面对的是多个微服务、多个前端应用、还要做统一权限控制这套方案几乎是必经之路。它能解决的核心问题只有一个——让用户在多个系统间登录一次后续所有服务都信任这个“登录凭证”。适合想搭建统一认证中心的 Spring Boot / Spring Cloud 开发者参考尤其是正在做网关鉴权、多服务互通改造的团队。1. 微服务单点登录的整体设计思路1.1 为什么微服务环境下的 Session 方案走不通传统单体应用里Session 是最常见的登录状态管理方式。用户登录后服务端把 session 存在应用内存里给用户返回一个 sessionId浏览器后续请求带上这个 id服务端一比对就识别身份。这个模式在单体时代没问题但微服务化之后第一个难题就是服务拆分登录鉴权该放在哪个服务用户的请求先到网关网关需要判断用户是否已经登录再决定是否转发到下游服务。如果登录状态只存在于某一个业务服务的内存里网关和其他服务根本拿不到Session 不共享登录态自然就断了。常见的临时方案是把 Session 集中到 Redis所有服务共享一份 session 数据。但第二个问题接踵而来跨域。微服务的前端往往分布在不同的域名、端口浏览器对 Cookie 的 SameSite 属性、跨域传递规则约束很多一不小心 sessionId 就带不过去。就算做通了 Cookie 跨域也绕不开 Session 同步的网络开销和存储压力。更麻烦的是移动端和第三方系统对接时根本没法用 Cookie。JWT 解决的是另一个维度的问题把用户身份信息直接编码进一个自包含的令牌里服务端只需要验证签名就能信任令牌里的内容完全不需要在服务端保存会话状态。每次请求携带令牌网关验签通过用户身份就明确了。这对微服务架构几乎是最合适的方式——每个服务天然无状态水平扩容也不再需要额外处理会话同步。1.2 OAuth2 在单点登录中的角色划分OAuth2 本身不是为登录设计的它最初解决的是授权问题用户同意第三方应用访问自己的资源。但后来社区发现把 OAuth2 的授权码流程改造一番就能实现 SSO单点登录。在微服务环境里这个流程天然地把系统分成三个角色认证中心授权服务器负责登录页、用户名密码校验、颁发令牌是整个体系的核心独立成一个服务。网关客户端负责拦截所有外部请求判断用户是否需要登录如果没带令牌则引导到认证中心带了令牌则做校验。业务服务资源服务器不再关心用户从哪来、怎么登录的只需要判断令牌是否有效、当前用户是谁、有没有权限访问某个接口。用机场安检来类比认证中心就是办登机牌的地方网关是安检口业务服务是各个登机口。乘客在值机柜台验证身份拿到登机牌登录拿令牌过安检时工作人员查验登机牌是否真实有效网关验签到了登机口地勤只需要看登记牌上的航班信息用户身份不需要再重复验证身份证了。OAuth2 有四种授权模式做 SSO 最常用的是授权码模式。用户访问客户端应用客户端发现未登录重定向到认证中心用户输入账号密码认证中心回调客户端并携带一个授权码客户端再用授权码换取令牌。之后的请求都带令牌访问资源。另一种密码模式常用于移动端或服务间调用用户把账号密码直接交给客户端由客户端替用户换取令牌内部服务间也可以配合 client_credentials 模式做服务身份认证。2. 技术选型与关键决策2.1 老方案与 Spring Authorization Server 怎么选标题里写的是 Spring Security OAuth2严格说这已经是一套“过去式”的解决方案了。Spring 官方在 2022 年宣布 Spring Security OAuth2 项目停止维护推荐大家迁移到 Spring Authorization Server。这导致网上教程五花八门新老内容混杂初学者照着抄经常出现依赖冲突。老方案的依赖是spring-security-oauth2和spring-security-oauth2-autoconfigure使用EnableAuthorizationServer注解配置类继承AuthorizationServerConfigurerAdapter。优点是资料多、简单直接Spring Boot 2.x 的老项目可以直接接很多中小企业现在线上跑的还是这套。缺点是项目已经停止维护不修复安全漏洞新版本的 Spring Boot 无法兼容。如果你是新项目优先选 Spring Authorization Server。它本身是一个独立项目通过EnableWebSecurity配合一组OAuth2AuthorizationServerConfigurer配置项工作灵活性更高但配置量也更大和旧版的对接思路完全不同。我的建议是旧项目在 Spring Boot 2.7 及以下已经跑得好好的没有必要线上大动干戈迁移新项目从第一天就按 Spring Authorization Server 设计省得日后做技术债。这篇文章后面主流程按老方案来演示因为大家看到的参考资料多半是这个写法也方便对照排查。在关键部分我会补充新方案对应怎么配置让你能根据自己的项目版本切换。2.2 JWT 签名算法与密钥管理JWT 本身只是令牌格式安全性完全取决于签名算法和密钥的管理方式。生产环境强烈建议用非对称加密 RS256而不是对称的 HS256。HS256 是认证中心用同一个密钥签名和验证。问题在于如果网关、业务服务也要验签密钥就必须分发给所有服务。一旦某个服务被攻破或密钥被泄露整个系统都能被伪造令牌。RS256 不同认证中心持有私钥签名其他服务只持有公钥验证。私钥可以锁死在认证服务器上公钥再怎么泄露也无法伪造令牌。密钥生成方式有两种。一种是用 JDK 自带的 keytool 工具生成 JKS 密钥库keytool -genkeypair -alias auth-jwt -keyalg RSA -storetype JKS -keystore auth-jwt.jks -keysize 2048 -validity 365另一种更通用的是用 openssl 生成 PEM 格式的 RSA 密钥对openssl genrsa -out private_key.pem 2048 openssl rsa -pubout -in private_key.pem -out public_key.pem密钥生成后认证中心通过配置读取私钥网关和业务服务部署公钥。记住一点密钥文件属于敏感信息带入项目仓库、硬编码在 yml 里都是坏习惯最好是放到配置中心或环境变量里。2.3 JWT 无状态不是银弹JWT 最大的卖点是“无状态”服务端不用存储令牌。但这个特性同时带来了三个痛点无法主动失效。用户注销、密码修改、账号封禁后已经发出的 JWT 在有效期内依然可用。解决办法是引入 Redis 黑名单机制登出时把令牌的 jti唯一ID存进 Redis有效期设置为令牌剩余有效期网关校验时检查是否在黑名单。令牌体积大。JWT 里塞的用户信息越多Header 就越长每次请求都携带网络开销比 Session ID 大得多。业务服务每次解析还要消耗 CPU 做签名验算。敏感信息泄露风险。JWT 的 Payload 是 base64 编码不是加密任何人拿到令牌都能直接解码看到内容。千万别往 JWT 里塞密码、手机号、身份证号这类敏感数据。所以选择 JWT 还是 Opaque Token不透明令牌要看实际场景。内部系统做 SSOJWT 足够方便面向公众互联网的系统敏感度高我更建议用 Opaque Token认证中心每收到一个令牌就去令牌存储里查一下状态天然支持吊销代价是每次校验多一次内部调用。3. 核心代码实现认证中心、网关、资源服务3.1 工程结构与依赖版本为了把整套逻辑说清楚我搭了一个示例工程包含四个模块auth-server认证中心负责登录、发令牌gateway统一入口完成 JWT 校验和转发order-service业务服务模拟受保护资源user-service用户服务对接用户表数据父工程pom.xml引入 Spring Boot 2.7.x 和 Spring Cloud 2021.0.x。认证中心额外引入dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-oauth2/artifactId /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-jwt/artifactId /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-jose/artifactId /dependency如果你用 Spring Boot 2.6.xspring-cloud-starter-oauth2的版本要控制在 2.2.x 附近2.7 用 2.2.13.RELEASE 这类最后版本具体以你项目的 BOM 为准。新版 Spring Boot 3 直接不要尝试老方案组件根本起不来。3.2 认证中心的关键配置老方案示例认证中心的核心是AuthorizationServerConfigurerAdapter的配置类Configuration EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { Autowired private DataSource dataSource; Autowired private AuthenticationManager authenticationManager; Autowired private UserDetailsService userDetailsService; Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess(permitAll()) .checkTokenAccess(isAuthenticated()); } Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain new TokenEnhancerChain(); enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(), jwtAccessTokenConverter())); endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .tokenStore(tokenStore()) .tokenEnhancer(enhancerChain) .accessTokenConverter(jwtAccessTokenConverter()); } Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter new JwtAccessTokenConverter(); converter.setKeyPair(keyPair()); return converter; } Bean public KeyPair keyPair() { // 生产环境从配置中心读取密钥信息 ClassPathResource resource new ClassPathResource(auth-jwt.jks); KeyStoreKeyFactory factory new KeyStoreKeyFactory(resource, storepass.toCharArray()); return factory.getKeyPair(auth-jwt, keypass.toCharArray()); } }这里解释几个关键点的用意。客户端信息用clients.jdbc(dataSource)存储是为了支持多个客户端应用动态注册而不是硬编码在代码里。tokenStore用JwtTokenStore它实际上不真正存储令牌只是对令牌做 JWT 格式的解析这是无状态设计的核心。JwtAccessTokenConverter配置了 RSA 密钥对生成的令牌会用私钥签名。为了让 JWT 携带用户 ID、角色等业务数据还要写一个自定义的TokenEnhancerpublic class CustomTokenEnhancer implements TokenEnhancer { Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { MapString, Object additionalInfo new HashMap(); if (authentication.getPrincipal() instanceof CustomUserDetails) { CustomUserDetails user (CustomUserDetails) authentication.getPrincipal(); additionalInfo.put(userId, user.getUserId()); additionalInfo.put(nickname, user.getNickname()); additionalInfo.put(authorities, user.getAuthorities()); } ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; } }这样生成的 JWT Payload 里除了标准的user_name、scope等字段还会有我们自定义的userId、nickname、authorities。网关和业务服务拿到令牌后直接从这个结构里读用户信息非常方便。认证中心的登录安全配置还涉及WebSecurityConfigurerAdapter需暴露AuthenticationManagerBean并配置登录页、放行/oauth/**等相关路径。这一步也是老坑高发区我记得第一次搭的时候漏了AuthenticationManagerBean报错信息却指向别的地方排查了很久。3.3 Spring Authorization Server 的对应配置如果你用的是新方案逻辑上的对应关系大概是这样的RegisteredClient替代oauth_client_details表一个 WebSecurity 配置类配合OAuth2AuthorizationServerConfigurer完成授权服务器配置。Configuration EnableWebSecurity public class AuthorizationServerConfig { Bean Order(1) public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer new OAuth2AuthorizationServerConfigurer(); http.apply(authorizationServerConfigurer); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .tokenGenerator(tokenGenerator()) .authorizationServerContext(authorizationServerContext()); return http.formLogin().and().build(); } Bean public JWKSourceSecurityContext jwkSource() { // 构造 RSA 密钥对并注册为 JWK } }新方案的配置粒度更细一切以 Bean 定制为主网上没有太多现成项目可以直接抄需要仔细读官方文档。我对团队的实际建议依然是新项目要么快速切到新方案要么干脆先用老方案把业务跑通再找时间平滑迁移不要在方案切换中阻塞业务开发。3.4 网关层的统一 JWT 校验网关在整套链路里做的是“集中式校验”。我用 Spring Cloud Gateway 写了一个全局过滤器所有请求进来先过这一层判断是否放行还是需要认证。Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private static final String[] WHITE_LIST { /auth/login, /auth/logout, /user/register, /oauth/token }; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getPath().value(); // 白名单直接放行 for (String whitePath : WHITE_LIST) { if (path.startsWith(whitePath)) { return chain.filter(exchange); } } // 从 Header 里提取令牌 String authHeader request.getHeaders().getFirst(Authorization); if (StringUtils.isEmpty(authHeader) || !authHeader.startsWith(Bearer )) { return unauthorized(exchange, 未携带令牌); } String token authHeader.substring(7); try { Claims claims JwtUtils.parseToken(token); // 校验黑名单 if (JwtBlacklist.isBlacklisted(claims.get(jti))) { return unauthorized(exchange, 令牌已失效); } // 把用户信息放到内部请求头里传递给下游服务 ServerHttpRequest mutateRequest request.mutate() .header(X-User-Id, claims.get(userId).toString()) .header(X-User-Name, claims.get(user_name).toString()) .header(X-User-Authorities, claims.get(authorities).toString()) .removeHeader(Authorization) .build(); return chain.filter(exchange.mutate().request(mutateRequest).build()); } catch (Exception e) { return unauthorized(exchange, 令牌无效或已过期); } } private MonoVoid unauthorized(ServerWebExchange exchange, String message) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); // 返回统一的 401 响应体 byte[] body ({\code\:401,\msg\:\ message \}).getBytes(); DataBuffer buffer exchange.getResponse().bufferFactory().wrap(body); return exchange.getResponse().writeWith(Mono.just(buffer)); } Override public int getOrder() { return -100; } }这段代码里有几个设计决策我展开说下。校验完 JWT 后网关把Authorization头移除换成X-User-Id、X-User-Name等内部请求头。这么做的好处是防止下游业务服务再次解析 JWT鉴权逻辑全集中在网关业务服务只需要信任网关传递的用户信息简化了大量重复代码。内部头名称可以约定成团队规范避免和外部请求头混淆。统一的 401 返回格式也很重要。如果没有在网关层拦截异常业务服务随便抛个异常返回给前端的结构五花八门前端要兼容多种格式就很累。统一在网关出口保证响应结构联调效率会高很多。3.5 业务服务如何解析用户身份网关把 JWT 解析成了内部头业务服务就不需要再集成 JWT 解码逻辑了。我建议写一个轻量的内部认证过滤器读请求头里的X-User-Id等信息构造Authentication对象。Component public class UserContextFilter implements Filter { Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) servletRequest; String userId httpRequest.getHeader(X-User-Id); if (StringUtils.isNotEmpty(userId)) { UserContext.setUserId(Long.valueOf(userId)); UserContext.setUserName(httpRequest.getHeader(X-User-Name)); UserContext.setAuthorities(httpRequest.getHeader(X-User-Authorities)); } try { chain.doFilter(servletRequest, servletResponse); } finally { UserContext.clear(); } } }基于 ThreadLocal 的UserContext可以保证同一个请求在服务内部的业务代码中随时都能取到当前用户不需要每个 Controller 都从 Request 里取参数。用完后在 finally 里清理避免线程池复用导致的数据串号。如果你坚持让每个业务服务都做资源服务器配置也可以在每个服务里加 OAuth2 资源服务器配置类配合授权中心的公钥做验签里面还要处理AuthenticationManager冲突、权限前缀等细节整体复杂度会高不少。综合对比下来除非服务需要独立对外开放接口否则“网关统一校验 内部头传递”是维护成本最低的模式。4. 从零搭一个可跑通的 SSO 示例4.1 环境准备与数据库初始化本地环境建议 JDK 8 或 11、Maven 3.6、MySQL 8.0、Redis 6.x。示例工程中认证中心依赖一个数据库用来存储客户端详情。需要建oauth_client_details表这是老方案中的关键表结构如下CREATE TABLE oauth_client_details ( client_id varchar(256) NOT NULL, resource_ids varchar(256) DEFAULT NULL, client_secret varchar(256) DEFAULT NULL, scope varchar(256) DEFAULT NULL, authorized_grant_types varchar(256) DEFAULT NULL, web_server_redirect_uri varchar(256) DEFAULT NULL, authorities varchar(256) DEFAULT NULL, access_token_validity int DEFAULT NULL, refresh_token_validity int DEFAULT NULL, additional_information varchar(4096) DEFAULT NULL, autoapprove varchar(256) DEFAULT NULL, PRIMARY KEY (client_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;插入一条网关客户端的记录INSERT INTO oauth_client_details ( client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, access_token_validity, refresh_token_validity ) VALUES ( gateway-client, {bcrypt}加密后的客户端密钥, all, authorization_code,refresh_token,password,client_credentials, http://localhost:9000/login/oauth2/code/gateway, 7200, 86400 );access_token_validity设成 7200 秒即 2 小时refresh_token_validity设成 86400 秒24 小时。这两个值要根据业务场景调整内部后台系统2 小时令牌算合理面向用户的前台系统我建议缩短到 30 分钟左右不然令牌泄露的风险窗口太长。初始化用户表可以放一张简单的users表字段包括id、username、password、nickname、status等。密码用 BCrypt 加密存储不要存明文。4.2 认证中心的参数细节认证中心的重中之重是spring.security.oauth2相关的配置。在application.yml里注意如下几个点server.port不要和其他服务撞加密 BCrypt 密钥的类要显式声明 Bean否则 OAuth2 会默认用{noop}明文模式安全隐患很大登录页路径如果不自定义默认由 Spring Security 生成一套简单的页面练手可以生产环境迟早要自己写前端或对接现有后台oauth_client_details表的client_secret字段存的是 BCrypt 加密后的内容网关调用/oauth/token时依然传原始密钥认证中心会自动比对认证服务内的WebSecurityConfigurer要暴露AuthenticationManagerBean Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }这一步漏了会出现authenticationManager must be specified之类的报错启动不会报等真正换令牌时才炸。4.3 网关路由与令牌透传细节网关的application.yml要配置动态路由把外部请求转发到不同服务spring: cloud: gateway: routes: - id: auth-server uri: http://localhost:9000 predicates: - Path/auth/** - id: order-service uri: http://localhost:8082 predicates: - Path/api/order/** - id: user-service uri: http://localhost:8081 predicates: - Path/api/user/**细心的读者会发现auth-server被映射在/auth/**但认证中心的/oauth/token路径没有被这个前缀覆盖。实际配置时要保证/oauth/**路径不被网关拦截重定向到登录流程否则授权码流程就断在中途。通常的做法是把/oauth/**加入网关白名单由认证中心自己处理。网关转发时还容易遇到一个问题请求头大小写。服务器内部有些组件对 Header 名称的大小写敏感尤其跨服务 HTTP 调用时X-User-Id有时会被转成x-user-id。如果下游过滤器读取名称不匹配用户信息就丢了。解决的方法是统一约定比如业务服务全部用X-User-ID或统一转小写处理并在联调阶段就覆盖到这个场景。4.4 端到端联调与验收场景整个链路跑通后我用三种场景做验收。场景一未登录访问受保护资源。直接请求订单服务接口curl -i http://localhost:8080/api/order/1期望返回 401响应体是网关统一的错误结构。场景二通过授权码模式获取令牌并访问接口。先在浏览器中访问认证中心授权地址登录后拿到授权码再用授权码换令牌curl -X POST http://localhost:9000/oauth/token \ -u gateway-client:secret \ -d grant_typeauthorization_codecode授权码redirect_urihttp://localhost:9000/login/oauth2/code/gateway拿到access_token后请求携带令牌curl http://localhost:8080/api/order/1 \ -H Authorization: Bearer 令牌内容期望返回订单数据而且业务服务日志能打印当前用户 ID。场景三令牌过期后用刷新令牌续期。把access_token有效期调短等过期后调用刷新接口curl -X POST http://localhost:9000/oauth/token \ -u gateway-client:secret \ -d grant_typerefresh_tokenrefresh_token刷新令牌拿到新的access_token后继续访问接口中间不能弹出重新登录。这三个场景跑通说明主链路是健康的。之后再接真正的前端页面、对接用户中心、加权限控制都是在主流程上的扩展。4.5 常见问题与排查技巧实录版本和依赖永远是第一坑。Spring Cloud 和 Spring Boot 的版本矩阵非常严格我用过 Spring Boot 2.7 spring-cloud-starter-oauth22.2.13.RELEASE 跑通也见过别人在 Spring Boot 2.6 上一直报ClassNotFoundException: org/springframework/security/oauth2/config/annotation/web/configurers/OAuth2LoginConfigurer。出现这类错误第一时间去查 Spring Cloud 与 Spring Boot 的版本对照表而不是改代码。第二个高频问题是 JWT 签名验证失败。常见的表现是网关验签时报JwtException: JWT signature does not match locally computed signature。原因多数是认证中心的JwtAccessTokenConverter配置的密钥和网关用的验签密钥不是同一对。尤其用new JwtAccessTokenConverter()但不调setKeyPair或setSigningKey时系统默认使用随机密钥重启后所有已签发的令牌全部失效。排查思路很简单认证中心/oauth/token_key接口能拿到当前公钥用这个公钥离线验证 JWT 签名如果失败就是密钥不一致。第三个坑出现在 Feign 调用场景。网关校验通过后业务服务 A 通过 Feign 调用业务服务 BFeign 请求默认不携带任何原请求的 Header导致 B 服务读取不到X-User-Id。解决方法是写一个 RequestInterceptorConfiguration public class FeignConfig { Bean public RequestInterceptor requestInterceptor() { return template - { ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes ! null) { HttpServletRequest request attributes.getRequest(); template.header(Authorization, request.getHeader(Authorization)); template.header(X-User-Id, request.getHeader(X-User-Id)); template.header(X-User-Name, request.getHeader(X-User-Name)); } }; } }这里需要留意RequestContextHolder是否拿到了请求上下文。在异步线程里、Async方法中上下文可能丢失Feign 拦截器会获取到空值。异步场景要手动把用户信息传递进新线程或者用自定义的异步任务装饰器。第四个问题是 JWT 的注销和黑名单。老方案本身没有登出吊销接口我自己的处理方式是引入 Redis 维护一个黑名单jti为 key令牌剩余有效期为过期时间。网关校验阶段先查 Redis再验 JWT 签名。同时认证中心提供一个/logout接口接收access_token解析出jti后写入黑名单。这个方案可以解决员工离职、账号被封等强失效场景代价是网关多一次 Redis 查询。如果对性能特别敏感可以把黑名单缓存到本地 Caffeine再通过消息通知各网关实例同步。第五个问题是刷新令牌和前端无感刷新。默认配置里刷新令牌有效期比较长前端可以在access_token快过期时自动调用刷新接口拿到新令牌后替换本地存储。这里要注意并发问题多个接口同时返回 401避免前端同时发多个刷新请求要加一个互斥锁只发一次刷新请求其余等待结果。我们踩过的坑就是没有锁刷新接口被并发打了三四次授权中心被刷出一堆重复令牌。这套方案我这边跑了两个多月整体稳定但回顾下来最大的体会是单点登录的重点不只是把令牌发出来更在于整条链路里怎么把用户身份可靠地传下去。网关统一校验和内部头透传看似简单其实隐含了很多约定——哪些 Header 是内部保留的、下游服务如何读取、跨服务调用的上下文怎么传递都需要团队一起定清楚。如果你刚开始做这个改造我建议先按文中的示例把主流程跑通再去补黑名单、权限控制这些边角功能。一开始就追求面面俱到遇到的坑会成倍增长。后续还可以考虑把公钥统一放到配置中心实现密钥轮换接入分布式会话控制支持多端互踢也可以把 JWT 换成 Opaque Token满足更严格的安全要求。这些扩展都是在这套主链路之上做加法前提是主链路没有设计歪。

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

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

免费获取报价 →
↑