Spring Securityで簡単認証機能入門

Java

はじめに

アプリケーション開発では、「セキュリティ」と「認証」は、ユーザーのデータを守り、アクセスを管理するために重要です。Spring Frameworkには、これらを簡単に実装できるSpring Securityという強力なツールがあります。

Spring Securityの概要

Spring Securityとは?

Spring Securityは、Webアプリケーションの「認証(ログイン)」や「認可(権限管理)」を提供するフレームワークです。

  • 認証(Authentication): 「この人が本当に誰なのか?」を確認します。
    • 例: ユーザー名とパスワードでログイン。
  • 認可(Authorization): 「この人はこの操作をしてもいいのか?」を確認します。
    • 例: 管理者だけが特定のページにアクセスできるようにする。

Spring Securityでログイン機能を実装

作成は下記の流れで行なっていきます。

  • 依存関係の追加
  • エンティティの作成
  • リポジトリの作成
  • サービスの作成(認証(Authentication)の設定)
  • Spring Securityの設定
  • コントローラーおよびログインページの作成

プロジェクトに依存関係を追加

build.gradle に以下の依存関係を追加します。

Bash
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'

エンティティ: User

ユーザー情報を保存するエンティティを作成します。

src/main/java/com/example/sql/model/User.java

Java
package com.example.sql.model;

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String role; // 例: "ROLE_USER", "ROLE_ADMIN"

    public User() {}

    public User(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

    // ゲッターとセッター
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getRole() { return role; }
    public void setRole(String role) { this.role = role; }
}

リポジトリ: UserRepository

ユーザー情報をデータベースから取得するためのリポジトリを作成します。

src/main/java/com/example/sql/repository/UserRepository.java

Java
package com.example.securitydemo.repository;

import com.example.securitydemo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

ポイント

findByUsername

findByUsername は、Spring Data JPA が自動的に解釈してクエリを生成する仕組みを利用しています。

  1. findBy:
    • この部分は 検索処理を行う ことを表します。
    • find」は「データを検索する」アクションで、「By」は検索条件の指定を示します。
  2. Username:
    • By に続く部分(Username)は、エンティティのフィールド名 に対応します。
    • User エンティティの username フィールドに基づいて検索条件が作られます。

Spring Data JPA のクエリ生成の流れ

  • Spring Data JPA は、findByUsername のメソッド名を解析して以下のような SQL を自動生成します
SQL
SELECT * FROM user WHERE username = ?; (user はテーブル名、username はカラム名)
  • つまり、findBy<FieldName> という命名規則に従っている場合、Spring Data JPA が自動的に適切なクエリを生成します。

今回はusernameを使ってユーザーを特定するので追加しています。

ユーザー詳細サービスの設定

Spring Securityが使用するユーザー情報を提供するサービスを作成します。

src/main/java/com/example/sql/service/UserDetailsServiceImpl.java

Java
package com.example.sql.service;

import com.example.sql.model.User;
import com.example.sql.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

import java.util.Collections;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                Collections.singleton(new SimpleGrantedAuthority(user.getRole()))
        );
    }
}

ポイント

  1. UserDetailsServiceの実装
    • このクラスは Spring Security がユーザー情報を取得する際に使用するサービスを提供します。
    • インターフェース UserDetailsService のメソッド loadUserByUsername をオーバーライドすることで、カスタムな認証ロジックを実装します。

セキュリティ設定

Spring Securityの設定をカスタマイズします。

目的

SecurityConfig は、Spring Security を利用した認証と認可の設定を行うためのクラスです。このクラスは、アプリケーションのセキュリティポリシーを定義します。

src/main/java/com/example/sql/config/SecurityConfig.java

Java
package com.example.sql.config;

import com.example.sql.service.UserDetailsServiceImpl;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

@Configuration
public class SecurityConfig {

    private final UserDetailsServiceImpl userDetailsService;

