Spring Boot 4 Complete Guide · レッスン

リソースサーバーのJWT検証とクレーム

JWTアクセストークンを検証し、発行者とオーディエンスを確認して、クレームから権限を抽出します。

レッスン 1/413 ステップ

「リソースサーバーのJWT検証とクレーム」はCoddyKit上の無料Spring Boot 4 Complete Guideレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはSpring Boot 4 Complete Guide学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Spring Boot 4 Complete Guideコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

The Resource Server Role

In OAuth2, a resource server is the API that holds protected data. It does not log users in or issue tokens. Its only job at request time is to validate the access token that an authorization server already issued, then authorize the call.

  • Tokens are usually JWTs (JSON Web Tokens) signed by the authorization server.
  • Validation is stateless: the resource server verifies the signature and claims without calling a database.
  • Spring Security ships a dedicated oauth2ResourceServer DSL for exactly this.

In this lesson you will validate JWTs, verify the iss and aud claims, and turn claims into Spring authorities.

Anatomy of a JWT

A JWT has three Base64URL parts separated by dots: header.payload.signature. The payload carries claims the resource server inspects:

  • iss — issuer, the URL of the authorization server.
  • sub — subject, the user or client id.
  • aud — audience, who the token is meant for.
  • exp / nbf / iat — expiry, not-before, issued-at timestamps.
  • scope or scp — granted OAuth2 scopes.

The example below shows how a decoded payload looks as plain JSON. Validating means: signature is genuine AND these claims are acceptable.

// A decoded JWT payload (claims) as JSON
{
  "iss": "https://issuer.example.com",
  "sub": "user-1234",
  "aud": ["orders-api"],
  "scope": "orders.read orders.write",
  "roles": ["ADMIN", "USER"],
  "iat": 1735689600,
  "nbf": 1735689600,
  "exp": 1735693200
}

Minimal Resource Server Config

With spring-boot-starter-oauth2-resource-server on the classpath, you enable JWT validation through the security DSL. The jwt() configurer wires up signature verification and standard timestamp checks automatically.

Point Spring at the authorization server's metadata via the issuer URI and it will discover the JWK Set endpoint (the public keys) on its own.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults()));
        return http.build();
    }
}

Configuring the Issuer URI

The cleanest way to set things up is the issuer-uri property. On startup Spring fetches {issuer}/.well-known/openid-configuration (or the OAuth2 equivalent), reads the jwks_uri, and builds a JwtDecoder that caches and rotates signing keys.

It also installs an issuer validator: every token's iss claim must equal this value, or the token is rejected.

# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://issuer.example.com
          # Optional: pin the audience(s) accepted by this API
          audiences:
            - orders-api

How the JwtDecoder Validates

When issuer-uri is set, Spring builds a NimbusJwtDecoder backed by the discovered JWK Set. Each incoming token goes through:

  • Signature check — the token's kid selects a public key from the JWK Set; the signature must verify.
  • Timestamp check — exp must be in the future, nbf in the past (with small clock skew).
  • Issuer check — iss must match the configured issuer.

You can build the same decoder manually when you need to attach extra validators.

@Bean
JwtDecoder jwtDecoder() {
    String issuer = "https://issuer.example.com";
    NimbusJwtDecoder decoder =
        JwtDecoders.fromIssuerLocation(issuer);

    OAuth2TokenValidator<Jwt> withIssuer =
        JwtValidators.createDefaultWithIssuer(issuer);
    decoder.setJwtValidator(withIssuer);
    return decoder;
}

Verifying the Audience Claim

The default validators check timestamps and issuer, but not the audience. Skipping the aud check is a real risk: a token minted for another API on the same issuer would otherwise be accepted here. This is the classic token confusion attack.

Write a custom OAuth2TokenValidator<Jwt> that asserts your API's identifier is present in the aud list, and chain it with the defaults.

public class AudienceValidator
        implements OAuth2TokenValidator<Jwt> {

    private final String audience;

    public AudienceValidator(String audience) {
        this.audience = audience;
    }

    @Override
    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        if (jwt.getAudience().contains(audience)) {
            return OAuth2TokenValidatorResult.success();
        }
        OAuth2Error error = new OAuth2Error(
            OAuth2ErrorCodes.INVALID_TOKEN,
            "Required audience is missing", null);
        return OAuth2TokenValidatorResult.failure(error);
    }
}

Chaining Validators in the Decoder

Combine the audience validator with the default-with-issuer chain using DelegatingOAuth2TokenValidator. Order does not matter for correctness — all validators must pass — but keep signature and timestamp checks (handled internally) plus issuer and audience together.

This decoder bean overrides the auto-configured one while still reusing the discovered JWK keys.

@Bean
JwtDecoder jwtDecoder(
        @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuer,
        @Value("${api.audience}") String audience) {

    NimbusJwtDecoder decoder =
        JwtDecoders.fromIssuerLocation(issuer);

    OAuth2TokenValidator<Jwt> validator =
        new DelegatingOAuth2TokenValidator<>(
            JwtValidators.createDefaultWithIssuer(issuer),
            new AudienceValidator(audience));

    decoder.setJwtValidator(validator);
    return decoder;
}

From Scopes to Authorities

