【Spring Framework】Spring Securityを使用したログイン機能の実装

## 概要
この記事では、Spring Securityを使ったログイン機能の実装手順を記載します。
私自身も詳しく触れるのは初めてなので、いろいろ検証しながら進めていきます。

## 実装

まず、Spring Initializr などのツールを使って、新しいSpring Bootプロジェクトを作成します。
今回は以下のプロジェクトを使用します

- Spring スターター・プロジェクト
    - Javaバージョン：21
    - タイプ：Maven
    - Spring Bootバージョン：3.5.0

### HomeControllerの作成
HomeController.java のような名前で新しいクラスを作成し、以下のように記述します。

```java:HomeController.java
package myapp.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HomeController {
	@GetMapping("/")
	public String home() {
		return "Welcome to the Home Page!";
	}
}
```

このControllerは、`@GetMapping("/")` によってルートパスへのGETリクエストを処理します。
ブラウザで`http://localhost:8080`にアクセスすると、単純に文字列「Welcome to the Home Page!!」を返します。
![SnapCrab_localhost8080 - Google Chrome_2025-6-8_21-43-45_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920265/e70ffbd3-98a8-4f3e-a3c4-a1ce32205f4b.png)


### Spring Security を使ったログイン機能実装
シンプルなWebアプリケーションの準備ができたところで、いよいよSpring Securityを導入します。このステップでは、Spring Securityが提供するデフォルトのユーザーとパスワードを使用して、ログイン機能を有効にします。

まず、pom.xml（Mavenの場合）またはbuild.gradle（Gradleの場合）にSpring Securityの依存関係を追加します。
今回はMavenを選択したのでpom.xmlに以下の記述を追加します。
```xml:pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
```

依存関係を追加したら、アプリケーションを再起動します。
ブラウザで`http://localhost:8080`にアクセスしてみましょう。今度は「Hello World!」と表示される代わりに、Spring Securityが提供するログイン画面が表示されるはずです。
![SnapCrab_Please sign in - Google Chrome_2025-6-8_21-46-13_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920265/35f87c29-e6c5-4e3c-b90f-3c3962b300df.png)
spring securityのデフォルトログインではユーザー名は`user`、パスワードはコンソールに表示されるようです。
ログインしてみとHomeControllerの画面が開きます。

Spring Securityは、spring-boot-starter-security を導入するだけで、以下の動作を自動的に行ってくれるようです。

- **すべてのリクエストに対する認証の要求**:
`http://localhost:8080` へのアクセスがログイン画面にリダイレクトされたことからもわかるように、デフォルトですべてのリクエストに対して認証を要求します。
- **デフォルトユーザーの提供**
user というユーザー名で、一時的なパスワードが生成されます。
- **ログインフォームの自動生成**
標準的なログインフォームが自動的に提供されます。


このように、Spring Securityは最小限の設定で強力なセキュリティ機能を提供します。

### DBのユーザーパスワード（ハッシュ化なし）を実装

ここでは、ローカルのMSSQLServerを使用します。
application.properties にデータベース接続情報を追加し、簡単なユーザーテーブルを作成します。

```properties:application.properties
# MSSQL の接続URL
spring.datasource.url=jdbc:sqlserver://localhost:1433;databaseName=test;trustServerCertificate=true

# ユーザー名とパスワード
spring.datasource.username=DBユーザー名
spring.datasource.password=DBパスワード

# ドライバークラス
spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
```

```
user_id     user_login_id        password             user_name
----------- -------------------- -------------------- ----------
          1 AAA                  {noop}passA          ユーザー1
```

パスワードの前に`{noop}`とついてるのはハッシュ化を回避するためです。
後の工程で削除します。

データベースとの連携にSpring Data JPAを使用するため`pom.xml`に以下の記述を追加します。
```xml:pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.microsoft.sqlserver</groupId>
    <artifactId>mssql-jdbc</artifactId>
</dependency>
```

