REST APIを開発していると、 セッションを使わずにリクエストごとに認証を完結させたい という場面が必ず来ます。フォーム認証やBasic認証でSpring Securityに慣れてきた方の、次のステップが JWT(JSON Web Token)によるステートレス認証 です。
Spring SecurityでBasic認証・フォーム認証を実装する方法 では認証の基本を解説しました。本記事ではその続きとして、REST API向けに JWT発行・検証・Filterへの組み込み をゼロから実装します。
読み終えると、次のことが独力でできるようになります。
/api/auth/loginにPOSTしてJWTを受け取る- 取得したトークンをAuthorizationヘッダーに付与して保護エンドポイントにアクセスする
- curl / Postmanでエンドツーエンドの動作を確認する
実装の全体像は次の流れです。
依存関係追加 → JwtUtil → ログインエンドポイント → JwtAuthenticationFilter → SecurityFilterChain設定
順番に進めましょう。
JWTとステートレス認証の仕組みを3分で理解する
JWTの3パート構成
JWT(JSON Web Token)は ドット(.)で区切られた3つのパート から構成されます。
header.payload.signature
| パート | 内容 | エンコード |
|---|---|---|
| Header | アルゴリズム種別(例:HS256) | Base64URL |
| Payload | クレーム(ユーザー名・有効期限など) | Base64URL |
| Signature | HeaderとPayloadをまとめて秘密鍵で署名したもの | バイナリのBase64URL |
HeaderとPayloadは Base64URLエンコードされているだけ で暗号化はされていません。機密情報(パスワードなど)はPayloadに含めないようにしましょう。Signatureが改ざん検知の要です。
ステートレス認証のフロー
クライアント サーバー
| |
| POST /api/auth/login |
| { username, password } -------> |
| | 認証成功 → JWT生成
| { token: "eyJ..." } <--------- |
| |
| GET /api/protected |
| Authorization: Bearer eyJ... --> |
| | JWTを検証 → SecurityContextにセット
| 200 OK <--------- |
ポイントは サーバー側にセッションを保持しない ことです。トークン自体に認証情報が含まれるため、複数のサーバーインスタンスに水平スケールしやすくなります。フォーム認証・Basic認証との根本的な違いはここにあります。
本記事のスコープ外: OAuth2/OIDC連携、リフレッシュトークンの詳細実装、マイクロサービス間のJWT伝播は扱いません。
依存関係の追加(Maven / Gradle)
本記事では jjwt(Java JWT)ライブラリを使います。jjwt 0.11以降はアーティファクトが3つに分かれています。
| アーティファクト | 役割 |
|---|---|
jjwt-api | APIインターフェース |
jjwt-impl | 実装(ランタイムのみ必要) |
jjwt-jackson | JacksonによるJSON解析 |
Maven(pom.xml)
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Gradle(build.gradle)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
}
注意: 古い
jjwt 0.9.x系はAPIが大きく異なります。ネット上の記事でJwts.parser().setSigningKey(...)という書き方を見かけたら 0.9.x 系のコードです。本記事は 0.11以降の新API を使います。バージョンは Maven Central で最新版を確認してください。
application.properties:JWT秘密鍵・有効期限の外部化
まず設定値を外部化しておきます。秘密鍵は 本番環境では環境変数やVaultで管理 し、application.propertiesに直書きしないようにしましょう(詳細は後述)。
# JWT設定
# jwt.secretは32文字(256ビット)以上のランダムな文字列を設定してください
# 生成例: openssl rand -hex 32
jwt.secret=please-change-this-secret-key-in-production-environment-32chars
jwt.expiration=86400000
# 86400000ms = 24時間
単位に注意:
jwt.expirationは ミリ秒 で指定します。秒と間違えると1000倍短いトークンが発行されます(よくある罠です)。
JwtUtil:トークン生成・クレーム抽出・有効期限検証
JWT操作をすべて一か所にまとめたユーティリティクラスを実装します。
package com.example.demo.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
// 秘密鍵を生成(application.propertiesの値をUTF-8バイト列としてHMAC-SHA256鍵に変換)
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
// トークン生成
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
// ユーザー名の抽出
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 有効期限の抽出
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 汎用クレーム抽出
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
// 署名検証を行いながらClaimsを取得する。不正なトークンはここで例外になる
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
// 有効期限チェック
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// トークン検証(署名検証 + 有効期限チェック + ユーザー名一致確認)
// 注意: extractAllClaims内でExpiredJwtException等がスローされる場合があります。
// 呼び出し元のJwtAuthenticationFilterでtry-catchしてください。
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
}
例外の扱い方針:
| 例外 | 意味 | 対処 |
|---|---|---|
ExpiredJwtException | 有効期限切れ | 401を返す |
MalformedJwtException | トークン形式不正 | 401を返す |
SignatureException | 署名不正(改ざん) | 401を返す |
UnsupportedJwtException | 非対応のJWT形式 | 401を返す |
これらの例外は後述の JwtAuthenticationFilter でキャッチします。
UserDetailsServiceの実装
Spring SecurityがユーザーをロードするためのServiceを用意します。
注意: 以下のインメモリ実装(
UserDetailsConfig)とDB連携実装(UserDetailsServiceImpl)は どちらか一方のみ プロジェクトに定義してください。両方定義するとUserDetailsServiceのBean定義が競合してアプリが起動しません。インメモリからDB連携に移行する手順:
UserDetailsConfigクラスの@Configurationアノテーションを削除するか、クラスごと削除するUserDetailsServiceImplの@Serviceアノテーションを有効にする(下記コードでは既に付与済み)PasswordEncoderの@Bean定義をSecurityConfig等に移動する
インメモリ実装(サンプル・検証用)
package com.example.demo.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class UserDetailsConfig {
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
return new InMemoryUserDetailsManager(
User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build()
);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
DB連携への拡張(実用パターン)
本番環境ではDBからユーザーをロードします。インターフェースの実装は以下のパターンが基本です。
package com.example.demo.security;
import com.example.demo.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.map(user -> User.builder()
.username(user.getUsername())
.password(user.getPassword()) // BCryptでエンコード済みの値
.roles(user.getRole())
.build()
)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
}
getPassword() は BCryptでエンコード済みのハッシュ値 を返す必要があります。平文パスワードをそのまま返すと認証が常に失敗するので注意してください。
LoginRequest / AuthenticationResponse DTOクラス
package com.example.demo.auth;
import jakarta.validation.constraints.NotBlank;
public record LoginRequest(
@NotBlank String username,
@NotBlank String password
) {}
package com.example.demo.auth;
public record AuthenticationResponse(String token) {}
バリデーションアノテーションの詳細は Spring Bootで@Validを使ってバリデーションする方法 を参照してください。
/api/auth/login エンドポイントの実装
package com.example.demo.auth;
import com.example.demo.security.JwtUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.*;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<AuthenticationResponse> login(@Valid @RequestBody LoginRequest request) {
try {
// Spring Securityに認証を委譲(失敗時はBadCredentialsExceptionがスロー)
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password())
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(401).build();
}
// 認証成功 → JWTを生成して返す
UserDetails userDetails = userDetailsService.loadUserByUsername(request.username());
String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(token));
}
}
ポイント: AuthenticationManager.authenticate() はSpring Securityの認証メカニズム全体を呼び出します。パスワードのBCrypt検証やUserDetailsServiceの呼び出しを自動で行ってくれるため、自前で比較処理を書く必要はありません。
エラーハンドリングの設計については Spring BootのREST APIで例外処理を統一する方法 も参考にしてください。
JwtAuthenticationFilter の実装(OncePerRequestFilter)
すべてのリクエストで 1度だけ 実行されるFilterを実装します。これがJWT認証の中核です。
注意: このクラスには
@Componentを付けないでください。@Componentを付けるとSpring Bootがサーブレットフィルターとして自動登録してしまい、SecurityFilterChainへのaddFilterBefore登録と合わせてフィルターが 二重登録 されます。SecurityConfig内で@Beanとして定義することで、セキュリティチェーン専用のフィルターとして正しく機能します。
package com.example.demo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
// Authorizationヘッダーが存在しない、またはBearerトークンでない場合はスキップ
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String token = authHeader.substring(7); // "Bearer "の7文字を除去
final String username;
try {
username = jwtUtil.extractUsername(token);
} catch (Exception e) {
// トークン形式不正・署名エラーなど → 401を返してFilterChainを止める
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token");
return;
}
// ユーザー名が取得でき、かつまだ認証されていない場合のみ処理を続ける
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, userDetails)) {
// 認証オブジェクトを生成してSecurityContextにセット
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
} else {
// 有効期限切れなど検証失敗
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT token expired or invalid");
return;
}
}
filterChain.doFilter(request, response);
}
}
OncePerRequestFilter を使う理由: サーブレットのFilterはリクエストの転送(forward)などで複数回呼ばれる場合があります。OncePerRequestFilter はリクエストごとに 確実に1回だけ 実行されることを保証します。
SecurityFilterChainの設定
package com.example.demo.security;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
// JwtAuthenticationFilterをBeanとして定義(@Componentは付けない)
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtUtil, userDetailsService);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ステートレスAPIではCSRFトークンは不要(セッションを使わないため)
.csrf(csrf -> csrf.disable())
// セッションを一切生成・使用しない
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 未認証リクエストに対して401を返す
.exceptionHandling(e -> e.authenticationEntryPoint(
(req, res, ex) -> res.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
))
// エンドポイントごとのアクセス制御
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // ログインは認証不要
.anyRequest().authenticated() // それ以外はすべて認証必須
)
// JwtAuthenticationFilterをUsernamePasswordAuthenticationFilterの前に挿入
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// AuthenticationManagerをBeanとして公開(AuthenticationControllerで使用)
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config
) throws Exception {
return config.getAuthenticationManager();
}
}
addFilterBefore の位置について: UsernamePasswordAuthenticationFilter の 前に 挿入することで、Spring Securityがフォーム認証を試みる前にJWT検証が走ります。JWTが有効であればSecurityContextに認証情報がセットされるため、以降のFilterは認証済みとして扱います。
csrf().disable() の根拠: CSRFは主に ** ブラウザのセッションCookie ** を悪用した攻撃です。ステートレスなJWT認証ではCookieではなくAuthorizationヘッダーでトークンを送るため、CSRF攻撃が成立しません。ただし ** HTTPS化は必須** です(後述)。
動作確認:curl / Postman でエンドツーエンドテスト
アプリを起動したら、以下の手順で動作を確認しましょう。
1. ログインしてJWTを取得
curl -s -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "user", "password": "password"}'
レスポンス例:
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzA2MDAwMDAwLCJleHAiOjE3MDYwODY0MDB9.xxxxx"
}
2. 取得したトークンで保護エンドポイントにアクセス
# TOKEN変数にレスポンスのtokenをセット
TOKEN="eyJhbGciOiJIUzI1NiJ9..."
curl -s http://localhost:8080/api/hello \
-H "Authorization: Bearer $TOKEN"
3. 異常系の確認
Spring Security は未認証リクエストに対して 401 Unauthorized を返します。exceptionHandling の authenticationEntryPoint で明示的に設定しているため、以下のいずれのケースでも 401 が返ります。
# トークンなし → 401
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/hello
# 出力例: 401
# 改ざんトークン → 401
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/hello \
-H "Authorization: Bearer invalidtoken"
# 出力例: 401
# 期限切れトークン → 401
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/hello \
-H "Authorization: Bearer <expired-token>"
# 出力例: 401
Postmanでの効率的なテスト
Postmanの Environment Variables に token 変数を定義し、ログインリクエストの Tests タブに以下を設定すると、以降のリクエストに自動でトークンが付与されます。
const response = pm.response.json();
pm.environment.set("token", response.token);
その後のリクエストのAuthorizationタブで Bearer {{token}} と指定するだけです。
よくあるエラーと対処法
403 Forbiddenが返る(トークンを送っているのに)
SecurityFilterChainのrequestMatchers設定が正しいか確認するSecurityConfig内に@Bean public JwtAuthenticationFilter jwtAuthenticationFilter()が定義されているか確認するaddFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)が設定されているか確認する
401 Unauthorized(トークンを送っているのに)
SecurityContextHolder.getContext().setAuthentication()が実際に呼ばれているかデバッグログで確認するapplication.propertiesのjwt.secretが 32文字(256ビット)以上のランダムな文字列 になっているか確認する- トークンを発行したサーバーと検証するサーバーで 秘密鍵が同一 かどうか確認する
ExpiredJwtException が頻発する
jwt.expirationの単位を確認する。ミリ秒 で指定する必要があります。86400と書くと86.4秒で切れます(正しくは86400000)
SignatureException:署名検証に失敗する
- 環境ごとに
jwt.secretの値が異なっていないか確認する(開発・ステージング・本番で同じトークンを使い回している場合など) jwt.secretに十分な長さ(32文字以上)の文字列が設定されているか確認する
NullPointerException in doFilterInternal
Authorizationヘッダーが存在しないリクエストに対するnullチェックを忘れていないか確認する。本記事のFilterではauthHeader == nullのチェックを最初に行っています
よくある質問
JWTとセッション認証はどう違いますか?REST APIにJWTが向いている理由は?
セッション認証はサーバー側にセッションストアが必要で、複数インスタンスへのスケールアウト時にセッション共有(Redis等)が必要になります。JWTはトークン自体に認証情報を含むため サーバー側にステートが不要 で、スケールアウトが容易です。REST APIは本質的にステートレスな設計が好ましいため、JWTとの相性が良いです。
jjwtとjava-jwt(Auth0)どちらを使うべきですか?
どちらも広く使われていますが、Spring Bootコミュニティでの採用例が多く、APIが直感的な jjwt を本記事では採用しています。機能的にはほぼ同等です。プロジェクトで既にどちらかを使っているなら統一するのがよいでしょう。
秘密鍵はどこに保存すれば安全ですか?
application.properties への直書きは 開発環境のみ にしてください。本番環境では 環境変数(JWT_SECRET などで注入)または HashiCorp Vault などのシークレット管理ツールを使用することを強く推奨します。
トークンの有効期限はどのくらいに設定すべきですか?
一般的には アクセストークンは15分〜1時間 が推奨されます。本記事のサンプルは24時間ですが、これは検証用の設定です。有効期限が長いほどトークン漏洩時のリスクが高まります。短命なアクセストークンと長命なリフレッシュトークンを組み合わせる設計が実用的です。
Spring Boot 3.x(Spring Security 6)でSecurityFilterChainの書き方が変わりましたか?
はい。Spring Security 5系では http.csrf().disable() のようなメソッドチェーン記法が使われていましたが、Spring Security 6では ラムダDSL形式(csrf(csrf -> csrf.disable()) 形式)が標準になりました。本記事のコードは Spring Boot 3.x / Spring Security 6対応の書き方 で統一しています。
JwtAuthenticationFilterでaddFilterBeforeに追加しているのにJWTが検証されない場合は何が原因ですか?
最も多い原因は @Component による二重登録です。@Component を付けるとSpring Bootがサーブレットフィルターとして自動登録し、セキュリティチェーン外で先に実行されてしまいます。本記事のように @Component を除去して SecurityConfig 内で @Bean として定義することで解消します。
ログアウト機能はどう実装しますか?JWTをサーバー側で無効化できますか?
JWT自体はステートレスなため、発行済みトークンをサーバー側から無効化するには ブラックリスト(無効化トークンの一覧をRedis等に保存) するアプローチが一般的です。ただし、これはステートレスの利点を一部失うトレードオフを伴います。有効期限を短く設定してリフレッシュトークンと組み合わせる方が多くのケースで現実的です。
次のステップ:リフレッシュトークンとセキュリティ強化
ここまでの実装で、JWT認証の基本的な仕組みは完成しました。実用的なシステムに向けた次のステップを紹介します。
リフレッシュトークン
アクセストークンの有効期限を短く設定(15〜60分)しつつ、リフレッシュトークン(数日〜数週間有効)を使ってアクセストークンを再発行する仕組みです。漏洩時のリスクを限定できますが、リフレッシュトークン自体の管理(DBへの保存・無効化)が必要になります。詳細実装は別記事で解説予定です。
トークンブラックリスト(ログアウト処理)
ログアウト時に無効化したいトークンのJTI(JWT ID)をRedisに保存し、Filter内でブラックリスト確認を行うアプローチです。ステートレスの利点を一部犠牲にしますが、即時無効化が必要なケースでは有効です。
HTTPS必須
JWTはAuthorizationヘッダーで送信されるため、平文HTTP通信では盗聴リスク があります。本番環境では必ずHTTPSを使用してください。ローカル開発以外でHTTPを使うことは避けましょう。
本番環境での秘密鍵管理
前述の通り、application.properties への秘密鍵直書きは避け、環境変数 または HashiCorp Vault などのシークレット管理ツールを使用してください。CI/CDパイプラインでの秘密鍵の扱いにも注意が必要です。
まとめ
本記事では、Spring Boot REST APIにJWT認証を組み込む手順を解説しました。
| ステップ | 実装内容 |
|---|---|
| 1 | jjwtの依存関係追加 |
| 2 | application.propertiesに秘密鍵・有効期限を外部化 |
| 3 | JwtUtil でトークン生成・検証ロジックを実装 |
| 4 | UserDetailsService でユーザーロードの土台を整備 |
| 5 | /api/auth/login でJWT発行エンドポイントを実装 |
| 6 | JwtAuthenticationFilter でリクエストごとのトークン検証を実装 |
| 7 | SecurityFilterChain でSTATELESS設定・Filter挿入・認可ルールを設定 |
| 8 | curl / Postmanでエンドツーエンドテストを実施 |
Basic認証・フォーム認証からJWT認証へのステップアップは、REST APIの設計として自然な進化です。ぜひ自分のプロジェクトに組み込んでみてください。