    public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/error", "/resources/**").permitAll() // 認証不要
                .anyRequest().authenticated() // 認証が必要
            )
            .formLogin(form -> form
                .loginPage("/login") // カスタムログインページ
                .defaultSuccessUrl("/", true) // 認証成功後のリダイレクト先
                .failureUrl("/login?error=true") // 認証失敗時のリダイレクト先
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout=true") // ログアウト成功後のリダイレクト先
                .permitAll()
            );
        return http.build();
    }


    @Bean
    public HttpFirewall allowSemicolonFirewall() {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        firewall.setAllowSemicolon(true);
        return firewall;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
}

主要ポイント

SecurityFilterChain

役割
HTTPリクエストに対するセキュリティ設定を定義します。

ポイント

  • CSRF無効化
    → テストやシンプルなアプリケーションで CSRF 保護を無効化しています(実運用では注意が必要)。
Java
.csrf(csrf -> csrf.disable())
  • リクエストごとの認可設定
Java
.authorizeHttpRequests(auth -> auth
    .requestMatchers("/login", "/error", "/resources/**").permitAll() // 認証不要
    .anyRequest().authenticated() // 他のリクエストは認証が必要
)
  • フォームログイン設定
    /login, /error, /resources/** は認証不要、それ以外は認証が必要
Java
.authorizeHttpRequests(auth -> auth
    .requestMatchers("/login", "/error", "/resources/**").permitAll() // 認証不要
    .anyRequest().authenticated() // 他のリクエストは認証が必要
)
  • ログアウト設定
    /logout エンドポイントでログアウトを処理し、成功後に /login?logout=true へリダイレクト。
Java
.logout(logout -> logout
    .logoutUrl("/logout") // ログアウトのエンドポイント
    .logoutSuccessUrl("/login?logout=true") // ログアウト成功後のリダイレクト先
    .permitAll()
)
HttpFirewall

役割
HTTPリクエストの構文検証やセキュリティ強化を行います。

今回は;(セミコロン)を URL 内で許可しています。
通常、セミコロンは HTTPリクエストのパラメータ区切りに使われ、セキュリティリスクがある場合があるため、デフォルトではブロックされます。特定の要件がある場合にのみ設定します。

Java
@Bean
public HttpFirewall allowSemicolonFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}
PasswordEncoder

役割
パスワードをハッシュ化して保存・認証時に比較します。

ポイント
安全なハッシュアルゴリズムである BCrypt を使用。

Java
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

AuthenticationManager

役割
認証プロセスを管理します。

ポイント
Spring Security が提供する認証マネージャをカスタマイズせず利用。

Java
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
    return authConfig.getAuthenticationManager();
}

コントローラー作成

src/main/java/com/example/sql/controller/LoginController.java

Java
package com.example.sql.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {

    // GETリクエストで "/login" にアクセスされたときに呼び出される
    @GetMapping("/login")
    public String loginPage() {
        return "login";  // "login.html" というテンプレートを返す
    }
}

ログインページ

ログインフォームを提供するHTMLを作成します。

src/main/resources/templates/login.html

HTML
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form th:action="@{/login}" method="post">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required>
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit">Login</button>
    </form>
    <div>
        <p th:if="${param.error}">Invalid username or password.</p>
        <p th:if="${param.logout}">You have been logged out.</p>
    </div>
</body>
</html>

ホームページ

ログイン後に表示されるホームページを作成します。

src/main/resources/templates/index.html

HTML
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Home</title>
</head>
<body>
    <h1>Welcome!</h1>
    <p>You are successfully logged in.</p>
    <a th:href="@{/logout}">Logout</a>
</body>
</html>

検証

テストデータの作成

ハッシュ化されたパスワードを用意する必要があります。
Spring CLIをインストールしている場合、以下のコマンドで生成できます。

SQL
spring encodepassword your_password_here

MySQLにアクセスし、データをinsertします。

SQL
INSERT INTO DB名.users (username, password, role)
VALUES ('user1’, 'password', 'ROLE_USER')

アプリケーションの起動

Bash
./gradlew bootRun

ブラウザでアクセス

  • ログインページ: http://localhost:8080/login
  • ログイン成功後: 指定したデフォルトページ(例: /)にリダイレクト。

もしうまくレンダリングされていない場合はsrc/main/resources/application.propertiesに下記を追加してください。

Bash
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

その他の高度な認証方法

OAuth2とは?

OAuth2は、ユーザーが安全に外部サービス(例: Google, Facebook)を利用してアプリにログインできる仕組みです。

メリット:

  • ユーザー名やパスワードを直接管理する必要がない。
  • 外部認証プロバイダー(Googleなど)を利用できる。

JWT(JSON Web Token)とは?

JWTは、認証情報を安全にやり取りするためのトークンです。ユーザーがログインすると、サーバーがJWTを生成し、クライアントに渡します。

  • メリット:
    • トークンを使うことで、サーバー側でセッションを管理する必要がない。
    • 軽量で効率的。
  • JWTを使った認証フロー:
    1. ユーザーがログインする。
    2. サーバーがJWTを生成してクライアントに返す。
    3. クライアントはリクエストごとにJWTを送る。
    4. サーバーはJWTを検証してリソースを提供する。

認証方法の選択

認証方法使用例メリットデメリット
Spring Security (基本)シンプルなアプリや内部システム容易にセットアップ可能拡張性に欠ける
OAuth2GoogleやFacebookでログイン外部認証を活用でき、セキュリティが向上外部サービス依存
JWTモバイルアプリや分散システム軽量でスケーラブルセキュリティ設定の理解が必要

まとめ

認証のフローをまとめるとこのようになります。

通常のMVCフローSpring Security認証フロー
クライアントがリクエストを送信するクライアントがリクエスト(例: /login)を送信する
コントローラーがリクエストを受け取るフィルタチェーンがリクエストを受け取る
サービスがリポジトリを通じてデータを取得UserDetailsServiceがリポジトリを通じてユーザーを取得
結果をコントローラーに返す認証結果をフィルタチェーンが処理し、SecurityContextに保存
コントローラーが結果をクライアントに返す認証成功後、リクエストはコントローラーに渡される

フィルタ層とは

フィルタ層は、リクエストやレスポンスに対して特定の処理(例: 認証、認可、ログ記録、データ変換など)を行う仕組みを指します。Spring Frameworkでは、特にServlet FilterFilter Chainを通じて、このフィルタリング処理が実現されます。

重要なポイント

  1. Spring Securityはフィルタ層で認証を処理
    • 通常のMVCフローとは異なり、認証部分は コントローラーよりも前 に処理されます。
    • そのため、認証処理はコントローラーの中ではなく、UserDetailsServiceSecurityConfig で定義します。
  2. 認証後にコントローラーにリクエストが渡る
    • 認証成功後、通常のコントローラーの処理(サービスクラスやリポジトリの呼び出し)に進みます。
  3. 設定ファイルでアクセス制御
    • Spring Securityは HttpSecurity を使って、リクエストごとのアクセス制御を定義できます。
    • コントローラーでリクエストを受ける前に、アクセス可能かを判定します。

セキュリティはアプリ開発において非常に重要です。この基本を理解し、実践してみましょう!
この記事が参考になれば幸いです。

コメント

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