ログイン認証について

# はじめに

SpringSecurityを使用したログイン認証については、framework内で処理が行われているため、少々わかりにくいことがあるかと思います。認証プロセスを理解することで認証周りの開発も躓くことなく進められると思いますので、認証プロセスについて解説していきます。

# SpringSecurityを使用した認証プロセス

ログイン認証時のユーザー名とパスワードの検証は、Spring Securityの認証プロセス内で行われています。具体的には、以下の流れになります。

1. ユーザがログインフォームからユーザ名とパスワードを入力して送信します
1. ```UsernamePasswordAuthenticationFilter```がリクエストを受け取り、ユーザー名とパスワードを抽出します
1. ```AuthenticationManager```によって認証プロセスが開始されます
1. 取得したユーザー情報のパスワードと、入力されたパスワードを```PasswordEncoder```を使用して比較します
1. パスワードが一致すれば認証成功となり、ユーザーの認証情報が```SecurityContextHolder```に保存されます
1. パスワードが一致しない場合は認証失敗となり、エラーが返されます

認証後は、認証情報は```SecurityContextHolder```に保持されます。
SecurityContextHolderのAuthenticationをクリアすることでログアウト可能となります。



# ログイン認証についてのサンプルコード

## 
認証機能、ログイン画面、ホーム画面（認証後表示）、ログアウト機能を作成しました。
認証用のユーザについては```InMemoryUserDetailsManager```を使用してメモリ上に保持しています。サンプルコードを記載します。

### 認証、ログアウト機能
```AuthenticationManager```は```UserDetailsService```と```PasswordEncoder```の組み合わせで認証プロセスが開始されます。



* ```UserDetailsService```はユーザアカウントを作成
* ```PasswordEncoder```はパスワードのエンコードとマッチングに```BCryptPasswordEncoder```を使用




```java SpringConfig.java
package com.example.springboot_login.config;

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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated() // すべてのリクエストは認証が必要
        ).formLogin(form -> form.loginPage("/login") // ログインページのURL
                .permitAll() // ログインページへのアクセスを許可
                .defaultSuccessUrl("/home", true) // 認証成功後のリダイレクト先を/homeに設定
        ).logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/login?logout"));

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user1 = User.withUsername("user1")
                .password(passwordEncoder().encode("password1")).roles("USER").build();
        UserDetails user2 = User.withUsername("user2")
                .password(passwordEncoder().encode("password2")).roles("USER").build();
        UserDetails admin = User.withUsername("admin")
                .password(passwordEncoder().encode("adminpassword")).roles("ADMIN").build();

        return new InMemoryUserDetailsManager(user1, user2, admin);
    }

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

}

```

### コントローラー
リクエストの受け取るコントローラーの定義

```java HomeController.java
package com.example.springboot_login.contoller;

import java.security.Principal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


@Controller
public class HomeController {
    @GetMapping("/home")
    public String home(Model model, Principal principal) {
        model.addAttribute("username", principal.getName());
        return "home";
    }

    @GetMapping("/login")
    public String login() {
        return "login"; // login.htmlを返す
    }

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        return "redirect:/login?logout";
    }

}

```

### HTML
HTMLの定義

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

```

```html login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Home Page</title>
</head>
<body>
    <h2>Welcome to the Home Page!</h2>
    <p>You have successfully logged in.</p>
    <h1>Welcome, <span th:text="${username}"></span>!</h1>
    <a th:href="@{/logout}">Logout</a>
</body>
</html>

```

# まとめ
SpringSecurityを使用することで、認証処理は自動で行われることになります。認証機能自体の実装をすることなく利用できるため非常に楽かと思います。

サンプルソースについてはGITに公開しているので、参考にしてみてください。
[springboot-login](https://github.com/yana141421356/springboot-login)

ユーザ情報をDBから取得するして比較したい、ユーザ情報をAPIで取得して比較したい、ユーザの検証はAPIで行っている等、認証経路は色々あると思いますでの、今後ケース別に記事として記載できればと思います。