Spring Security 6: JWT認証の完全ガイド
Spring Security 6でJWT認証を実装するための実践ガイド。設定、トークン生成、検証、セキュリティのベストプラクティスを解説します。

JWT(JSON Web Token)による認証は、現代のREST APIを保護するための標準となっています。Spring Security 6では関数型の設定アプローチが導入され、実装が簡潔になりつつセキュリティも強化されています。本ガイドでは、初期設定からエンドポイントの保護までの一連の流れを取り上げます。
このチュートリアルではSpring Boot 3.2+とSpring Security 6.2+を使用します。新しいバージョンでも、わずかな構文調整で同じ概念がそのまま使えます。
Spring SecurityにおけるJWTアーキテクチャ
JWT認証はステートレスの原則に基づきます。サーバーはセッションを保持しません。各リクエストには、ユーザーのアイデンティティを証明する署名付きトークンが含まれます。このアーキテクチャにより、インスタンス間でセッションを共有せずに水平スケールが可能になります。
JWT認証のフローは複数のステップに分けられます。ユーザーは資格情報で認証し、署名付きJWTトークンを受け取り、その後のすべてのリクエストにこのトークンを付与します。サーバーは署名を検証し、トークンからユーザー情報を取り出します。
// Conceptual representation of the authentication flow
public class JwtAuthenticationFlow {
// 1. Initial authentication: POST /api/auth/login
// → Verify credentials against database
// → Generate signed JWT token
// → Return token to client
// 2. Authenticated requests: GET /api/protected
// → Header: Authorization: Bearer <token>
// → Extract and validate token
// → Create SecurityContext
// → Access protected resource
}このアプローチはスティッキーセッションの問題を取り除き、分散環境へのデプロイを容易にします。
Maven依存関係の設定
プロジェクトはSpring Securityの依存関係とJWTライブラリを必要とします。JJWT(Java JWT)は、トークン操作のための流暢でメンテナンスの行き届いたAPIを提供します。
<!-- pom.xml -->
<dependencies>
<!-- Spring Security for authentication management -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Web for REST endpoints -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JJWT: token generation and validation -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<!-- Spring Data JPA for user persistence -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>JJWTを3つのモジュール(api、impl、jackson)に分けるのは、カプセル化の原則に従っています。コンパイル時にはAPIだけが見え、実装は実行時の詳細にとどまります。
JWTの生成と検証を行うサービス
JWTサービスは、生成・クレーム抽出・検証といったトークンに関するすべての操作を一元化します。安全な秘密鍵が各トークンに署名し、その完全性を保証します。
@Service
public class JwtService {
// Secret key injected from application.yml
@Value("${app.jwt.secret}")
private String secretKey;
// Token validity duration (24 hours by default)
@Value("${app.jwt.expiration:86400000}")
private long jwtExpiration;
// Generates a JWT token for an authenticated user
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
// Generates a token with custom claims
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder()
.claims(extraClaims) // Additional claims (roles, permissions)
.subject(userDetails.getUsername()) // Principal identifier
.issuedAt(new Date()) // Creation date
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSigningKey(), Jwts.SIG.HS256) // HMAC-SHA256 signature
.compact();
}
// Extracts the username (subject) from the token
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// Extracts a specific claim via an extraction function
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// Validates the token: correct signature and not expired
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
// Checks if the token has expired
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// Extracts the expiration date
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// Parses the token and extracts all claims
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey()) // Verifies the signature
.build()
.parseSignedClaims(token) // Parses the signed token
.getPayload(); // Returns the claims
}
// Generates the signing key from the Base64-encoded secret
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}秘密鍵は十分に長く(HS256では最低256ビット)、安全に保管しなければなりません。ソースコードに含めることは決して避けてください。
JWT鍵には環境変数またはシークレットマネージャーを使用してください。鍵が漏洩すると、任意のユーザー向けの有効なトークンを偽造できてしまいます。
Userエンティティの構成
ユーザーエンティティはSpring SecurityのUserDetailsを実装し、認証システムと直接統合できるようにします。
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String firstName;
@Column(nullable = false)
private String lastName;
// Role stored as enum for type safety
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
// UserDetails implementation: returns user authorities
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
}
// Username corresponds to email in this implementation
@Override
public String getUsername() {
return email;
}
// Account always active (adapt as needed)
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// Getters and setters omitted for brevity
}public enum Role {
USER, // Standard user
ADMIN // Administrator with extended privileges
}Role列挙型は取り得る値を限定し、SpEL式での権限チェックを簡潔にします。
JWT認証フィルター
JWTフィルターはすべてのリクエストを横取りし、トークンを抽出して検証します。Spring Securityのフィルターチェーンに、標準の認証フィルターより前で組み込まれます。
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
// Retrieve the Authorization header
final String authHeader = request.getHeader("Authorization");
// Check for Bearer prefix
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// Extract the token (without the "Bearer " prefix)
final String jwt = authHeader.substring(7);
try {
// Extract username from token
final String userEmail = jwtService.extractUsername(jwt);
// Check that user is not already authenticated
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// Load user details from database
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
// Validate token (signature + expiration + user match)
if (jwtService.isTokenValid(jwt, userDetails)) {
// Create authentication object
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
// Add request details
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// Set authentication in security context
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (ExpiredJwtException e) {
// Token expired: user must re-authenticate
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token expired");
return;
} catch (JwtException e) {
// Invalid token: incorrect signature or malformed format
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid token");
return;
}
// Continue the filter chain
filterChain.doFilter(request, response);
}
}OncePerRequestFilterは、forwardやincludeが発生してもフィルターをリクエストごとに一度だけ実行することを保証します。
Spring Bootの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
Spring Security 6の構成
Spring Security 6は、ラムダを用いた関数型のアプローチでセキュリティチェーンを構成します。この構成ではアクセスルールを定義し、JWTフィルターを組み込みます。
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Enables @PreAuthorize and @PostAuthorize
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Disable CSRF since API is stateless (no session cookies)
.csrf(csrf -> csrf.disable())
// Configure authorization rules
.authorizeHttpRequests(auth -> auth
// Public endpoints: authentication and registration
.requestMatchers("/api/auth/**").permitAll()
// API documentation accessible without authentication
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// Admin endpoints reserved for administrators
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// All other requests require authentication
.anyRequest().authenticated()
)
// Stateless mode: no server-side HTTP session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// Custom authentication provider
.authenticationProvider(authenticationProvider)
// Insert JWT filter before standard authentication filter
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository userRepository;
// User loading service for Spring Security
@Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
// Authentication provider with password encoder
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
// Authentication manager exposed as bean
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
// BCrypt encoder for secure password hashing
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}ステートレスなAPIではCSRFを無効化しても安全です。認証はブラウザが自動送信するクッキーではなく、明示的に渡されるヘッダーに依存しているからです。
認証コントローラー
コントローラーは登録とログインのエンドポイントを公開します。これらのエンドポイントは公開されており、認証成功後にJWTトークンを返します。
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authService;
// POST /api/auth/register - Register a new user
@PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(
@Valid @RequestBody RegisterRequest request
) {
return ResponseEntity.ok(authService.register(request));
}
// POST /api/auth/login - Login existing user
@PostMapping("/login")
public ResponseEntity<AuthenticationResponse> login(
@Valid @RequestBody AuthenticationRequest request
) {
return ResponseEntity.ok(authService.authenticate(request));
}
// POST /api/auth/refresh - Token refresh (optional)
@PostMapping("/refresh")
public ResponseEntity<AuthenticationResponse> refresh(
@RequestHeader("Authorization") String authHeader
) {
return ResponseEntity.ok(authService.refreshToken(authHeader));
}
}public record AuthenticationRequest(
@NotBlank(message = "Email required")
@Email(message = "Invalid email format")
String email,
@NotBlank(message = "Password required")
String password
) {}public record RegisterRequest(
@NotBlank(message = "First name required")
String firstName,
@NotBlank(message = "Last name required")
String lastName,
@NotBlank(message = "Email required")
@Email(message = "Invalid email format")
String email,
@NotBlank(message = "Password required")
@Size(min = 8, message = "Password must contain at least 8 characters")
String password
) {}public record AuthenticationResponse(
String token,
String type,
long expiresIn
) {
public AuthenticationResponse(String token, long expiresIn) {
this(token, "Bearer", expiresIn);
}
}JavaのレコードはDTOの定義を簡潔にしながら、不変性を保証します。
認証サービス
このサービスは登録と認証のロジックを取りまとめ、トークンの生成はJwtServiceに委譲します。
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;
@Value("${app.jwt.expiration:86400000}")
private long jwtExpiration;
// Register a new user
@Transactional
public AuthenticationResponse register(RegisterRequest request) {
// Check that email is not already in use
if (userRepository.existsByEmail(request.email())) {
throw new EmailAlreadyExistsException("Email already registered");
}
// Create user entity with hashed password
User user = new User();
user.setFirstName(request.firstName());
user.setLastName(request.lastName());
user.setEmail(request.email());
user.setPassword(passwordEncoder.encode(request.password()));
user.setRole(Role.USER);
// Persist the user
userRepository.save(user);
// Generate and return JWT token
String jwtToken = jwtService.generateToken(user);
return new AuthenticationResponse(jwtToken, jwtExpiration);
}
// Authenticate an existing user
public AuthenticationResponse authenticate(AuthenticationRequest request) {
// Delegate verification to AuthenticationManager
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.email(),
request.password()
)
);
// Load user (authentication succeeded)
User user = userRepository.findByEmail(request.email())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// Generate token with additional claims
Map<String, Object> claims = new HashMap<>();
claims.put("role", user.getRole().name());
claims.put("userId", user.getId());
String jwtToken = jwtService.generateToken(claims, user);
return new AuthenticationResponse(jwtToken, jwtExpiration);
}
// Refresh token (extend session)
public AuthenticationResponse refreshToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new InvalidTokenException("Invalid token");
}
String oldToken = authHeader.substring(7);
String userEmail = jwtService.extractUsername(oldToken);
User user = userRepository.findByEmail(userEmail)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// Generate new token
String newToken = jwtService.generateToken(user);
return new AuthenticationResponse(newToken, jwtExpiration);
}
}AuthenticationManagerは資格情報の検証を集約し、認証戦略の差し替えを容易にします。
セキュリティを高めるには、より長い有効期限を持ち、データベースに保存して取り消し可能なリフレッシュトークンの仕組みを導入してください。
アノテーションによるエンドポイント保護
メソッドレベルのセキュリティを使うと、ビジネスコードの中で細やかな権限制御ができます。
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// Accessible to all authenticated users
@GetMapping("/me")
public ResponseEntity<UserDTO> getCurrentUser(
@AuthenticationPrincipal User currentUser
) {
return ResponseEntity.ok(UserDTO.from(currentUser));
}
// Only the concerned user or an admin can modify the profile
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request
) {
return ResponseEntity.ok(userService.updateUser(id, request));
}
// Reserved for administrators
@PreAuthorize("hasRole('ADMIN')")
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
return ResponseEntity.ok(userService.findAll());
}
// Deletion with post-execution verification
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')") // All methods require ADMIN
public class AdminController {
private final AdminService adminService;
@GetMapping("/dashboard")
public ResponseEntity<DashboardDTO> getDashboard() {
return ResponseEntity.ok(adminService.getDashboardStats());
}
@PostMapping("/users/{id}/role")
public ResponseEntity<UserDTO> changeUserRole(
@PathVariable Long id,
@RequestParam Role newRole
) {
return ResponseEntity.ok(adminService.changeUserRole(id, newRole));
}
}@AuthenticationPrincipalアノテーションは認証済みユーザーを直接注入し、SecurityContextに手動でアクセスする必要をなくします。
認証エラーのハンドリング
エラーを集中的に扱うことで、認証が失敗したときに一貫した分かりやすい応答を返せます。
@RestControllerAdvice
public class SecurityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(SecurityExceptionHandler.class);
// Authentication error (incorrect credentials)
@ExceptionHandler(BadCredentialsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleBadCredentials(BadCredentialsException ex) {
return new ErrorResponse(
"INVALID_CREDENTIALS",
"Invalid email or password",
null
);
}
// Access denied (authenticated but not authorized)
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorResponse handleAccessDenied(AccessDeniedException ex) {
return new ErrorResponse(
"ACCESS_DENIED",
"Access to this resource is not authorized",
null
);
}
// Expired JWT token
@ExceptionHandler(ExpiredJwtException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleExpiredToken(ExpiredJwtException ex) {
return new ErrorResponse(
"TOKEN_EXPIRED",
"Session expired, please log in again",
null
);
}
// Invalid JWT token
@ExceptionHandler(JwtException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleInvalidToken(JwtException ex) {
log.warn("Invalid JWT token: {}", ex.getMessage());
return new ErrorResponse(
"INVALID_TOKEN",
"Invalid authentication token",
null
);
}
// Email already in use during registration
@ExceptionHandler(EmailAlreadyExistsException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ErrorResponse handleEmailExists(EmailAlreadyExistsException ex) {
return new ErrorResponse(
"EMAIL_EXISTS",
ex.getMessage(),
null
);
}
}public record ErrorResponse(
String code,
String message,
Map<String, String> details
) {}標準化されたエラーコードはクライアント側の処理とデバッグを容易にします。
application.ymlの設定
外部化された設定により、環境ごとにJWTのパラメーターを調整できます。
# application.yml
app:
jwt:
# Base64 secret key (256 bits minimum for HS256)
# Generate with: openssl rand -base64 32
secret: ${JWT_SECRET:yourSuperSecretKeyOf256BitsMinimum}
# Validity duration in milliseconds (24 hours)
expiration: 86400000
spring:
datasource:
url: jdbc:postgresql://localhost:5432/springjwt
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
jpa:
hibernate:
ddl-auto: validate
show-sql: false# application-dev.yml
app:
jwt:
# Short expiration for development (1 hour)
expiration: 3600000
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: update
logging:
level:
org.springframework.security: DEBUG本番環境では、秘密鍵は環境変数またはVaultのようなシークレット管理サービスから取得すべきです。
統合テスト
テストでは認証システム全体の振る舞いを検証します。登録から保護されたリソースへのアクセスまでを通しで確認します。
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = Replace.ANY)
class AuthenticationIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void shouldRegisterAndAuthenticateUser() {
// Registration
RegisterRequest registerRequest = new RegisterRequest(
"John", "Doe", "john@example.com", "password123"
);
ResponseEntity<AuthenticationResponse> registerResponse = restTemplate
.postForEntity("/api/auth/register", registerRequest, AuthenticationResponse.class);
assertThat(registerResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(registerResponse.getBody().token()).isNotBlank();
// Login with same credentials
AuthenticationRequest loginRequest = new AuthenticationRequest(
"john@example.com", "password123"
);
ResponseEntity<AuthenticationResponse> loginResponse = restTemplate
.postForEntity("/api/auth/login", loginRequest, AuthenticationResponse.class);
assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(loginResponse.getBody().token()).isNotBlank();
}
@Test
void shouldRejectInvalidCredentials() {
AuthenticationRequest request = new AuthenticationRequest(
"unknown@example.com", "wrongpassword"
);
ResponseEntity<ErrorResponse> response = restTemplate
.postForEntity("/api/auth/login", request, ErrorResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void shouldAccessProtectedResourceWithValidToken() {
// Register to get a token
RegisterRequest registerRequest = new RegisterRequest(
"Jane", "Doe", "jane@example.com", "password123"
);
AuthenticationResponse authResponse = restTemplate
.postForObject("/api/auth/register", registerRequest, AuthenticationResponse.class);
// Access protected resource with token
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(authResponse.token());
ResponseEntity<UserDTO> response = restTemplate.exchange(
"/api/users/me",
HttpMethod.GET,
new HttpEntity<>(headers),
UserDTO.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().email()).isEqualTo("jane@example.com");
}
@Test
void shouldRejectAccessWithoutToken() {
ResponseEntity<Void> response = restTemplate
.getForEntity("/api/users/me", Void.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
}これらのテストは、登録・ログイン・許可されたアクセス・拒否されたアクセスといった主要なシナリオをカバーします。
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
まとめ
Spring Security 6によるJWT認証の実装は、明確なパターンに従います。JWTサービスがトークンの生成と検証を担当し、フィルターがリクエストを横取りしてセキュリティコンテキストを確立し、構成がアクセスルールを定めます。
デプロイ用チェックリスト:
- ✅ 環境変数に保存された256ビット以上の秘密鍵
- ✅ 本番ではトークンを通信路上で保護するためHTTPSを必須
- ✅ コンテキストに合わせたトークン有効期間(15分〜24時間)
- ✅ 再認証なしで長時間のセッションを支えるリフレッシュトークン
- ✅ ビジネスコードによる標準化されたエラーハンドリング
- ✅ 認証失敗の試行のロギング
- ✅ 認証シナリオを網羅する統合テスト
- ✅ 認証エンドポイントへのレートリミット
このアーキテクチャはREST APIを保護するための堅牢な土台になり、多要素認証やOAuth2連携など、より高度な要件にも拡張しやすいまま保たれます。
タグ
共有
関連記事

Spring Security 6: OAuth2リソースサーバーの設定
Spring Security 6でOAuth2リソースサーバーを設定する実践ガイドです。JWT検証、issuer設定、スコープ管理、Keycloak連携について説明します。

Spring Modulith: モジュラーモノリスアーキテクチャ解説
Spring Modulith で Java のモジュラーモノリスを構築する方法を学びます。アーキテクチャ、モジュール、非同期イベント、Spring Boot 3 のコード例によるテスト。

Spring Batch 5 面接対策: パーティショニング・チャンク・フォールトトレランス
Spring Batch 5 の面接を制覇する15の必須質問。パーティショニング、チャンク処理、フォールトトレランスを Java 21 のコード例と共に解説します。