バックエンド

【Spring Boot】Spring Security × JWT認証で作成した7つのクラスの役割を整理してみた

kim

おはようございます!DWSの木村です!

今回、案件で利用しているSpring Bootを理解するために、自己学習でSpring Securityを使ったJWT認証をjjwtと自作クラスで実装を行いました。

その中で7つのクラスを作成したので、その役割を整理していきます。

※本記事は、Spring SecurityにおけるJWT認証の内部的な流れを理解するためのサンプル実装をもとに、各クラスの役割を整理したものです。
実際の本番環境では、Spring SecurityのOAuth2 Resource Server機能、外部IdPとの連携、鍵管理、鍵ローテーション、トークン失効、監査ログ、権限設計などを含めて検討してください。

JWT とは?

JWT(JSON Web Token)は、ユーザー名や権限、有効期限などの情報を、
署名付きのJSON形式データとしてやり取りするためのコンパクトなトークン形式です。

例えば、上記のような認証フォームにユーザー名とパスワードを入力して、ログインボタンをクリックした場合を想定します。

そうすると、Serverはユーザー名とパスワードを検証し、認証に成功した場合にJWTを生成してClient側に返します。

次に、Clientは受け取ったJWTを使って、APIにGETやPOSTなどのリクエストを送ります。(以下図ではGETリクエストを例にしています。)

ClientがAPIリクエストを送る際、AuthorizationヘッダーにJWTを含めて送信します。

Server側はJWTの署名や有効期限を検証し、問題がなければAPIの処理を実行してレスポンスを返します。

ここまでで、ログイン後にJWTを受け取り、そのJWTを使ってAPIへリクエストを送る流れを確認しました。

次に、実際にClientへ返されるJWTがどのような構造になっているのかを見ていきます。

JWTの実体はただの文字列で、ピリオド(.)で3つに区切られています。

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.xyzSignature
       ↑                      ↑                    ↑
   Header(アルゴリズム)  Payload(中身)       Signature(署名)
  • Header:署名アルゴリズムの種類(例:HS256)
  • Payload:ユーザー名・発行日時・有効期限などの情報
  • Signature:Header + Payload を秘密鍵で署名したもの(改ざん検知に使う)

ここで重要なのは、HeaderとPayloadは暗号化されているわけではなく、Base64URL形式でエンコードされているだけという点です。

そのため、デコードすれば中身を確認できます。

一方で、SignatureはJWTが改ざんされていないかを検証するために使われます。
つまり、JWTは「中身を隠す」ためのものではなく、「改ざんされていないことを確認する」ための仕組みだと理解すると分かりやすいです。

そのため、パスワードなどの機密情報をPayloadに含めないよう、この点に注意してください。

Spring Securityとは

Spring Securityは、Spring Bootアプリに認証・認可の仕組みを提供するフレームワーク です。

ざっくり言うと、次のような仕事をします。

  • 認証:「このユーザーは本物か?」を確認する
  • 認可:「この API にアクセスしていいか?」を判断する
  • SecurityContext:「今ログイン中のユーザーは誰か?」をリクエスト中に保持する

今回は、ユーザーが認証成功時にJWTを発行し、以降のAPIアクセスではそのJWTを使って「認証済みのユーザーかどうか」を判断する実装を行っていきます。

全体構成

まず、全体構成を図で整理します。複雑な処理の流れになっているので、処理の詳細については、この後、実コードを見ながら説明したいと思います。

ログイン時:トークン発行

ログイン後:認証付き APIのトークン検証

フォルダ構成

src/main/
├── java/com/example/training_tracker_api/
│   │
│   ├── auth/
│   │   ├── controller/
│   │   │   └── AuthController.java              ③ ログインAPI
│   │   └── dto/
│   │       ├── LoginRequest.java                ログインリクエスト
│   │       └── LoginResponse.java               ログインレスポンス(JWT返却)
│   │
│   └── common/
│       └── security/
│           ├── JwtProperties.java               ① JWT設定値
│           ├── JwtService.java                  ② JWT生成・検証
│           ├── CustomUserDetailsService.java    ④ ユーザー情報の取得
│           ├── SecurityConfig.java              ⑤ Spring Security設定
│           ├── JwtAuthenticationFilter.java     ⑥ リクエストごとのJWT認証
│           └── CurrentUserProvider.java         ⑦ ログインユーザー取得
│
└── resources/
    └── application.properties                   (jwt.secret / jwt.expiration-ms)

