JWTEncoder: не удалось выбрать ключ подписи JWK

У меня есть сервер аутентификации + сервер ресурсов в одном приложении. Я потратил много времени на поиск и отладку, но о 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

  1. Чего мне не хватает на сервере?
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
0
326
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Несколько вещей, которые я бы попробовал:

Я бы удостоверился, что 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);
            }
        };
    }
}

Другие вопросы по теме