データベースとの連携をSpring Data JPAで行うため、ユーザー情報を格納するEntityクラスと、データベース操作を行うためのRepositoryインターフェースを作成します。

```java:UserEntity.java
package myapp.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import lombok.Data;

@Entity
@Table(name = "m_user")
@Data
public class User {
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "user_id")
	private int userId;

	@Column(name = "user_login_id")
	private String userLoginId;
	
	@Column(name = "password")
	private String password;
	
	@Column(name = "user_name")
	private String username;
}
```
```java:UserRepository.java
package myapp.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import myapp.entity.User;

public interface UserRepository extends JpaRepository<User, Integer> {
    Optional<User> findByUserLoginId(String userLoginId);
}
```
Spring Securityがデータベースからユーザー情報を取得するためには、UserDetailsService インターフェースを実装したクラスが必要です。このクラスは、ユーザー名に基づいて UserDetails オブジェクトを返します。Spring Data JPAを使用する場合、通常はリポジトリインターフェースを定義し、それを UserDetailsService の実装クラスで利用します。

```java:LoginUserService.java
package myapp.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import myapp.entity.User;
import myapp.repository.UserRepository;

@Service
public class LoginUserService implements UserDetailsService {
	
	@Autowired
	private UserRepository userRepository;

	@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUserLoginId(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return new org.springframework.security.core.userdetails.User(
                // username
                user.getUserLoginId(),
                // password
                user.getPassword(),
                // enabled
                true,
                // accountNonExpired
                true,
                // credentialsNonExpired
                true,
                // accountNonLocked
                true,
                // authorities
                AuthorityUtils.createAuthorityList("ROLE_USER")
        );
    }

}
```

各パラメータの詳細は以下の通りです

| パラメータ名            | 詳細                                                                                              |
| :---------------------- | :------------------------------------------------------------------------------------------------ |
| `username`              | `DaoAuthenticationProvider` に提示されるユーザー名です。                                      |
| `password`              | `DaoAuthenticationProvider` に提示されるべきパスワードです。                                 |
| `enabled`               | ユーザーが有効である場合に `true` を設定します。アカウントが無効な場合、ログインはできません。 |
| `accountNonExpired`     | アカウントが期限切れではない場合に `true` を設定します。期限切れの場合、ログインは拒否されます。 |
| `credentialsNonExpired` | 認証情報（パスワードなど）が期限切れではない場合に `true` を設定します。パスワードの有効期限管理などに使用されます。 |
| `accountNonLocked`      | アカウントが**ロックされていない場合に `true` を設定します。例えば、複数回のログイン失敗によりアカウントが一時的にロックされた場合などに `false` となります。 |
| `authorities`           | 呼び出し元が正しいユーザー名とパスワードを提示し、ユーザーが有効である場合に、そのユーザーに付与されるべき権限（ロール）のコレクションです。|

UserDetailsService をSpring Securityに認識させるために、セキュリティ設定クラス (SecurityConfig) を作成します。

```java:SecurityConfig.java
package myapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .formLogin(formLogin -> formLogin
                .permitAll()
            )
            .rememberMe(Customizer.withDefaults());

        return http.build();
    }
}
```

##### `@Configuration` アノテーション
このクラスがSpringのコンフィギュレーションクラスであることを示します。

##### `@EnableWebSecurity` アノテーション
Spring SecurityのWebセキュリティ統合を有効にします。これにより、Spring Bootの自動設定と連携して、Webアプリケーションにセキュリティ機能が適用されます。

##### SecurityFilterChain Beanの定義
filterChainメソッドは、Spring Securityの主要な部品であるSecurityFilterChainを作っています。このSecurityFilterChainは、Webからのリクエストが私たちのアプリケーションに届く前に、セキュリティのチェックを行うための仕組みを定めています。

このメソッド内で、HttpSecurityオブジェクトをカスタマイズすることで、以下のセキュリティ設定を行っています。