1. 設定値の管理:JwtProperties

JwtProperties という、application.properties に定義したJWT関連の設定値を型安全に読み込むためのクラスを作成しました。読み込んだ値はサービス側(JwtService)で注入して利用します。

今回は、秘密鍵や有効期限などの値をこのクラスにまとめています。

プロパティを読み込むクラスについては、以下の公式ドキュメントでも確認できます。

Type-safe Configuration Properties

https://docs.spring.io/spring-boot/reference/features/external-config.html?utm_source=chatgpt.com#features.external-config.typesafe-configuration-properties

実際のコードは以下の通りです。

// jwt.* の設定値をまとめて受け取るクラス
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {

    private String secret;      // 署名に使う秘密鍵(文字列)

    private long expirationMs;  // トークンの有効期限(ミリ秒)

    // getter / setter は省略

    // アプリ起動直後に設定値の妥当性を検証する
    @PostConstruct
    void validate() {
        if (secret == null) {
            throw new IllegalStateException("jwt.secret is required");
        }

        // 秘密鍵が32バイト未満だとHMAC-SHA256の要件を満たさないためエラー
        int byteLength = secret.getBytes(StandardCharsets.UTF_8).length;
        if (byteLength < 32) {
            throw new IllegalStateException(
                "jwt.secret must be at least 32 bytes (current: " + byteLength + " bytes)"
            );
        }

        if (expirationMs <= 0) {
            throw new IllegalStateException("jwt.expiration-ms must be positive");
        }
    }
}

※なお、今回の実装では、JwtPropertiesをSpringのBeanとして登録するために、アプリケーションクラスに @EnableConfigurationProperties(JwtProperties.class) を付けています。これにより、application.propertiesjwt.*の値をJwtPropertiesに読み込み、JwtServiceから利用できるようになります。

秘密鍵や有効期限の値をプロパティから読み込むほか、
@PostConstructで起動時にバリデーションをかけています。設定値が不正な状態でアプリが動き続けるのを防ぎ、「起動したらすぐ気づける」設計にしています。

@PostConstructについては、以下の公式ドキュメントをご確認ください。

Using @PostConstruct and @PreDestroy

https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/postconstruct-and-predestroy-annotations.html

また、application.propertiesには秘密鍵や有効期限の値を以下のように書きます。
※秘密鍵は仮値を置いているので、実際の値はご自身で設定してください。

jwt.secret=your-256-bit-secret-key-here-minimum-32-bytes
jwt.expiration-ms=3600000

※上記は学習用のサンプルとしてapplication.propertiesに直接記載しています。
本番環境では、JWTの署名に使う秘密鍵を平文でリポジトリに含めないようにしてください。

この値を注入して利用する、サービス側(JwtService)の設定については次章で設定を確認します。

2. トークンの生成・検証:JwtService

JWT の生成と検証を担うJwtServiceクラスを作成しました。主な役割は以下の3つです。

  •  署名鍵の準備 :署名に使う秘密鍵と、有効期限の設定値を準備する
  • トークン発行:ログイン成功後、usernameをもとにJWTを生成する
  • トークン検証:リクエストに含まれるJWTの署名・有効期限を確認し、subjectからユーザー名を取り出す

実際のコードは以下の通りです。

@Service
public class JwtService {

    private final SecretKey signingKey; // 署名・検証に使う秘密鍵

    private final long expirationMs;    // 有効期限(ミリ秒)

    public JwtService(JwtProperties props) {
        // 文字列の秘密鍵をHMAC-SHA用のSecretKeyオブジェクトに変換
        this.signingKey = Keys.hmacShaKeyFor(
            props.getSecret().getBytes(StandardCharsets.UTF_8)
        );
        this.expirationMs = props.getExpirationMs();
    }

