JavaのSpring Securityを使ってCookie認証とJWT認証を導入する方法

Java

はじめに

この記事では、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認証を組み合わせた実装のステップです。

  1. 依存関係の追加
  2. SecurityConfigクラスの設定
  3. Cookieフィルターの実装
  4. JWTトークンのユーティリティクラス作成
  5. ログインコントローラーの作成

依存関係の追加

プロジェクトの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 を継承しており、リクエストごとに一度だけ実行されるカスタムフィルタです。主な目的は以下の通り:

  1. リクエストに含まれるCookieをチェックする。
  2. 特定のCookie(JWT_TOKEN)からJWTトークンを取得し、その有効性を検証する。
  3. トークンが有効であれば、認証情報を設定し、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トークンの有効性を検証し、認証情報を設定します。
  • 詳細:
    1. cookie.getValue()でトークンの値を取得。
    2. jwtUtils.validateToken(token)でトークンを検証。
      • トークンの署名や有効期限を確認。
    3. jwtUtils.getUsernameFromToken(token)でトークンに含まれるユーザー名を抽出。
    4. jwtUtils.getAuthentication(token)でSpring Securityの認証情報を取得。
    5. 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で使用可能にする。
  • 詳細:
    1. トークンからユーザー名を取得。
    2. 権限情報(ROLE_USER)を作成。
    3. ユーザー名と権限を持つUserオブジェクトを生成。
    4. 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 は、以下の機能を実現します:

  1. ユーザーのログイン認証。
  2. ユーザーが指定した組織に関連付けられているかの検証。
  3. JWTトークンの生成とCookieへの設定。
  4. 成功または失敗のレスポンスを返却。

フィールドの依存性注入

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エンドポイント)

サーバー側の処理

  1. 認証情報の検証:
    • サーバーが送信されたユーザー名とパスワードを確認。
    • データベースなどで対応するユーザーを検索し、パスワードを照合(BCryptPasswordEncoderでハッシュ化)。
  2. JWTトークンの生成:
    • 認証が成功した場合、JwtUtils.generateTokenWithOrganization() を使用してJWTを生成。
    • JWTには以下の情報が含まれる:
      • sub: ユーザー名
      • organizationId: 組織ID(カスタムクレーム)
      • iat: 発行日時
      • exp: 有効期限
  3. Cookieへの保存:
    • サーバーは生成したJWTをHTTPレスポンスのCookieに格納。
    • Cookieには以下のセキュリティ設定を適用(推奨):
      • Secure: HTTPS通信時のみ送信。
      • HttpOnly: JavaScriptからアクセス不可。

結果

  • ユーザーはログイン成功後、ブラウザにJWTトークンが含まれたCookieが保存されます。

リクエスト時の認証

  • 認証が必要なAPIエンドポイント(例:/api/protected/resource)にリクエストを送信。

サーバー側の処理

  1. Cookieの確認:
    • リクエスト内のCookieを取得(HttpServletRequest.getCookies())。
    • Cookie配列からJWT_TOKENという名前のCookieを検索。
  2. JWTトークンの検証:
    • 見つかったJWTトークンをJwtUtils.validateToken()で検証。
      • 署名の正当性(SECRET_KEYを使用)。
      • トークンの有効期限。
    • トークンが無効な場合は認証エラーを返す(例: HTTP 401 Unauthorized)。
  3. 認証情報の設定:
    • トークンが有効な場合、以下の情報を使用してSpring Securityの認証情報を作成:
      • ユーザー名(JwtUtils.getUsernameFromToken()
      • 権限(デフォルトでROLE_USER
    • SecurityContextHolderに認証情報を設定。
  4. リクエスト処理の継続:
    • 認証が成功した場合、リクエストがフィルタチェーンを通過し、保護されたリソースにアクセス可能。

注意点やベストプラクティス

  1. HTTPSを使用する
    • Cookie認証とJWT認証は盗聴のリスクがあるため、HTTPSを有効にしてください。
  2. Secure属性とHttpOnly属性
    • CookieにはSecure属性とHttpOnly属性を設定してください。
  3. トークンの有効期限管理
    • JWTの有効期限を適切に設定し、期限切れトークンの再利用を防ぎます。
  4. トークンの署名キー管理
    • 安全な方法で署名キーを管理します(環境変数や秘密管理ツールの利用)。

まとめ

フローの概要

  • ユーザーはログインリクエストを送信し、サーバーがユーザー情報、組織情報、パスワードを検証します。
  • ユーザーが正当であり、組織に関連付けられている場合、JWTトークンを生成してCookieに設定。
  • 認証が成功すると、フロントエンドは保護されたリソースにアクセス可能。

コードの特徴

  • ユーザー認証: メールアドレスとハッシュ化されたパスワードで認証。
  • 組織検証: ユーザーが指定された組織に所属しているか確認。
  • JWTトークン: ユーザーと組織情報を含むトークンを生成し、ステートレスな認証を実現。
  • Cookie設定: セキュリティ強化のためHttpOnlySecure属性を利用。

実装のメリット

  1. ステートレスな認証:
    • サーバー側でセッションを保持せず、JWTを利用することでスケーラビリティ向上。
  2. 柔軟性:
    • JWTにカスタムクレーム(組織IDなど)を含めることで、必要に応じた認証情報を保持。
  3. セキュリティ:
    • パスワードはハッシュ化して保存し、トークンはCookieに保存することでセキュリティリスクを低減。
  4. シンプルな統合:
    • Cookieを通じてフロントエンドと容易に連携。

注意点

  • HTTPS必須:
    • 本番環境ではcookie.setSecure(true)でHTTPS通信を強制。
  • 例外処理:
    • ユーザーや組織が見つからない場合のエラーハンドリングを適切に行う。
  • トークンの有効期限:
    • トークンが期限切れになった場合のリフレッシュロジックを追加する必要あり。
  • Cookie管理:
    • Cookieが不正アクセスされないよう、セキュリティ属性を設定(HttpOnlySecure)。

次のステップ

  • リフレッシュトークンの実装: トークンの期限切れ時の処理を設計。
  • ログアウト機能: サーバーまたはクライアント側でCookieを削除する仕組みを追加。
  • アクセス制御の拡張: ユーザーのロールや権限に基づいたリソースへのアクセス制限を導入。

コメント

タイトルとURLをコピーしました