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

Spring Boot后端如何同时验证自研JWT与Azure AD JWT?

问题描述

我有一个基于React前端、Spring Boot 2.7.0后端的Web应用,支持两种登录方式:

  • 账号密码登录:后端完成认证并生成自研JWT返回
  • Azure AD弹窗登录:通过MSAL库弹出登录窗口,登录成功后由MSAL生成Azure AD的JWT,用于向后端请求资源

现在需要在后端实现同时验证自研JWT和Azure AD生成的JWT的功能,目前的SecurityConfig代码中,jwtDecoder方法不知道如何根据待验证的JWT选择对应的解码器,请问最优实现方案是什么?

用户当前的SecurityConfig代码:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private String jwkIssuerUri = "https://login.microsoftonline.com/xxxxxxxxxxxxx/v2.0";

    @Autowired
    JwtTokenValidator jwtTokenValidator;

    private final CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.cors(Customizer.withDefaults()).csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/authenticate")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .oauth2ResourceServer(oauth -> oauth.jwt())
                .exceptionHandling((ex) ->
                        ex.authenticationEntryPoint(
                                new BearerTokenAuthenticationEntryPoint())
                                .accessDeniedHandler(new BearerTokenAccessDeniedHandler())).build();
    }

    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(authProvider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    JwtEncoder jwtEncoder() {
        String key = "secret key";
        return new NimbusJwtEncoder(new ImmutableSecret<>(key.getBytes()));
    }

    @Bean
    JwtDecoder jwtDecoder() {
        if(******* TO DO IF AZURE AD*******) {
            NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(this.jwkIssuerUri);
            OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(this.jwkIssuerUri);
            OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, this.jwtTokenValidator);
            jwtDecoder.setJwtValidator(withAudience);
            return jwtDecoder;
        } else {
            String key = "secret key";
            byte[] bytes = key.getBytes();
            SecretKeySpec originalKey = new SecretKeySpec(bytes, 0, bytes.length,"RSA");
            return NimbusJwtDecoder.withSecretKey(originalKey).macAlgorithm(MacAlgorithm.HS512).build();
        }
    }

}
最优实现方案

核心思路是自定义JwtDecoder代理类,根据JWT的iss(签发者)字段自动路由到对应解码器:提前初始化两个独立解码器,代理类读取JWT的签发者信息,选择匹配的解码器完成验证。

1. 实现自定义多JWT解码器

创建代理类实现JwtDecoder接口,内部持有自研和Azure AD的解码器实例,通过iss字段判断使用哪一个:

import com.nimbusds.jwt.SignedJWT;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;

import java.text.ParseException;

public class MultiJwtDecoder implements JwtDecoder {

    private final JwtDecoder selfIssuedJwtDecoder;
    private final JwtDecoder azureAdJwtDecoder;
    private final String azureAdIssuer;

    public MultiJwtDecoder(JwtDecoder selfIssuedJwtDecoder, JwtDecoder azureAdJwtDecoder, String azureAdIssuer) {
        this.selfIssuedJwtDecoder = selfIssuedJwtDecoder;
        this.azureAdJwtDecoder = azureAdJwtDecoder;
        this.azureAdIssuer = azureAdIssuer;
    }

    @Override
    public Jwt decode(String token) throws JwtException {
        try {
            // 仅解析JWT载荷获取签发者,不验证签名,性能开销极小
            SignedJWT signedJWT = SignedJWT.parse(token);
            String issuer = signedJWT.getJWTClaimsSet().getIssuer();
            
            if (azureAdIssuer.equals(issuer)) {
                return azureAdJwtDecoder.decode(token);
            } else {
                return selfIssuedJwtDecoder.decode(token);
            }
        } catch (ParseException e) {
            throw new JwtException("Failed to parse JWT token", e);
        }
    }
}

2. 修改SecurityConfig配置

拆分原有的jwtDecoder方法,分别初始化自研和Azure AD的解码器,最后用自定义代理类包装:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private String jwkIssuerUri = "https://login.microsoftonline.com/xxxxxxxxxxxxx/v2.0";
    private String azureAdIssuer = "https://login.microsoftonline.com/xxxxxxxxxxxxx/v2.0"; // 与Azure AD实际签发者一致
    private String selfJwtSecret = "secret key";

    private final JwtTokenValidator jwtTokenValidator;
    private final CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.cors(Customizer.withDefaults())
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/authenticate")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .oauth2ResourceServer(oauth -> oauth.jwt(jwt -> jwt.decoder(multiJwtDecoder()))) // 指定自定义解码器
                .exceptionHandling(ex ->
                        ex.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                                .accessDeniedHandler(new BearerTokenAccessDeniedHandler()))
                .build();
    }

    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(authProvider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    JwtEncoder jwtEncoder() {
        return new NimbusJwtEncoder(new ImmutableSecret<>(selfJwtSecret.getBytes()));
    }

    // 初始化自研JWT解码器
    @Bean
    JwtDecoder selfIssuedJwtDecoder() {
        SecretKeySpec secretKey = new SecretKeySpec(selfJwtSecret.getBytes(), "HmacSHA512");
        return NimbusJwtDecoder.withSecretKey(secretKey)
                .macAlgorithm(MacAlgorithm.HS512)
                .build();
    }

    // 初始化Azure AD JWT解码器
    @Bean
    JwtDecoder azureAdJwtDecoder() {
        NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(jwkIssuerUri);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(azureAdIssuer);
        OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withIssuer, jwtTokenValidator);
        jwtDecoder.setJwtValidator(validator);
        return jwtDecoder;
    }

    // 注册自定义代理解码器
    @Bean
    JwtDecoder multiJwtDecoder() {
        return new MultiJwtDecoder(selfIssuedJwtDecoder(), azureAdJwtDecoder(), azureAdIssuer);
    }
}

3. 关键注意事项

  • 签发者区分:自研JWT生成时需指定唯一的iss值(例如https://your-app-domain.com/self-issuer),确保与Azure AD的iss不重复
  • 错误处理:代理类中捕获解析异常并抛出标准JwtException,Spring Security会自动交由配置的异常处理器处理
  • 扩展性:后续如需支持更多类型JWT,只需新增对应解码器实例,并在代理类中添加路由判断逻辑即可

内容的提问来源于stack exchange,提问作者Mario Rudman

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 12:29:56