У меня есть сервер аутентификации + сервер ресурсов в одном приложении. Я потратил много времени на поиск и отладку, но о 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
, соответствует именно тем критериям, которые вы ожидаете. В противном случае селектор может искать определенные атрибуты (например, использование или alg), которые вы не определили.
Я бы также добавил исключение или ошибку регистрации, чтобы помочь в процессе отладки. для JWKSource
Наконец, вы можете попробовать упростить конфигурацию перед тестированием, чтобы увидеть, есть ли что-то ненормальное, примерно так:
/* 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);
}
};
}
}
Чтобы устранить проблему «JWTEncoder: не удалось выбрать ключ подписи JWK», предоставленное решение правильно настраивает классы SecurityConfig
и TokenService
для обработки кодирования и декодирования JWT с использованием метода симметричного ключа (HMAC). Установив jwt.key
в application.properties
, решение гарантирует, что ключ будет постоянно доступен в различных компонентах приложения. Конфигурация внутри SecurityConfig
устанавливает фильтры безопасности и настройки сервера ресурсов, гарантируя правильное декодирование и проверку JWT по предоставленному симметричному ключу. Этот подход использует возможности Spring Security, упрощая обслуживание и интеграцию в приложение Spring Boot.
Класс TokenService
предназначен для настройки утверждений и заголовков JWT, обеспечивая гибкость и контроль над содержимым и структурой токена. Определив OAuth2TokenCustomizer
, решение гарантирует, что токены включают необходимые утверждения, такие как срок действия (exp
), дата выдачи (iat
) и пользовательские атрибуты. Метод JwtEncoder
внутри TokenService
обрабатывает подпись JWT с использованием алгоритма HMAC, обеспечивая целостность и подлинность токена. Такое разделение задач, при котором SecurityConfig
управляет фильтрами безопасности, а TokenService
занимается настройкой и кодированием токенов, способствует созданию чистой и модульной базы кода. Это также гарантирует согласованность процессов кодирования и декодирования JWT, что снижает вероятность ошибок и повышает общую безопасность.
Чтобы устранить проблему «JWTEncoder: не удалось выбрать ключ подписи JWK», вы можете обновить конфигурацию следующим образом:
Убедитесь, что ваш jwt.key правильно установлен в application.properties.
jwt.key=r26BoWWyTQMp/8rkD3RnRKsbHkRsmQWjTvJTfmhrQxU=
Обновите SecurityConfig, чтобы использовать TokenService для кодирования и декодирования JWT. Вот полная конфигурация:
@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();
}
}
Here is the TokenService class:
@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);
}
};
}
}