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
相关产品推荐
相关产品推荐