    // トークンを生成する(ログイン成功時に呼ばれる)
    public String generateToken(String username) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + expirationMs); // 現在時刻 + 有効期限

        return Jwts.builder()
            .subject(username)  // Payload に username を格納
            .issuedAt(now)      // 発行日時
            .expiration(expiry) // 有効期限
            .signWith(signingKey) // 秘密鍵で署名
            .compact();           // 文字列に変換して返す
    }

    // トークンを検証してユーザー名を取り出す(リクエストごとに呼ばれる)
    public String parseAndValidate(String token) {
        return Jwts.parser()
            .verifyWith(signingKey) // 署名の検証(改ざん検知)
            .build()
            .parseSignedClaims(token) // 有効期限の検証も内部で行われる
            .getPayload()
            .getSubject(); // Payload から username を取り出す
    }
}

役割は前述のとおり、署名鍵の準備、トークン発行、トークン検証の3つです。ここでは、その中で使用している Jwts(jjwt ライブラリ)について少し詳しく見ていきます。

Jwts(jjwt ライブラリ)について

Jwts(jjwt ライブラリ)について、今回以下の2つの役割を担っています。

  • Jwts.builder() :トークン生成
  • Jwts.parser() :トークン検証
        return Jwts.builder()
            .subject(username)  // Payload に username を格納
            .issuedAt(now)      // 発行日時
            .expiration(expiry) // 有効期限
            .signWith(signingKey) // 秘密鍵で署名
            .compact();           // 文字列に変換して返す

まず、Jwts.builder()ではログイン時にJWTを組み立てます。subjectにユーザー名、issuedAtに発行時刻、expirationに有効期限を設定します。その後、signWithで署名し、compact()で文字列形式のJWTに変換します。

      return Jwts.parser()
            .verifyWith(signingKey) // 署名の検証(改ざん検知)
            .build()
            .parseSignedClaims(token) // 有効期限切れの場合も、この解析処理の中で例外が発生する
            .getPayload()
            .getSubject(); // 問題がなければ、payloadからsubject(ユーザー名)を取り出す

次にJwts.parser()では、ログイン後にJWTを解析するためのパーサーを作成し、verifyWith(signingKey)で署名検証に使用する鍵を指定します。

その後、parseSignedClaims(token)を実行すると、JWTの署名検証や有効期限の確認が行われます。

Jwts(jjwt ライブラリ)の詳細については、以下jjwtのGitHub READMEをご確認ください。

jjwt

https://github.com/jwtk/jjwt

3. ログイン処理:AuthController

AuthControllerは、ログインAPI(POST /api/auth/login)を提供するクラスです。ユーザー名・パスワードの認証はSpring Security(AuthenticationManager)任せ、認証成功後のJWT発行はJwtServiceに任せます。

実際のコードは以下の通りです。

AuthControllerのコード

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;

    public AuthController(AuthenticationManager authenticationManager, JwtService jwtService) {
        this.authenticationManager = authenticationManager;
        this.jwtService = jwtService;
    }

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
        try {
            // Spring Security に認証を委譲する
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getUsername(),
                    request.getPassword()
                )
            );
        } catch (BadCredentialsException e) {
            // 認証に失敗した場合は401を返す
            return ResponseEntity.status(401).build();
        }

        // 認証成功 → トークンを生成してレスポンスに含める
        String token = jwtService.generateToken(request.getUsername());
        return ResponseEntity.ok(new LoginResponse(token));
    }
}

リクエストのDTOとレスポンスのDTOも以下の通り記載します。

// リクエストDTO
public class LoginRequest {
    @NotBlank(message = "username is required")
    private String username;

    @NotBlank(message = "password is required")
    private String password;

    // getter / setter 省略
}
// レスポンスDTO(recordで1行)
public record LoginResponse(String token) {}

ここで使用しているUsernamePasswordAuthenticationTokenは、ユーザー名とパスワードを Spring Securityの認証処理に渡すためのオブジェクトです。

今回のコードでは、リクエストで受け取ったusernamepasswordUsernamePasswordAuthenticationTokenに詰めて、AuthenticationManager.authenticate()に渡しています。

つまり、AuthController自身がパスワードを照合しているのではなく、「このユーザー名とパスワードで認証してください」という依頼をAuthenticationManagerに渡しているイメージです。

