You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何无状态实现带身份提供商的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 04:36:15