```java
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
```

これは、任意のリクエスト (anyRequest()) に対して認証 (authenticated()) を要求することを意味します。
`anyRequest()`を指定してるのでアプリケーション内のどのURLにアクセスしようとしても、まずログインが求められます。
```java
.formLogin(formLogin -> formLogin.permitAll())
```

これは、フォームベースのログインを有効にする設定です。
formLogin.permitAll() は、ログインページ自体へのアクセスは認証なしで許可することを意味します。
```java
.rememberMe(Customizer.withDefaults())
```
これは、ログイン状態を保持する機能の設定です
Customizer.withDefaults() を使用すると、ブラウザを閉じたり再起動したりしても、一定期間ログイン状態が維持されます。
```java
return http.build();
```
設定されたHttpSecurityオブジェクトからSecurityFilterChainをビルドして返します。これにより、上記のルールがアプリケーションのセキュリティフィルターチェーンに適用されます。

プロジェクトをリビルドし、再度`http://localhost:8080`でアクセスするとDBに登録したユーザー名、パスワードでログインできることが確認できます。

### パスワードのハッシュ化

Spring Securityを使ったログイン機能の実装で、パスワードを平文で扱うのはNGです。
今回はBCryptを使用しハッシュ化を行います。

####  BCryptの特徴

* ソルト（ランダムキー）を自動で生成し、同じパスワードでも毎回異なるハッシュ値が作られる。
* DBに保存するハッシュ値にソルト情報が含まれており、照合時に自動的に取り出して検証される。
* Spring Securityでは、`BCryptPasswordEncoder`を使うだけで、入力パスワードとDBのハッシュ値を自動で比較してくれる。

本来はシステムから登録するのですが、今回はテスト用でPythonで生成したハッシュ化したパスワードを直接DBに登録します。
以下がBCryptでハッシュ化を行うPythonのコードです。
実行するとコンソールにハッシュ化されたパスワードが出力されます。
```python:BCrypt.py
import bcrypt

# パスワード
raw_password = "password"

# ソルトを自動生成してハッシュ化
hashed_password = bcrypt.hashpw(raw_password.encode('utf-8'), bcrypt.gensalt())

print(hashed_password.decode())
```
```console:コンソール
$2b$12$fNC9nS8mUmMuyx.dYpDY8u4z7rRhTrT9R1V87iuUknyUY9vWtHAZe
```
コンソールの内容をDBのパスワードに設定します。
```
user_id     user_login_id        password                                                                                             user_name
----------- -------------------- ---------------------------------------------------------------------------------------------------- --------------------------------------------------
          1 AAA                  $2b$12$fNC9nS8mUmMuyx.dYpDY8u4z7rRhTrT9R1V87iuUknyUY9vWtHAZe                                         テストA
```

続いてSecurityConfigにBCryptを設定します。
SecurityConfigに以下のように@BeanでPasswordEncoderを設定するだけで、Spring Securityが自動的にエンコード処理を行ってくれるようです。
```java:SecurityConfig.java
package myapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // 追加部分
    // PasswordEncoderを@Beanで登録
	@Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .formLogin(formLogin -> formLogin
                .permitAll()
            )
            .rememberMe(Customizer.withDefaults());

        return http.build();
    }
}
```
これでシステムにログインしてみると、
![SnapCrab_Please sign in - Google Chrome_2025-6-12_0-32-43_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920265/f45fca65-740f-45af-90e3-d8376086fef2.png)
![SnapCrab_localhost8080 - Google Chrome_2025-6-8_21-43-45_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920265/92bf77b7-0184-4e91-8f47-250b6ec1d9d5.png)

無事ログインできました!

Spring Securityはシンプルな実装で、本格的なログイン機能がすぐに構築できるところがすごいと思いました。

ソースコード
https://github.com/shikama777/springSecurityProject/tree/master