2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SpringSecurityのJWT検証と失敗時の制御

2
Last updated at Posted at 2025-12-23

はじめに

ZYYXのアドベントカレンダー企画で7本目の投稿となります

案件でSpringSecurityについて初めて触れ、SpringSecurityでのJWT形式トークンの検証処理の実装と検証失敗時の制御の実装を行ったため、備忘録として記します

サンプルコード

今回ではAPIのリクエストに対して、JWTの検証を行いその検証失敗時の動作を制御することを考えます

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
    @Order(0)
    @Bean
    public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .requestMatcher(new AndRequestMatcher(
                        new AntPathRequestMatcher("/api/**"),
                        new ApiAppRequestMatcher()
                ))
                .authorizeRequests(auth -> auth.anyRequest().authenticated())
                .oauth2ResourceServer(oauth -> oauth
                        .bearerTokenResolver(request -> "access_token"))
                        .jwt(jwt -> jwt.decoder(jwtDecoderUsingJwkSetUri()))
                        .authenticationEntryPoint(customJwtAuthenticationEntryPoint())
                )

        return http.build();
    }

    @Bean
    JwtDecoder jwtDecoderUsingJwkSetUri() {
        var decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();

        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);

        OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

        decoder.setJwtValidator(validator);
        return decoder;
    }

    @Bean
    public AuthenticationEntryPoint customJwtAuthenticationEntryPoint() {
        return new CustomJwtAuthenticationEntryPoint();
    }
}

SecurityConfigのFilterChainの設定

SecutityConfigクラスにて、特定のパスに適用するFilterChainを登録します
requestMatcherはAntPathRequestMatcherを利用して定義しています

パスごとに適用するFilterChainがある場合、FilterChainを複数登録することになりますが、その際の適用順序は@Orderで決定することができ、若い番号ほど優先されます

@Order(0)
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
    http
            requestMatcher(new AndRequestMatcher(
                    new AntPathRequestMatcher("/api/**"),
                    new ApiRequestMatcher()
            ))
            .authorizeRequests(auth -> auth.anyRequest().authenticated())
            // 省略

    return http.build();
}

RequestMatcherクラスをインプリメントしてヘッダーのAPIキーや、UserAgentを判定する自作のRequestMatcherクラスを適用することもできます

public class ApiRequestMatcher implements RequestMatcher {
    @Override
    public boolean matches(HttpServletRequest request) {
        String userAgent = request.getHeader("userAgent");

        boolean isApi = // userAgentに対する判定処理

        return isApi;
    }
}

また、古いSpringSecurityのバージョンであれば以下の様にWebSecurityConfigurerAdapterを継承して、configureメソッドから作成する様な形でしたが、最近は非推奨となっています
おそらく、5.4からがFilterChainを利用できるようになり、5.7から非推奨となり6.0で完全に削除されたかと思います

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 省略
    }

公開鍵の取得

JWTをデコードする際の公開鍵を取得します
今回では、公開鍵のパスをjwtSetUriで直接指定して、取得しています

var decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();

以下の様にissuerUriを指定して、公開鍵を自動的に取得する方法もあります
この場合はiss検証については合わせて自動でやってくれます

var decoder = JwtDecoders.fromIssuerLocation(issuerUri);

JWTの検証設定

今回はJWT検証時にiss,exp,nbf,audを検証することを考えます
標準のJwtValidatorsを利用すると、クレームのiss,exp,nbfについてはデフォルトで設定されています

// デフォルトのiss,exp/nbfのValidator
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);

// audienceの検証用のValidator
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);

// デフォルトのValidatorとaudienceのValidatorを組み合わせたValidator
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

decoder.setJwtValidator(validator);

audについては、デフォルトのJwtValidatorsには含まれていないため、以下のように自作のValidatorを作成し、DelegatingOAuth2TokenValidatorからデフォルトのJwtValidatorsとAudienceValidatorを組み合わせたOAuth2TokenValidatorを作成します

@RequiredArgsConstructor
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    private final String audience;

    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        List<String> audiences = jwt.getAudience();
        if (audiences.contains(audience)) {
            return OAuth2TokenValidatorResult.success();
        } else {
            OAuth2Error error = new OAuth2Error(
                    "invalid_token",
                    "The required audience is missing",
                    null
            );
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
}

また、今回出てきたクレームは以下です
他にもJWTトークンにクレームはありますが、一般的によく検証される項目はこの4つなのかなと

クレーム名 説明 検証
iss(Issuer) 発行者の識別子 本来の発行先であるかをチェックする
exp(ExpirationTime) JWTが無効になる日時 いわゆるトークンの有効期限で期限切れでないかをチェックする
nbf(NotBefore) JWTが有効になる日時 トークンが有効になる前に利用されていないかをチェックする
aud(Audience) アクセスが許可されたリソースの識別子 自身のシステムに対してアクセスを許可されたものかチェックする

FilterChainへのJWT検証の登録

以下によって、検証対象となるトークンの対象の設定・JWTデコーダーの登録・検証失敗時の動作の設定を行なっています

bearerTokenResolverでは、トークンの検証対象となる文字列を設定しています
今回は、access_tokenというヘッダにある値を検証対象としています
デフォルトでは、Authorizationヘッダーとなっているため、Authorizationヘッダーを利用している場合は設定不要です

authenticationEntryPointでは、トークン検証が失敗した場合に実行されるメソッドで、SpringSecurity標準動作でなく、カスタムのレスポンスボディなどを返却したり指定の動作を実装する場合に利用します
詳しくは次項で解説します

.oauth2ResourceServer(oauth -> oauth
        .bearerTokenResolver(request -> request.getHeader("access_token"))
        .jwt(jwt -> jwt.decoder(jwtDecoderUsingJwkSetUri()))
        .authenticationEntryPoint(customJwtAuthenticationEntryPoint())

AuthenticationEntryPointの登録

AuthenticationEntryPointを登録していないデフォルトの動作では、レスポンスのステータスが401でボディがない状態となっています

システムで定義している共通のレスポンスボディに変換してレスポンスを返したい場合があると思いますが、その場合は以下のAuthenticationEntryPointをインプリメントした自作クラスを作成します
これによって、システム共通のレスポンス形式に合わせることもできます
また、ログ出力なども実行することができます

public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {

        int status = HttpServletResponse.SC_UNAUTHORIZED;

        ErrorResponse errorResponse = new ErrorResponse(
                status,
                authException.getMessage()
        );

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(status);

        objectMapper.writeValue(response.getWriter(), errorResponse);
    }
}

public class ErrorResponse {

    private final int status;
    private final String message;

    public ErrorResponse(int status, String message) {
        this.status = status;
        this.message = message;
    }
}

まとめ

SpringSecurityについては、初めは理解が乏しくどこのクラスでどう処理を実装したらいいのか分からず苦労していましたが、最終的に整理された実装を見ると実装量も少なく便利だなという印象を受けます

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?