By default Spring maps the scope (or scp) claim to authorities, prefixing each with SCOPE_. So scope: "orders.read" becomes the authority SCOPE_orders.read, which you can require in the DSL or with method security.

  • hasAuthority("SCOPE_orders.read") in authorizeHttpRequests.
  • @PreAuthorize("hasAuthority('SCOPE_orders.write')") on a method.

This default works out of the box, no converter needed.

http.authorizeHttpRequests(auth -> auth
    .requestMatchers(HttpMethod.GET, "/orders/**")
        .hasAuthority("SCOPE_orders.read")
    .requestMatchers(HttpMethod.POST, "/orders/**")
        .hasAuthority("SCOPE_orders.write")
    .anyRequest().authenticated());

Extracting Roles from a Custom Claim

Many identity providers (Keycloak, Auth0, Entra ID) put roles in a non-standard claim such as roles or realm_access.roles, not in scope. To map these, supply a JwtAuthenticationConverter with a custom JwtGrantedAuthoritiesConverter-like function.

Here we read a top-level roles array and emit ROLE_-prefixed authorities so hasRole(...) works.

@Bean
JwtAuthenticationConverter jwtAuthConverter() {
    JwtAuthenticationConverter converter =
        new JwtAuthenticationConverter();

    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
        List<String> roles =
            jwt.getClaimAsStringList("roles");
        if (roles == null) return List.of();
        return roles.stream()
            .map(r -> new SimpleGrantedAuthority("ROLE_" + r))
            .collect(Collectors.toList());
    });
    return converter;
}

Wiring the Converter and Reading Claims

Register the converter on the resource server DSL so it is used to build the Authentication. Once wired, your controllers can inject the validated Jwt and read any claim directly — it is already verified by the time the handler runs.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain chain(HttpSecurity http,
            JwtAuthenticationConverter converter) throws Exception {
        http.oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt.jwtAuthenticationConverter(converter)));
        return http.build();
    }
}

@RestController
class MeController {
    @GetMapping("/me")
    Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) {
        return Map.of(
            "sub", jwt.getSubject(),
            "issuer", jwt.getIssuer().toString(),
            "roles", jwt.getClaimAsStringList("roles"));
    }
}

Building Authorities in Plain Java

The role-mapping logic is just data transformation — the same logic the converter runs, minus the framework. The snippet below is a standalone program that demonstrates turning a claim value into ROLE_-prefixed and SCOPE_-prefixed authorities, exactly mirroring Spring's defaults.

import java.util.*;
import java.util.stream.*;

public class AuthorityMapping {
    static List<String> fromRoles(List<String> roles) {
        return roles.stream()
            .map(r -> "ROLE_" + r)
            .collect(Collectors.toList());
    }

    static List<String> fromScope(String scope) {
        return Arrays.stream(scope.split(" "))
            .filter(s -> !s.isBlank())
            .map(s -> "SCOPE_" + s)
            .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        List<String> authorities = new ArrayList<>();
        authorities.addAll(fromRoles(List.of("ADMIN", "USER")));
        authorities.addAll(fromScope("orders.read orders.write"));
        System.out.println(authorities);
    }
}

Quick Check

Your resource server uses issuer-uri, so signature, expiry, and issuer are validated automatically. A pentester sends a valid, unexpired token that was issued by the same authorization server but minted for a different API. Your endpoint accepts it. What is the fix?

Recap

You configured a Spring Boot 4 resource server to validate JWT access tokens end to end:

  • Issuer URI auto-discovers the JWK Set and installs signature, timestamp, and issuer validation.
  • Audience must be checked explicitly with a custom OAuth2TokenValidator<Jwt> to block token confusion — defaults do not do this.
  • Validators chain via DelegatingOAuth2TokenValidator so issuer + audience + defaults all apply.
  • Authorities come from the scope claim as SCOPE_* by default; a JwtAuthenticationConverter maps custom claims like roles to ROLE_*.
  • Controllers read verified claims via @AuthenticationPrincipal Jwt.

Validate the signature, then never trust a claim you have not explicitly verified.

無料で開始

AI チューターと学ぶ Java — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
21
レッスン
84

よくある質問

「リソースサーバーのJWT検証とクレーム」レッスンは無料ですか?

はい。「リソースサーバーのJWT検証とクレーム」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Spring Boot 4 Complete Guideコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Spring Boot 4 Complete Guideコースには全4レッスンが含まれています。

「リソースサーバーのJWT検証とクレーム」で何を学びますか?

JWTアクセストークンを検証し、発行者とオーディエンスを確認して、クレームから権限を抽出します。 ブラウザで直接実行するハンズオンコードでSpring Boot 4 Complete Guideを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Spring Boot 4 Complete Guideを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのSpring Boot 4 Complete Guideは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「リソースサーバーのJWT検証とクレーム」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このSpring Boot 4 Complete Guideレッスンでコードを書いて実行できますか?

はい。すべてのSpring Boot 4 Complete Guideレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. リソースサーバーのJWT検証とクレーム
  2. OAuth2クライアントと認可コードフロー
  3. SpELとカスタム投票者によるメソッドセキュリティ
  4. 不透明トークンのイントロスペクションとトークン交換
← Spring Boot 4 Complete Guideに戻る