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

Spring Boot资源服务:SAML断言认证及JWT兑换方案咨询

问题背景

我有一个整合了SAML认证、OAuth2资源服务与OAuth2授权客户端的Spring Boot(v2.7.10)项目,其@EnableWebSecurity配置如下:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/","/login").permitAll().and()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .saml2Login().defaultSuccessUrl("/dashboard")
            .and()
            .oauth2ResourceServer(oauth2 -> oauth2.jwt())
            .oauth2Login(
                    oauth2 -> oauth2.
                            defaultSuccessUrl("/dashboard", true)
            )
            .logout().logoutSuccessUrl("/login").and()
            .logout(logout -> logout
                    .logoutSuccessUrl("/login")
            )
            .saml2Logout(Customizer.withDefaults())
    ;
    return http.build();
}

当前场景

可通过外部IDP的OAuth2配置生成JWT令牌,也可通过外部IDP的SAML2提供商配置生成并验证SAML2断言;现有一批API通过资源服务以JWT Bearer令牌保护(格式:Authorization: Bearer ey....)。

问题

  1. Spring Boot中是否可配置为使用请求头中的SAML断言保护API,并由已配置的外部SAML IDP验证?示例请求头:Authorization: SAML Base64EncodedSAML。
  2. 若上述方案不可行,能否配置Spring暴露接收SAML断言并返回对应IDP的JWT访问令牌的API?

补充说明:已尝试过SAML Bearer断言兑换JWT令牌的方案,但需Spring Boot专属实现。


解决方案

问题1:直接用请求头中的SAML断言保护API

Spring Security默认不直接支持Authorization: SAML <Base64断言>格式的认证,但可通过自定义认证过滤器结合Spring Security SAML2的验证能力实现:

  1. 自定义SAML Bearer认证过滤器
    实现OncePerRequestFilter提取请求头中的SAML断言,解码后发起认证:

    public class SamlBearerAuthFilter extends OncePerRequestFilter {
        private final Saml2AuthenticationTokenConverter tokenConverter;
        private final AuthenticationManager authenticationManager;
    
        public SamlBearerAuthFilter(AuthenticationManager authenticationManager, Saml2AuthenticationTokenConverter tokenConverter) {
            this.authenticationManager = authenticationManager;
            this.tokenConverter = tokenConverter;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
            if (authHeader != null && authHeader.startsWith("SAML ")) {
                try {
                    String base64Saml = authHeader.substring(5);
                    byte[] samlBytes = Base64.getDecoder().decode(base64Saml);
                    String samlAssertion = new String(samlBytes, StandardCharsets.UTF_8);
    
                    Saml2AuthenticationToken authRequest = tokenConverter.convert(samlAssertion);
                    Authentication authentication = authenticationManager.authenticate(authRequest);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                } catch (IllegalArgumentException | AuthenticationException e) {
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid SAML assertion");
                    return;
                }
            }
            filterChain.doFilter(request, response);
        }
    }
    
  2. 集成到SecurityFilterChain
    将自定义过滤器添加到OAuth2资源服务过滤器之前,同时配置SAML2断言验证所需的Bean:

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager, Saml2AuthenticationTokenConverter tokenConverter) throws Exception {
        http
                .csrf().disable()
                .addFilterBefore(new SamlBearerAuthFilter(authenticationManager, tokenConverter), OAuth2ResourceServerAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/","/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .saml2Login().defaultSuccessUrl("/dashboard")
                .and()
                .oauth2ResourceServer(oauth2 -> oauth2.jwt())
                .oauth2Login(oauth2 -> oauth2.defaultSuccessUrl("/dashboard", true))
                .logout(logout -> logout.logoutSuccessUrl("/login"))
                .saml2Logout(Customizer.withDefaults());
        return http.build();
    }
    
    @Bean
    public Saml2AuthenticationTokenConverter saml2AuthenticationTokenConverter(RelyingPartyRegistrationRepository repo) {
        return new Saml2AuthenticationTokenConverter(repo);
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
  3. 关键注意事项

    • 确保外部IDP的元数据已通过application.yml的spring.security.saml2.relyingparty.registration节点正确配置。
    • 自定义过滤器需处理断言解码、格式校验等异常,返回标准HTTP错误码。

问题2:暴露SAML断言兑换JWT的API

若直接用SAML断言保护API的方案不适用,可实现专属API接收SAML断言,调用外部IDP的令牌端点兑换JWT:

  1. 实现令牌兑换接口
    利用Spring的RestTemplate调用IDP的令牌端点,使用urn:ietf:params:oauth:grant-type:saml2-bearer授权类型兑换令牌:

    @RestController
    @RequestMapping("/api/token")
    public class SamlTokenExchangeController {
        private final RestTemplate restTemplate;
        private final RelyingPartyRegistrationRepository registrationRepo;
    
        public SamlTokenExchangeController(RestTemplate restTemplate, RelyingPartyRegistrationRepository registrationRepo) {
            this.restTemplate = restTemplate;
            this.registrationRepo = registrationRepo;
        }
    
        @PostMapping("/exchange")
        public ResponseEntity<Map<String, Object>> exchangeSamlForJwt(@RequestBody Map<String, String> request) {
            String samlAssertion = request.get("saml_assertion");
            if (samlAssertion == null || samlAssertion.isBlank()) {
                return ResponseEntity.badRequest().body(Collections.singletonMap("error", "SAML assertion is required"));
            }
    
            RelyingPartyRegistration registration = registrationRepo.findByRegistrationId("your-idp-registration-id");
            if (registration == null) {
                return ResponseEntity.badRequest().body(Collections.singletonMap("error", "IDP registration not found"));
            }
    
            MultiValueMap<String, String> tokenParams = new LinkedMultiValueMap<>();
            tokenParams.add("grant_type", "urn:ietf:params:oauth:grant-type:saml2-bearer");
            tokenParams.add("assertion", samlAssertion);
            tokenParams.add("client_id", registration.getClientId());
            tokenParams.add("client_secret", registration.getClientSecret());
            tokenParams.add("scope", "openid api:read"); // 根据IDP要求调整
    
            try {
                ResponseEntity<Map> response = restTemplate.postForEntity(registration.getProviderDetails().getTokenUri(), tokenParams, Map.class);
                return ResponseEntity.ok(response.getBody());
            } catch (RestClientException e) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Collections.singletonMap("error", "Failed to exchange SAML assertion: " + e.getMessage()));
            }
        }
    }
    
  2. 配置接口安全规则
    在SecurityFilterChain中允许匿名访问兑换接口(或限制为可信客户端访问):

    .authorizeRequests()
    .antMatchers("/","/login", "/api/token/exchange").permitAll()
    .anyRequest().authenticated()
    
  3. 关键注意事项

    • 确认外部IDP支持urn:ietf:params:oauth:grant-type:saml2-bearer授权类型。
    • 客户端凭证(client_id/client_secret)需妥善存储,避免硬编码,可通过Spring Cloud Config或环境变量注入。
    • 可添加前置校验逻辑,先验证SAML断言的签名,再提交给IDP兑换令牌。

内容的提问来源于stack exchange,提问作者M Aqib Naeem

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 00:15:26