AuthControllerではAuthenticationManager.authenticate()を呼び出しているだけで、パスワード照合の処理は直接書いていません。

パスワード照合は、Spring Security内部のDaoAuthenticationProviderPasswordEncoderを使って行います。

ここで「DaoAuthenticationProviderなんて、コードにあったっけ?」と思った方もいるかもしれません。

これは正しい反応です。

DaoAuthenticationProviderは自分で直接呼び出すクラスではなく、Spring Securityの認証処理の内部で使われるものです。
このあと、AuthenticationManagerとあわせてこの流れを整理します。

AuthenticationManagerについて

AuthenticationManagerは、Spring Securityの認証処理の入口です。

再度になりますが、ログイン処理では、以下のようにユーザー名とパスワードを UsernamePasswordAuthenticationTokenに詰めて、authenticate() メソッドに渡しています。

         // Spring Security に認証を委譲(パスワードのハッシュ照合もここで行われる)
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getUsername(),
                    request.getPassword()
                )
            );

AuthenticationManagerに渡されたあと、Spring Security内部では以下図のように、DaoAuthenticationProviderを呼び出します。

その後、DaoAuthenticationProviderは、次章以降で説明するCustomUserDetailsServicePasswordEncoderを使って認証処理を行います。

CustomUserDetailsServiceでDBからユーザー情報を取得し、PasswordEncoderでログイン時に入力されたパスワードと、保存済みのハッシュ化パスワードを照合します。

ここで説明したDaoAuthenticationProviderの処理は、以下の公式ドキュメントでも確認できます。

DaoAuthenticationProvider

https://spring.pleiades.io/spring-security/reference/servlet/authentication/passwords/dao-authentication-provider.html

4. ユーザー情報の取得:CustomUserDetailsService

Spring Security がパスワード照合をするとき、内部で「ユーザー情報をどこから取るか」を UserDetailsServiceインターフェース経由で呼びに来ます。これを実装したのが CustomUserDetailsServiceです。

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // Spring Securityからusernameを受け取り、DBからユーザー情報を返す
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // DB にユーザーがいなければ UsernameNotFoundException を投げる
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));

        // ロールを"ROLE_USER"などの形式に変換
        List<SimpleGrantedAuthority> roles = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
            .toList();

        // Spring Securityが扱えるUserDetailsオブジェクトに変換して返す
        return User.builder()
            .username(user.getUsername())
            .password(user.getPassword()) // ハッシュ化済みパスワード
            .authorities(roles)
            .build();
    }
}

CustomUserDetailsServiceは、DBからユーザー情報を取得し、Spring Securityが扱える UserDetailsとして返します。

ここで返すpasswordは、DBに保存されているハッシュ化済みパスワードです。
ログイン時に入力されたパスワードとの照合は、Spring Security内部でPasswordEncoderを使って行われます。

ここで利用しているUserDetailsServiceについては、以下の公式ドキュメントでも確認できます。

UserDetailsService

https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/user-details-service.html

5. セキュリティ設定:SecurityConfig

SecurityConfigは、Spring Securityの認証・認可のルールを組み立てる設定クラスです。

どのURLを認証なしで許可するか、どのURLに認証を必要とするか、どのフィルタを使ってリクエストを処理するかなどをここで設定します。

今回のJWT認証でも、ログインAPIの扱いや、JWTを検証するためのフィルタ設定をこのクラスにまとめます。

@Configuration
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            // AuthorizationヘッダーのJWTで認証するため、CSRFを無効化する
            .csrf(csrf -> csrf.disable())

            // セッションを使わない(JWTはステートレスなのでサーバー側にセッションを持たない)
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            // 認証エラー時は401を返す
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, e) ->
                    res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))
            )

            // 認可設定(どのエンドポイントを認証不要にするか)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/h2-console/**").permitAll() // 開発用 DB コンソールは認証不要
                .requestMatchers("/api/auth/login").permitAll() // ログインは認証不要(当然)
                .anyRequest().authenticated()                   // それ以外はすべて認証必須
            )

            // h2-consoleはiframeで動くためsameOriginを許可
            .headers(h -> h
                .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
            )

            // JWTフィルタをSpring Securityのフィルタチェーンに組み込む
            // UsernamePasswordAuthenticationFilter の前に実行されるように指定
            .addFilterBefore(
                jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class
            )
            .build();
    }

    // パスワードのハッシュ化にBCryptを使う
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // AuthControllerから認証処理を呼び出すために必要
    @Bean
    public AuthenticationManager authenticationManager(
        AuthenticationConfiguration authenticationConfiguration
    ) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

