如何无状态实现带身份提供商的Spring Boot微服务安全防护
最终方案结论
针对你当前的Angular前端+Spring Boot微服务场景,以外部Ping Federate做SSO身份源、权限数据存在自有业务库的前提,无状态OAuth2最优落地方式为:通过Spring OAuth2 Client对接Ping Federate完成身份认证,认证通过后加载自有库的角色权限数据,生成非对称签名的自包含JWT令牌返回前端,所有下游微服务作为资源服务本地验签鉴权,全程不做服务端会话/令牌存储。
架构选型对比
两种方案的优劣势非常明确,直接选第二种即可:
- 方案1(定制独立Authorization Server对接Ping Federate):平白新增一层服务链路,运维成本翻倍。你之前参考的官方示例默认是有状态实现,本质是依赖HTTP Session存储授权码流转临时数据、认证上下文、授权确认记录,要改无状态需要重写大量内置逻辑,投入产出比极低,没有必要。
- 方案2(OAuth2 Client完成Ping侧认证后加载权限生成令牌):链路最短、改造量极小,认证逻辑和权限加载逻辑收敛在网关/接入层,不需要跨服务同步用户上下文,天然适配微服务无状态要求,是当前场景的最优选择。
无状态落地核心规则
无状态的核心要求是服务端零认证相关存储:不存HTTP会话、不存认证临时数据、不存令牌状态,所有认证授权信息全部写入非对称签名的自包含JWT,下游微服务仅需本地持有验签公钥,收到请求直接本地完成令牌校验、权限判断,不需要查库、不需要远程调用认证服务,性能和扩展性最优。
落地时遵循以下要点:
- 前端Angular采用Authorization Code + PKCE流程对接接入层,全程不在前端存储客户端密钥,符合SPA安全规范,Spring Security高版本默认支持该流程,不需要额外开发。
- 认证通过后以Ping Federate返回的用户唯一标识为条件,查询自有业务库加载对应角色、权限数据,写入JWT的自定义claims。
- 全局配置Spring Security会话策略为
SessionCreationPolicy.STATELESS,彻底禁用服务端会话自动创建逻辑。 - 访问令牌设置较短过期时间(比如1小时),刷新令牌同样采用签名JWT实现、设置更长过期时间,服务端不存储任何令牌记录,校验时仅验证签名和过期时间即可。
- JWT签名采用RS256非对称算法,私钥仅保存在认证接入层,验签公钥分发给所有下游微服务,避免对称密钥泄露的全局风险。
核心代码示例
接入层安全配置(对接Ping Federate+认证成功生成JWT)
@Configuration @EnableWebSecurity @EnableOAuth2Client public class GatewaySecurityConfig { @Value("${security.jwt.private-key}") private PrivateKey jwtSignPrivateKey; @Autowired private AppUserRepository appUserRepository; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // 全局配置无状态会话,永远不创建HttpSession .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) // 配置Ping Federate作为身份源 .oauth2Login(oauth2 -> oauth2 .clientRegistrationRepository(pingFederateClientRegistration()) .successHandler(customAuthSuccessHandler()) ) .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**", "/actuator/health").permitAll() .anyRequest().authenticated() ); return http.build(); } // 组装Ping Federate客户端配置,参数可从配置文件注入 private ClientRegistrationRepository pingFederateClientRegistration() { ClientRegistration pingRegistration = ClientRegistration.withRegistrationId("pingfederate") .clientId("your-service-client-id") .clientSecret("your-service-client-secret") .authorizationUri("https://your-ping-instance/as/authorization.oauth2") .tokenUri("https://your-ping-instance/as/token.oauth2") .userInfoUri("https://your-ping-instance/as/userinfo.oauth2") .userNameAttributeName("sub") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") .scope("openid", "profile") .build(); return new InMemoryClientRegistrationRepository(pingRegistration); } // 自定义认证成功逻辑:查库加载权限,生成JWT返回前端 private AuthenticationSuccessHandler customAuthSuccessHandler() { return (request, response, authentication) -> { OAuth2AuthenticationToken pingAuth = (OAuth2AuthenticationToken) authentication; String pingUserSub = pingAuth.getPrincipal().getAttribute("sub"); // 从自有业务库查询用户、角色、权限 AppUser user = appUserRepository.findByPingSub(pingUserSub) .orElseThrow(() -> new UsernameNotFoundException("业务系统无当前用户信息")); List<String> roleCodes = user.getRoles().stream().map(Role::getCode).toList(); List<String> permissionCodes = user.getRoles().stream() .flatMap(role -> role.getPermissions().stream()) .map(Permission::getCode) .toList(); // 生成签名JWT String accessToken = Jwts.builder() .setSubject(pingUserSub) .claim("userId", user.getId()) .claim("roles", roleCodes) .claim("permissions", permissionCodes) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) .signWith(jwtSignPrivateKey, SignatureAlgorithm.RS256) .compact(); // 直接返回令牌给前端,后续请求前端将令牌放在Authorization请求头 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JsonUtils.toJson(Map.of( "accessToken", accessToken, "expiresIn", 3600 ))); }; } }
下游微服务资源服务配置(本地验签无状态鉴权)
@Configuration @EnableWebSecurity public class ResourceServerSecurityConfig { @Value("${security.jwt.public-key}") private PublicKey jwtVerifyPublicKey; @Bean public SecurityFilterChain resourceServerChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() // 示例:仅拥有order:edit权限的用户可以访问下单接口 .requestMatchers(HttpMethod.POST, "/api/order/**").hasAuthority("order:edit") .anyRequest().authenticated() ) // 配置JWT本地验签 .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .publicKey(jwtVerifyPublicKey) .jwtAuthenticationConverter(customJwtPermissionConverter()) ) ); return http.build(); } // 从JWT claims中解析权限,转换为Spring Security识别的权限对象 private JwtAuthenticationConverter customJwtPermissionConverter() { JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(jwt -> { List<String> permissions = jwt.getClaimAsStringList("permissions"); return permissions.stream().map(SimpleGrantedAuthority::new).toList(); }); return converter; } }
注意:JWT仅做Base64编码不是加密,不要在claims中存放身份证号、手机号这类敏感信息,敏感数据只存业务库,令牌中仅放用户ID、角色、权限这类非敏感校验字段即可。
内容的提问来源于stack exchange,提问作者Kiran
相关产品推荐
相关产品推荐

