はじめに
この記事では、JavaのSpring Securityを使ってCookie認証とJWT認証を組み合わせた方法を初心者向けに解説します。Cookie認証とJWT認証の基本から具体的なコード例、注意点までをカバーし、簡単に実践できる内容を目指します。
Cookie認証とJWT認証とは?
Cookie認証
Cookie認証は、ユーザーがログインするときに発行されるセッション情報をブラウザのCookieに保存し、次回のリクエストでサーバーに送信する仕組みです。
メリット
- ユーザー体験の向上:一度ログインすると、再度ログインする必要がない。
- 標準的な技術:多くのブラウザがCookieをサポート。
デメリット
- サーバー側でセッションを管理するため、スケーラビリティが低下する可能性がある。
- セッション固定攻撃やCookieの盗難に対する脆弱性。
JWT認証
JWT(JSON Web Token)は、JSON形式のトークンを利用した認証方式です。トークンは署名付きで、改ざん防止がされています。
メリット
- サーバーにセッション情報を保存する必要がない(ステートレス)。
- 複数のサービス間で認証を共有しやすい。
- トークンの内容をデコードして情報を取得できる(例:ユーザーのロール)。
デメリット
- トークンの管理はクライアント側に依存するため、適切な対策が必要。
- トークンが有効期限内であれば取り消しが難しい。
- トークンのサイズが大きく、ネットワーク帯域への影響がある。
Cookie認証とJWT認証の組み合わせのメリット
- ユーザー体験を損なわず、ステートレス認証の利点を活かせる。
- セキュリティ向上:JWTの署名検証とCookieのセキュリティ設定を併用。
- 柔軟性:シングルサインオン(SSO)や分散システムで利用可能。
注意点
- CookieとJWTの両方のセキュリティを考慮した設定が必要(例:Secure属性、HttpOnly属性)。
Spring SecurityでCookie認証とJWT認証を導入するための手順
以下は、Cookie認証とJWT認証を組み合わせた実装のステップです。
- 依存関係の追加
SecurityConfig
クラスの設定- Cookieフィルターの実装
- JWTトークンのユーティリティクラス作成
- ログインコントローラーの作成
依存関係の追加
プロジェクトのbuild.gradle
(Gradleの場合)に必要な依存関係を追加します。
Plaintext
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
Security設定クラスの作成
目的
SecurityConfig
クラスは、Spring Securityをカスタマイズし、以下のようなセキュリティ要件を満たすために設計されています:
- Cookieベースの認証を追加
- フロントエンドとの統合を容易にするCORS設定
- CSRFの無効化(開発時の利便性向上、ただし本番環境では有効化推奨)
- ルートごとのアクセス制御を設定
PasswordEncoderの設定
Java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
- 目的: ユーザーのパスワードをハッシュ化するエンコーダーを定義します。
BCryptPasswordEncoder
: パスワードを安全に保存するための標準的なハッシュアルゴリズム(BCrypt)を使用します。
SecurityFilterChainの定義
Java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORSを有効化
.csrf(csrf -> csrf.disable()) // CSRFを無効化(本番環境では有効化すべきすべき)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 認証APIを許可
.anyRequest().authenticated() // 他は認証が必要
)
.addFilterBefore(new CookieAuthenticationFilter(jwtUtils), UsernamePasswordAuthenticationFilter.class) // Cookie認証を追加
.formLogin(form -> form.disable()); // フォーム認証を無効化
return http.build();
}
- CORS設定
- CORS: クロスオリジンリクエストを制御します(異なるオリジン間の通信を許可する設定)。
- 設定は
corsConfigurationSource()
メソッドで定義しています。
Java
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
- CSRF無効化
- CSRF (Cross-Site Request Forgery): 本番環境では有効化する必要がありますが、ここでは開発用に無効化しています。
Java
.csrf(csrf -> csrf.disable())
- リクエストのアクセス制御
/api/auth/**
へのリクエストは認証不要。- それ以外のエンドポイントは認証が必要です。
Java
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 認証APIを許可
.anyRequest().authenticated() // 他は認証が必要
)
- カスタムCookie認証のフィルター追加
- カスタムの
CookieAuthenticationFilter
を、UsernamePasswordAuthenticationFilter
の前に追加しています。 - 目的: クッキーを利用してリクエストの認証を行います。
jwtUtils
: JWT(JSON Web Token)の処理を行うユーティリティクラス。
- カスタムの
Java
.addFilterBefore(new CookieAuthenticationFilter(jwtUtils), UsernamePasswordAuthenticationFilter.class)
- フォーム認証を無効化
- Spring Securityのデフォルトのフォーム認証を無効化しています。
- 認証はCookieやトークンベースで行います。
Java
.formLogin(form -> form.disable())
CORS設定
Java
private UrlBasedCorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:5173")); // フロントエンドのURL(react)
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return source;
}
- 目的: フロントエンド(Reactアプリ)との通信を許可するためのCORS設定。
- 設定内容:
setAllowedOrigins
: 特定のオリジン(例:http://localhost:5173
)からのリクエストを許可。setAllowedMethods
: 許可するHTTPメソッド(GET
,POST
,PUT
,DELETE
,OPTIONS
)。setAllowedHeaders
: すべてのヘッダーを許可。setAllowCredentials
: Cookieを使った認証を許可。
完成コード
Java
package com.example.security.config;
import com.example.security.utils.JwtUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
public class SecurityConfig {
private final JwtUtils jwtUtils;
public SecurityConfig(
JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORSを有効化
.csrf(csrf -> csrf.disable()) // CSRFを無効化(本番環境では有効化すべきすべき)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 認証APIを許可
.anyRequest().authenticated() // 他は認証が必要
)
.addFilterBefore(new CookieAuthenticationFilter(jwtUtils), UsernamePasswordAuthenticationFilter.class) // Cookie認証を追加
.formLogin(form -> form.disable()); // フォーム認証を無効化
return http.build();
}
private UrlBasedCorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:5173")); // フロントエンドのURL(react)
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return source;
}
}
Cookieフィルターの実装
このクラスは OncePerRequestFilter
を継承しており、リクエストごとに一度だけ実行されるカスタムフィルタです。主な目的は以下の通り:
- リクエストに含まれるCookieをチェックする。
- 特定のCookie(
JWT_TOKEN
)からJWTトークンを取得し、その有効性を検証する。 - トークンが有効であれば、認証情報を設定し、Spring Securityのコンテキストに追加する。
コンストラクタ
Java
public CookieAuthenticationFilter(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
- 目的:
JwtUtils
クラスのインスタンスを注入します。 JwtUtils
の役割:- JWTの生成、検証、解析を行うユーティリティクラスです。
Cookieの取得とログ出力
Java
Cookie[] cookies = request.getCookies();
if (cookies != null) {
System.out.println("Cookies found: " + cookies.length);
Arrays.stream(cookies).forEach(cookie ->
System.out.println("Cookie name: " + cookie.getName() + ", value: " + cookie.getValue())
);
}
- 目的: リクエストに含まれるCookieを取得し、ログに出力します。
- 詳細:
request.getCookies()
でCookie配列を取得。null
チェックを行い、存在するCookieの名前と値をログ出力しています(デバッグ用)。
特定のCookie(JWT_TOKEN)の検索
Java
Arrays.stream(cookies)
.filter(cookie -> "JWT_TOKEN".equals(cookie.getName()))
.findFirst()
.ifPresentOrElse(
cookie -> { ... },
() -> System.out.println("JWT Token not found in cookies.")
);
- 目的: 取得したCookieの中から
JWT_TOKEN
という名前のCookieを検索します。 - 詳細:
Arrays.stream(cookies)
でCookie配列をストリーム処理。filter
で名前がJWT_TOKEN
のCookieをフィルタリング。findFirst()
で最初に見つかったものを取得。- 見つかった場合は
ifPresentOrElse
で処理を分岐。
JWTトークンの検証と認証設定
Java
String token = cookie.getValue();
if (jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsernameFromToken(token);
var authentication = jwtUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
- 目的: JWTトークンの有効性を検証し、認証情報を設定します。
- 詳細:
cookie.getValue()
でトークンの値を取得。jwtUtils.validateToken(token)
でトークンを検証。- トークンの署名や有効期限を確認。
jwtUtils.getUsernameFromToken(token)
でトークンに含まれるユーザー名を抽出。jwtUtils.getAuthentication(token)
でSpring Securityの認証情報を取得。SecurityContextHolder.getContext().setAuthentication(authentication)
で認証情報をSpring Securityに設定。
- 結果: ユーザーが認証済みとして扱われます。
次のフィルタの実行
Java
filterChain.doFilter(request, response);
- 目的: フィルタチェーンの次のフィルタを呼び出します。
- 詳細:
- フィルタは順番に実行されるため、次の処理に進む必要があります。
- この行がないとリクエストが止まってしまいます。
完成コード
Java
package com.example.security.config;
import com.example.security.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
public class CookieAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
public CookieAuthenticationFilter(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Cookieが存在するか確認
Cookie[] cookies = request.getCookies();
if (cookies != null) {
System.out.println("Cookies found: " + cookies.length);
Arrays.stream(cookies).forEach(cookie ->
System.out.println("Cookie name: " + cookie.getName() + ", value: " + cookie.getValue())
);
// JWTトークンが含まれているか確認
Arrays.stream(cookies)
.filter(cookie -> "JWT_TOKEN".equals(cookie.getName()))
.findFirst()
.ifPresentOrElse(
cookie -> {
String token = cookie.getValue();
System.out.println("JWT Token found: " + token);
// トークンの検証
if (jwtUtils.validateToken(token)) {
System.out.println("JWT validation successful.");
String username = jwtUtils.getUsernameFromToken(token);
System.out.println("Extracted username: " + username);
// 認証情報の設定
var authentication = jwtUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("Authentication set for user: " + username);
} else {
System.out.println("JWT validation failed.");
}
},
() -> System.out.println("JWT Token not found in cookies.")
);
} else {
System.out.println("No cookies found in the request.");
}
// 次のフィルタを呼び出す
filterChain.doFilter(request, response);
// デバッグログ:レスポンスステータスを出力
System.out.println("Response Status: " + response.getStatus());
}
}
JWTトークンのユーティリティクラス作成
JwtUtils
クラスは、JWTを使用した認証の実装で以下の機能を提供します:
- トークンの生成
- トークンの解析(クレーム情報の取得)
- トークンの有効性検証
- Spring Security用の認証情報の作成
定数とフィールド
Java
private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION_TIME = 86400000;
SECRET_KEY
:- JWTを署名するための秘密鍵です。
SignatureAlgorithm.HS256
を使用したHMACアルゴリズムでキーを生成。
EXPIRATION_TIME
:- トークンの有効期限をミリ秒単位で定義(24時間)。
トークンの生成
Java
public String generateTokenWithOrganization(String username, Long organizationId) {
return Jwts.builder()
.setSubject(username)
.claim("organizationId", organizationId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SECRET_KEY)
.compact();
}
- 目的: ユーザー名と追加の情報(例:組織ID)を含むJWTトークンを生成。
- 詳細:
setSubject
: トークンの主体(通常、ユーザー名)を設定。claim
: トークンにカスタムクレーム(ここではorganizationId
)を追加。setIssuedAt
: トークンの発行時刻を設定。setExpiration
: 有効期限を設定。signWith
: 秘密鍵でトークンを署名。compact
: トークンを文字列として生成。
トークンからユーザー名を取得
Java
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
- 目的: トークンからユーザー名を抽出。
- 詳細:
Claims::getSubject
でトークンの主体(sub
フィールド)を取得。- 内部的には
getClaimFromToken
メソッドを利用。
トークンから組織IDを取得
Java
public Long getOrganizationIdFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("organizationId", Long.class));
}
- 目的: トークンに含まれるカスタムクレーム(
organizationId
)を取得。 - 詳細:
claims.get("organizationId", Long.class)
でクレームを取得。
トークンを検証
Java
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
System.err.println("Invalid JWT token: " + e.getMessage());
return false;
}
}
- 目的: トークンが有効であるかを確認。
- 詳細:
setSigningKey
: トークンの署名を検証するための秘密鍵を設定。parseClaimsJws
: トークンを解析し、署名と有効期限をチェック。- 例外処理:
- 無効なトークンや期限切れのトークンの場合は
false
を返却。 - エラーメッセージをログ出力。
- 無効なトークンや期限切れのトークンの場合は
Spring Securityの認証情報を生成
Java
public Authentication getAuthentication(String token) {
String username = getUsernameFromToken(token);
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
User principal = new User(username, "", Collections.singletonList(authority));
return new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
}
- 目的: トークンから認証情報を作成し、Spring Securityで使用可能にする。
- 詳細:
- トークンからユーザー名を取得。
- 権限情報(
ROLE_USER
)を作成。 - ユーザー名と権限を持つ
User
オブジェクトを生成。 UsernamePasswordAuthenticationToken
で認証情報を作成。
トークンから指定したクレームを取得
Java
private <T> T getClaimFromToken(String token, java.util.function.Function<Claims, T> claimsResolver) {
final Claims claims = Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
return claimsResolver.apply(claims);
}
- 目的: トークンから指定されたクレーム情報を抽出。
- 詳細:
- トークンを解析し、
Claims
オブジェクトを取得。 claimsResolver
(ラムダ式またはメソッド参照)を使用して特定のクレームを抽出。
- トークンを解析し、
完成コード
Java
package com.example.security.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Collections;
import java.util.Date;
@Component
public class JwtUtils {
// 秘密鍵
private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// トークンの有効期間(24時間)
private static final long EXPIRATION_TIME = 86400000;
/**
* JWTトークンを生成 (組織IDを含む)
*
* @param username ユーザー名
* @param organizationId 組織ID
* @return トークン
*/
public String generateTokenWithOrganization(String username, Long organizationId) {
return Jwts.builder()
.setSubject(username)
.claim("organizationId", organizationId) // 組織IDを追加
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SECRET_KEY)
.compact();
}
/**
* トークンからユーザー名を取得
*
* @param token JWTトークン
* @return ユーザー名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* トークンから組織IDを取得
*
* @param token JWTトークン
* @return 組織ID
*/
public Long getOrganizationIdFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("organizationId", Long.class));
}
/**
* トークンを検証
*
* @param token JWTトークン
* @return 検証結果
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
System.err.println("Invalid JWT token: " + e.getMessage());
return false;
}
}
/**
* 認証情報を生成
*
* @param token JWTトークン
* @return 認証情報
*/
public Authentication getAuthentication(String token) {
String username = getUsernameFromToken(token);
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
User principal = new User(username, "", Collections.singletonList(authority));
return new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
}
/**
* トークンから指定したクレームを取得
*
* @param token JWTトークン
* @param claimsResolver クレーム解析ロジック
* @return クレームの値
*/
private <T> T getClaimFromToken(String token, java.util.function.Function<Claims, T> claimsResolver) {
final Claims claims = Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
return claimsResolver.apply(claims);
}
}
ログインコントローラーの作成
AuthController
は、以下の機能を実現します:
- ユーザーのログイン認証。
- ユーザーが指定した組織に関連付けられているかの検証。
- JWTトークンの生成とCookieへの設定。
- 成功または失敗のレスポンスを返却。
フィールドの依存性注入
Java
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtils jwtUtils;
- 目的:
- 必要なリポジトリやユーティリティを
@Autowired
で注入しています。
- 必要なリポジトリやユーティリティを
- 役割:
UserRepository
: ユーザー情報をデータベースから取得するため。PasswordEncoder
: パスワードの照合。JwtUtils
: JWTトークンの生成。
ログインエンドポイント
Java
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request, HttpServletResponse response) {}
- 目的:
/api/auth/login
エンドポイントにPOSTリクエストを受け取る。- リクエスト本文には、ユーザー名、パスワード、組織名が含まれる。
ユーザーの認証
Java
var user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new RuntimeException("ユーザーが見つかりません"));
- 目的:
- ユーザーのメールアドレスからデータベースに該当するユーザーを検索。
- 見つからない場合は例外をスロー(エラーレスポンスに繋がる)。
Java
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
return ResponseEntity.status(401).body("パスワードが正しくありません");
}
- 目的:
- フロントエンドから送信されたパスワードをデータベースに保存されているハッシュ化されたパスワードと照合。
- 照合が失敗した場合、HTTP 401(Unauthorized)を返却。
JWTトークンの生成
Java
String token = jwtUtils.generateTokenWithOrganization(user.getEmail(), organization.getId());
- 目的:
- ユーザーのメールアドレスと組織IDを含むJWTトークンを生成。
- JWTには以下の情報を含む:
- ユーザー名(
sub
フィールド) - 組織ID(カスタムクレーム)
- トークンの有効期限(24時間)
- ユーザー名(
JWTトークンのCookie設定
Java
Cookie cookie = new Cookie("JWT_TOKEN", token);
cookie.setHttpOnly(true);
cookie.setSecure(false); // HTTPSの場合はtrueに設定
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24); // 1日
response.addCookie(cookie);
- 目的:
- 生成したJWTトークンをブラウザに保存するためにHTTP Cookieとして設定。
- 詳細:
setHttpOnly(true)
:- JavaScriptからCookieにアクセスできないように設定(セキュリティ強化)。
setSecure(false)
:- HTTPS通信時のみCookieを送信する設定。
- 開発環境では
false
、本番環境ではtrue
に変更する必要あり。
setPath("/")
:- サーバー全体でCookieを利用可能。
setMaxAge(60 * 60 * 24)
:- Cookieの有効期限を1日に設定。
成功レスポンスの返却
Java
return ResponseEntity.ok(Map.of("message", "ログイン成功!"));
- 目的:
- ユーザーが正常に認証された場合、成功メッセージを返却。
- レスポンス形式:
- HTTPステータス: 200 OK
- ボディ:
{ "message": "ログイン成功!" }
完成コード
Java
package com.example.security.controller;
import com.example.security.dto.LoginRequest;
import com.example.security.entity.User;
import com.example.security.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request, HttpServletResponse response) {
var user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new RuntimeException("ユーザーが見つかりません"));
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
return ResponseEntity.status(401).body("パスワードが正しくありません");
}
// JWT生成
String token = jwtUtils.generateTokenWithOrganization(user.getEmail(), organization.getId());
// CookieにJWTをセット
Cookie cookie = new Cookie("JWT_TOKEN", token);
cookie.setHttpOnly(true);
cookie.setSecure(false); // HTTPSの場合はtrueに設定
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24); // 1日
response.addCookie(cookie);
return ResponseEntity.ok(Map.of("message", "ログイン成功!"));
}
}
認証の流れ
ユーザーのログイン
- ユーザーがログイン情報(例:ユーザー名とパスワード)をフロントエンド(Reactアプリなど)から送信します。
- リクエスト先:
/api/auth/login
(想定される認証APIエンドポイント)
サーバー側の処理
- 認証情報の検証:
- サーバーが送信されたユーザー名とパスワードを確認。
- データベースなどで対応するユーザーを検索し、パスワードを照合(
BCryptPasswordEncoder
でハッシュ化)。
- JWTトークンの生成:
- 認証が成功した場合、
JwtUtils.generateTokenWithOrganization()
を使用してJWTを生成。 - JWTには以下の情報が含まれる:
sub
: ユーザー名organizationId
: 組織ID(カスタムクレーム)iat
: 発行日時exp
: 有効期限
- 認証が成功した場合、
- Cookieへの保存:
- サーバーは生成したJWTをHTTPレスポンスのCookieに格納。
- Cookieには以下のセキュリティ設定を適用(推奨):
Secure
: HTTPS通信時のみ送信。HttpOnly
: JavaScriptからアクセス不可。
結果
- ユーザーはログイン成功後、ブラウザにJWTトークンが含まれたCookieが保存されます。
リクエスト時の認証
- 認証が必要なAPIエンドポイント(例:
/api/protected/resource
)にリクエストを送信。
サーバー側の処理
- Cookieの確認:
- リクエスト内のCookieを取得(
HttpServletRequest.getCookies()
)。 - Cookie配列から
JWT_TOKEN
という名前のCookieを検索。
- リクエスト内のCookieを取得(
- JWTトークンの検証:
- 見つかったJWTトークンを
JwtUtils.validateToken()
で検証。- 署名の正当性(
SECRET_KEY
を使用)。 - トークンの有効期限。
- 署名の正当性(
- トークンが無効な場合は認証エラーを返す(例: HTTP 401 Unauthorized)。
- 見つかったJWTトークンを
- 認証情報の設定:
- トークンが有効な場合、以下の情報を使用してSpring Securityの認証情報を作成:
- ユーザー名(
JwtUtils.getUsernameFromToken()
) - 権限(デフォルトで
ROLE_USER
)
- ユーザー名(
SecurityContextHolder
に認証情報を設定。
- トークンが有効な場合、以下の情報を使用してSpring Securityの認証情報を作成:
- リクエスト処理の継続:
- 認証が成功した場合、リクエストがフィルタチェーンを通過し、保護されたリソースにアクセス可能。
注意点やベストプラクティス
- HTTPSを使用する
- Cookie認証とJWT認証は盗聴のリスクがあるため、HTTPSを有効にしてください。
- Secure属性とHttpOnly属性
- CookieにはSecure属性とHttpOnly属性を設定してください。
- トークンの有効期限管理
- JWTの有効期限を適切に設定し、期限切れトークンの再利用を防ぎます。
- トークンの署名キー管理
- 安全な方法で署名キーを管理します(環境変数や秘密管理ツールの利用)。
まとめ
フローの概要
- ユーザーはログインリクエストを送信し、サーバーがユーザー情報、組織情報、パスワードを検証します。
- ユーザーが正当であり、組織に関連付けられている場合、JWTトークンを生成してCookieに設定。
- 認証が成功すると、フロントエンドは保護されたリソースにアクセス可能。
コードの特徴
- ユーザー認証: メールアドレスとハッシュ化されたパスワードで認証。
- 組織検証: ユーザーが指定された組織に所属しているか確認。
- JWTトークン: ユーザーと組織情報を含むトークンを生成し、ステートレスな認証を実現。
- Cookie設定: セキュリティ強化のため
HttpOnly
やSecure
属性を利用。
実装のメリット
- ステートレスな認証:
- サーバー側でセッションを保持せず、JWTを利用することでスケーラビリティ向上。
- 柔軟性:
- JWTにカスタムクレーム(組織IDなど)を含めることで、必要に応じた認証情報を保持。
- セキュリティ:
- パスワードはハッシュ化して保存し、トークンはCookieに保存することでセキュリティリスクを低減。
- シンプルな統合:
- Cookieを通じてフロントエンドと容易に連携。
注意点
- HTTPS必須:
- 本番環境では
cookie.setSecure(true)
でHTTPS通信を強制。
- 本番環境では
- 例外処理:
- ユーザーや組織が見つからない場合のエラーハンドリングを適切に行う。
- トークンの有効期限:
- トークンが期限切れになった場合のリフレッシュロジックを追加する必要あり。
- Cookie管理:
- Cookieが不正アクセスされないよう、セキュリティ属性を設定(
HttpOnly
、Secure
)。
- Cookieが不正アクセスされないよう、セキュリティ属性を設定(
次のステップ
- リフレッシュトークンの実装: トークンの期限切れ時の処理を設計。
- ログアウト機能: サーバーまたはクライアント側でCookieを削除する仕組みを追加。
- アクセス制御の拡張: ユーザーのロールや権限に基づいたリソースへのアクセス制限を導入。
コメント