※今回のサンプルコードでは説明を簡単にするため、JWT検証後の権限をROLE_USER固定にしています。実運用では、DBなどで管理しているユーザーの権限情報をもとに、適切な権限を設定する必要があります。
※H2 Consoleは開発用のため、本番環境では無効化する、または公開しないようにしてください

このクラスでは、主に以下の3つをBeanとして定義しています。

  • SecurityFilterChain
  • PasswordEncoder
  • AuthenticationManager

AuthenticationManagerは3章で説明したため、ここではSecurityFilterChainPasswordEncoderを中心に説明します。

SecurityFilterChain

SecurityFilterChainでは、リクエストに対する認証・認可ルールや、使用するフィルタを設定します。

このコードでは、主に以下の設定を行っています。

  • CSRFを無効化する
    • 今回はCookieやセッションではなく、AuthorizationヘッダーのJWTで認証するため
  • JWT認証に合わせて、セッションを作成しない設定にする
  • ログインAPIを認証なしでアクセスできるようにする
  • H2 Consoleを認証なしでアクセスできるようにする
    ※H2 Consoleは開発用のため、本番環境では無効化する、または公開しないようにしてください。
  • それ以外のAPIは、認証済みユーザーのみアクセスできるようにする
  • 次章で作成するJwtAuthenticationFilterをフィルタチェーンに追加する
    • addFilterBefore()により、Controllerの前でJWT検証が行われるようFilterをFilterChainに追加しています。

H2 Console は、開発用のH2データベースをブラウザから確認するための画面です。
また、JwtAuthenticationFilterの具体的な処理は次章で説明します。

ここで説明したSecurityFilterChainの処理は、以下の公式ドキュメントでも確認できます。

HttpSecurity

https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-httpsecurity

PasswordEncoder

PasswordEncoderは、パスワードのハッシュ化と照合に使うインターフェースです。

前章までで説明したように、Spring Security内部のDaoAuthenticationProviderは、PasswordEncoderを使ってパスワードを照合します。

今回の実装では、その具体的な実装クラスとしてBCryptPasswordEncoderをBeanとして登録しています。

上記図のように、ログイン時にはDaoAuthenticationProviderPasswordEncoderを使い、入力されたパスワードとDBに保存されているハッシュ化済みパスワードを照合します。

今回登録しているPasswordEncoderの実装がBCryptPasswordEncoderなので、実際の照合にはBCryptが使われます。

なお、ユーザー登録時にパスワードを保存する場合も、このPasswordEncoderを使ってハッシュ化できます。

6. リクエストごとのトークン検証:JwtAuthenticationFilter

JwtAuthenticationFilterは、リクエストごとに JWT を確認するためのフィルタです。

AuthorizationヘッダーからJWTを取り出し、問題がなければ認証済みユーザーとして扱えるようにします。

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_PREFIX = "Bearer ";

    private final JwtService jwtService;

    public JwtAuthenticationFilter(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {

        // リクエストヘッダーから "Bearer <token>" を取り出す
        String token = resolveToken(request);

        if (token != null) {
            try {
                // トークンを検証し、Payloadのsubjectからユーザー名を取り出す
                String username = jwtService.parseAndValidate(token);

                // 認証情報オブジェクトを作成(パスワードは不要なので null)
                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                        username,
                        null,
                        List.of(new SimpleGrantedAuthority("ROLE_USER"))
                    );

                // SecurityContext に認証情報をセット(これで「認証済み」とみなされる)
                SecurityContextHolder.getContext().setAuthentication(authentication);

            } catch (Exception e) {
                // トークンが不正・期限切れの場合は 401 を返して処理を止める
                SecurityContextHolder.clearContext();
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
                return;
            }
        }

        // 次のフィルタへ処理を渡す
        filterChain.doFilter(request, response);
    }

    // "Authorization: Bearer <token>"ヘッダーからトークン文字列だけ取り出す
    private String resolveToken(HttpServletRequest request) {
        String header = request.getHeader(AUTHORIZATION_HEADER);
        if (header != null && header.startsWith(BEARER_PREFIX)) {
            return header.substring(BEARER_PREFIX.length()); // "Bearer "の7文字を除いた部分
        }
        return null; // ヘッダーがなければnull(認証不要なエンドポイントはこのまま通過する)
    }
}

