我在一个应用程序中有一个身份验证服务器+资源服务器。我花了很多时间搜索和调试,但与此相关的 Spring Boot 3.+ 的更新页面或主题并不多。因此,我完成了这项工作,并希望添加一个将在我的客户端和服务器之间共享的自定义秘密。这就是问题开始的地方......
这是我的身份验证+资源服务器配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${security.jwt.secret}")
private String jwtSecret;
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
CorsConfigurationSource corsConfigurationSource) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));
http.cors(customizer -> customizer.configurationSource(corsConfigurationSource));
return http.build();
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorize -> authorize.requestMatchers("/oauth2/authorize").permitAll().anyRequest().authenticated())
.formLogin(formLogin -> formLogin.loginPage("/login").permitAll())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtEncoder jwtEncoder() {
byte[] keyBytes = Base64.getDecoder().decode(jwtSecret);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
OctetSequenceKey octetKey = new OctetSequenceKey.Builder(secretKeySpec)
.keyID("customKey")
.build();
JWKSet jwkSet = new JWKSet(octetKey);
JWKSource<SecurityContext> jwkSource = (jwkSelector, context) -> {
List<JWK> keys = jwkSelector.select(jwkSet);
if (keys.isEmpty()) {
System.out.println("No keys found matching selection criteria!");
} else {
System.out.println("Keys selected: " + keys.stream().map(JWK::getKeyID).collect(Collectors.joining(", ")));
}
return keys;
};
return new NimbusJwtEncoder(jwkSource);
}
@Bean
JwtDecoder jwtDecoder() {
byte[] keyBytes = Base64.getDecoder().decode(jwtSecret);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
return NimbusJwtDecoder.withSecretKey(secretKeySpec).build();
}
}
我的 app.properties 中有:
security.jwt.secret=r26BoWWyTQMp/8rkD3RnRKsbHkRsmQWjTvJTfmhrQxU=
我的一切都以非对称方式(私钥和公钥)工作,但我也想尝试这个......
现在,当使用客户端登录时,我总是收到:
org.springframework.security.oauth2.jwt.JwtEncodingException:尝试对 Jwt 进行编码时发生错误:无法选择 JWK 签名密钥
我会尝试的几件事:
我会确保
JWKSelector
使用的 NimbusJwtEncoder
与您期望的标准完全匹配,否则选择器可能会查找您未定义的特定属性(例如 use 或 alg)。
我还会添加异常或登录错误以帮助调试过程 对于 JWKSource
最后你可以在测试前先尝试siplyfy一下配置,看看是否有异常,像这样:
/* rest of code */
@Bean
public JwtEncoder jwtEncoder() {
String jwtSecret = "your-secret-key"; // non-Base64 encoded secret for testing
SecretKeySpec secretKeySpec = new SecretKeySpec(jwtSecret.getBytes(), "HmacSHA256");
return new NimbusJwtEncoder(secretKeySpec);
}
/* rest of code */
如果此配置工作正常,没有错误,问题可能出在自定义 JWK 选择逻辑中,在此之后发布您的发现,我们从那里开始
我已经解决了这个问题:
@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
@Value("${jwt.key}")
private String jwtKey;
private final TokenService tokenService;
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
CorsConfigurationSource corsConfigurationSource) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(jwtSpec -> {
jwtSpec.decoder(jwtDecoder());
}));
http.cors(customizer -> customizer.configurationSource(corsConfigurationSource));
return http.build();
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/hello").authenticated()
.anyRequest().permitAll())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder())))
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers(new AntPathRequestMatcher("/h2-console/**"));
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return tokenService.jwtCustomizer();
}
@Bean
public JwtEncoder jwtEncoder() {
return tokenService.jwtEncoder();
}
@Bean
public JwtDecoder jwtDecoder() {
byte[] keyBytes = Base64.getDecoder().decode(jwtKey);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
return NimbusJwtDecoder.withSecretKey(keySpec).build();
}
}
还有 TokenService 类:
@Service
public class TokenService {
@Value("${jwt.key}")
private String jwtKey;
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getJwsHeader().algorithm(MacAlgorithm.HS256);
Date expirationDate =
Date.from(Instant.now().plus(Duration.ofHours(5)));
Date issueDate = Date.from(Instant.now());
context.getClaims().claims(claims -> {
claims.put("exp", expirationDate);
claims.put("iat", issueDate);
claims.put("custom", "custom");
});
}
};
}
public JwtEncoder jwtEncoder() {
return parameters -> {
byte[] secretKeyBytes = Base64.getDecoder().decode(jwtKey);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "HmacSHA256");
try {
MACSigner signer = new MACSigner(secretKeySpec);
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder();
parameters.getClaims().getClaims().forEach((key, value) ->
claimsSetBuilder.claim(key, value instanceof Instant ? Date.from((Instant) value) : value)
);
JWTClaimsSet claimsSet = claimsSetBuilder.build();
JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
SignedJWT signedJWT = new SignedJWT(header, claimsSet);
signedJWT.sign(signer);
return Jwt.withTokenValue(signedJWT.serialize())
.header("alg", header.getAlgorithm().getName())
.subject(claimsSet.getSubject())
.issuer(claimsSet.getIssuer())
.claims(claims -> claims.putAll(claimsSet.getClaims()))
.issuedAt(claimsSet.getIssueTime().toInstant())
.expiresAt(claimsSet.getExpirationTime().toInstant())
.build();
} catch (Exception e) {
throw new IllegalStateException("Error while signing the JWT", e);
}
};
}
}