🔐 Spring Securityとは？Spring Bootでのログイン処理を実装してみた

## はじめに

Spring Bootを使ったWebアプリケーション開発では、セキュリティ対策も重要なポイント。その代表的なセキュリティフレームワークが「Spring Security」です。

Spring Securityについて調べても古い記事が多くエラーを大量に出してしまったりして苦労したので、自信の知識定着のため、今後のために自分が書いたコードを一部抜粋してコードを掲載します。他にもっといい書き方があればかえていきたいものです

本記事では、**Spring Securityの基本的な考え方**と**実際のログイン処理のコード例**を紹介します。

## 💡 Spring Securityとは？

Spring Securityは、Springアプリケーションにセキュリティ機能（認証・認可）を組み込むためのフレームワークです。

### 主な機能

- 認証（ログイン機能）
- 認可（アクセス制御）
- パスワードの暗号化
- セッション管理
- CSRF対策 など

## 🔧 実装準備

### 1. 依存関係の追加（`build.gradle`）

```groovy
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}
```

### 2. エンティティクラス（mUser）
```groovy
@Entity
public class mUser {
    private Integer usersid;
    private String username;
    private String password;
    private String role;

    // getter/setter省略
}
```

### 3. カスタムユーザークラス（CustomUserDetails）
```groovy
@Entity
public class CustomUserDetails implements UserDetails {
   /** DB から取得したユーザーエンティティ */
	private final MUser mUser;

	/** 権限コレクション */
	private final Collection<? extends GrantedAuthority> authorities;

	public CustomUserDetails(MUser mUser) {
		this.mUser = mUser;
		// ROLE_ 付きでそのまま権限に
		this.authorities = List.of(new SimpleGrantedAuthority(mUser.getRole()));
	}
    
    /* ─────────── 追加で使いたい独自 getter ─────────── */

	/** ユーザー SID を返す */
	public Integer getUserSid() {
        return mUser.getUserSid();
	}

	/** 表示用ユーザー名 */
	public String getDisplayName() {
		return mUser.getUserName();
	}

	/** MUser をそのまま欲しい場合 */
	public MUser getMUser() {
		return mUser;
	}
	/* ─────────── UserDetails 実装 ─────────── */

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return authorities;
	}

	@Override
	public String getPassword() {
		return mUser.getPassword();
	}

	/** Spring Security がログイン ID とみなす値 */
	@Override
	public String getUsername() {
		return mUser.getUserId();
	}

	@Override
	public boolean isAccountNonExpired() {
		return true; // 期限切れ判定を実装するならここで
	}

	@Override
	public boolean isAccountNonLocked() {
		return true; // ロック判定を実装するならここで
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true; // 資格情報の期限切れ判定を実装するならここで
	}

	@Override
	public boolean isEnabled() {
		return true; // 無効ユーザー判定を実装するならここで
	}
}
```

4.🔐 Spring Securityの設定クラス（SecurityConfig）
```groovy
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import lombok.RequiredArgsConstructor;

@Configuration // Springの設定クラスとして認識させる
@EnableWebSecurity // Spring SecurityのWebセキュリティ機能を有効化
@RequiredArgsConstructor // final フィールド や @NonNull が付いたフィールド に対して コンストラクタを自動生成
public class SecurityConfig {

	// ユーザー情報を取得する独自のUserDetailsService（DIで注入）
	@Autowired
	private CustomUserDetailsService userDetailsService;

	// セキュリティフィルターチェーンの設定
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			// リクエストごとのアクセス制御ルールを定義
			.authorizeHttpRequests(auth -> auth
				.requestMatchers("/login", "/signup/**", "/css/**", "/js/**").permitAll() // 認証不要のパス
				.anyRequest().authenticated() // それ以外は認証が必要
			)
			// フォームログインの設定
			.formLogin(login -> login
				.loginPage("/login") // ログインページのURL
				.defaultSuccessUrl("/dashboard", true) // ログイン成功後の遷移先
				.failureUrl("/login?error") // ログイン失敗時の遷移先
				.permitAll() // 認証なしでアクセス可能
			)
			// ログアウトの設定
			.logout(logout -> logout
				.logoutUrl("/logout") // ログアウト処理のURL
				.logoutSuccessUrl("/login?logout") // ログアウト後の遷移先
				.invalidateHttpSession(true) // セッションを無効化
				.deleteCookies("JSESSIONID") // クッキーを削除
				.permitAll() // 認証なしでアクセス可能
			)
			// 認証プロバイダーの設定
			.authenticationProvider(authenticationProvider());

		// セキュリティフィルターを構築して返却
		return http.build();
	}

	// DaoAuthenticationProviderのBean定義（ユーザー情報とパスワードの照合用）
	@Bean
	public DaoAuthenticationProvider authenticationProvider() {
		DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
		provider.setUserDetailsService((UserDetailsService) userDetailsService); // ユーザー情報取得ロジックを設定
		provider.setPasswordEncoder(passwordEncoder()); // パスワードのエンコーダーを設定
		return provider;
	}

	// パスワードをハッシュ化するためのエンコーダー（BCrypt）
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

```

5.🧩 ユーザー情報を取得する独自サービス（UserDetailsService）
```groovy
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.stereotype.Service;

import com.jp.wc.mybatis.login.CustomUserMapper;
import com.jp.wc.mybatis.model.MUser;

@Service
public class CustomUserDetailsService implements UserDetailsService {

	@Autowired
	private CustomUserMapper customUserMapper;

	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
        // ログイン情報をDBから取得
		MUser user = customUserMapper.selectByUserId(userId);
		if (user == null) {
			throw new UsernameNotFoundException("User not found");
		}

		return new CustomUserDetails(user);
	}

}
```

6.🧩 ログイン画面（Thymeleaf）
```groovy
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
	<meta charset="UTF-8">
	<title>ログイン</title>
    <!-- Bootstrap CSS CDN -->
	<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"
		integrity="sha384‑JcKb8q3iqJ61gNVpCF6e1QZr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous">
</head>

<body class="bg-light">
	<div class="container vh-100 d-flex justify-content-center align-items-center">
		<div class="card shadow-sm w-100" style="max-width: 400px;">
			<div class="card-body">
				<h3 class="card-title text-center mb-4">ログイン</h3>
				<form th:action="@{/login}" method="post">
					<div class="form-group">
						<label for="username">ユーザー名</label>
						<input type="text" class="form-control" id="username" name="username"
							placeholder="Enter username" required />
					</div>
					<div class="form-group">
						<label for="password">パスワード</label>
						<input type="password" class="form-control" id="password" name="password"
							placeholder="Enter password" required />
					</div>
					<button type="submit" class="btn btn-primary btn-block">ログイン</button>
				</form>
			</div>
		</div>
	</div>
</body>

</html>
```

7.🏠 ログイン後の画面（Controller）
```groovy
@Controller
public class testController {

    @GetMapping("/dashboard")
    public String dashboard() {
        return "dashboard";
    }

    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

```

## おわりに

Spring Securityを使えば、ログイン機能を比較的簡単に実装できます。
ただし、設定やカスタマイズには慣れが必要なので、今回のような基本構成から少しずつ手を加えていくのがおすすめです。