このコードでは、主に以下の処理を行っています。

  1. AuthorizationヘッダーからBearer 形式のJWTを取り出す
  2. JWTがある場合
    1. JwtServiceで署名や有効期限を検証する
    2. 検証に成功した場合、JWTから取得したユーザー名をもとに認証情報を作成する
    3. 作成した認証情報をSecurityContextにセットする
  3. JWTがない場合
    • 認証情報をセットせずに次のフィルタへ処理を渡す
  4. JWTが不正、または期限切れの場合
    • 401 を返す

ここまでが、JwtAuthenticationFilterの大まかな処理の流れです。

補足として、このクラスで使っているOncePerRequestFilterとresolveToken()についても確認します。

OncePerRequestFilter

OncePerRequestFilterは、1リクエストにつき1回だけ実行されるフィルタを作るための基底クラスです。

通常のFilterでは、リクエストの転送やエラー処理などの影響で、同じリクエスト内で複数回呼ばれる可能性があります。
OncePerRequestFilterを使うことで、JWT検証のような「1リクエストで1回だけ実行したい処理」を安全に実装できます。

今回のJwtAuthenticationFilterでは、リクエストごとにJWTを確認したいので、OncePerRequestFilterを継承しています。

ここで説明しているOncePerRequestFilterについては、以下の公式ドキュメントでも確認できます。

Class OncePerRequestFilter

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html

resolveToken()

resolveToken()AuthorizationヘッダーからJWT部分だけを取り出すために作成したprivateメソッドです。
AuthorizationヘッダーにJWTがない場合、resolveToken()nullを返します。
その場合は認証情報をセットせず、そのまま次のフィルタへ処理を渡します。

補足ですが、ログインAPI(/api/auth/login)についてはSecurityConfigでpermitAll()にしているため、ログインAPIはJWTなしでも通過できます。

7. ログイン中ユーザーの取得:CurrentUserProvider

CurrentUserProviderは、SecurityContextHolderからログイン中のユーザー名を取り出すためのクラスです。

@Component
public class CurrentUserProvider {

    // SecurityContext に格納された認証情報からユーザー名を取り出す
    public String getUsername() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        if (auth == null || !auth.isAuthenticated()) {
            throw new IllegalStateException("Authentication is not available");
        }

        return auth.getName(); // JwtAuthenticationFilter でセットした username が返る
    }
}

6章のJwtAuthenticationFilterでは、JWTの検証に成功したあと、
認証情報をSecurityContextHolderにセットしました。

Service 層ではSecurityContextHolderを直接参照することもできますが、
各Serviceに同じような取得処理を書くと責務が分散してしまいます。

そこで、ログイン中ユーザーの取得処理をCurrentUserProviderにまとめています。

@Service
public class PostService {

    private final CurrentUserProvider currentUserProvider;

    // 「自分の投稿だけ取得する」ような処理で使う
    public List<Post> findMyPosts() {
        String username = currentUserProvider.getUsername();

        // username を使って DB から自分の投稿を取得…
    }
}

まとめ

今回は、Spring SecurityでJWT認証を実装する際に作成した7つのクラスについて、それぞれの役割と処理の流れを整理しました。

初見では複雑に見える構成ですが、役割ごとに分けて見ていくと、ログイン処理、JWTの生成・検証、認証情報の保持、ログイン中ユーザーの取得という流れで整理できます。

一度仕組みを作っておくと、各Serviceから認証済みユーザーを扱いやすくなるため、API開発でも活用しやすい構成だと感じました。

この記事が、Spring SecurityでJWT認証を実装する際の助けになれば幸いです。

AUTHOR
kim
kim
記事URLをコピーしました