Spring Securityでフォーム認証を実装する(実装編)

SpringSecurityはバージョンごとに設定の書き方が大幅に変更されている。
ここでは、バージョンごとに共通となっている処理の記載方法についてまとめる。

依存関係の追加

dependencies {
  // Spring
  implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
  testImplementation 'org.springframework.security:spring-security-test'

  // Spring Boot2系の場合
  // Thymeleaf Extras Spring Security5
  implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

  // Spring Boot3系の場合
  // Thymeleaf Extras Spring Security6
  implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
}

フォームログイン画面を作成する

今回は、ユーザ名、メールアドレス、識別子を入力してもらうログイン画面を実装する。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ログイン画面</title>
</head>
<body>
    <form action="#" th:action="@{/login}" method="post">
        <h1>Login</h1>     
        <p>ユーザ名 : <input type="text" name="username" th:value="${username}" id="username"/></p>
        <p>メールアドレス : <input type="text" name="email" th:value="${email}" id="email"/></p>
        <p>ユーザ識別子 : <input type="text" name="sub" th:value="${sub}" id="sub"/></p>
        <p><input type="submit" value="ログイン" /></p>
    </form>
</body>
</html>

フォームログイン画面でユーザをプルダウンから選べるようにする

開発時の動作確認の際に毎回自由入力だと大変なため、プルダウンを選択することで任意のユーザ情報がテキストボックスに入力されるように実装を追加する。
プルダウンで反映されるユーザ情報はymlファイルに定義する。

プルダウンで表示するユーザ情報

- name: ユーザー選択
  email: 
  sub:

- name: 山田太郎
  email: yamada.tarou@co.jp
  sub: auth|000001

- name: 佐藤花子
  email: sato.hanako@co.jp
  sub: auth|000002

- name: 鈴木一郎
  email: suzuki.ichiro@co.jp
  sub: auth|000003

プルダウンで指定されたユーザ情報をテキストボックスに反映する処理

let userinfo;

/*
* プルダウンで選択されたユーザーの情報をテキストボックスに反映する。
*/
function getUserInfo() {
    const index = document.getElementById('userinfo').selectedIndex;

    const username = document.getElementById('username');
    const email = document.getElementById('email');
    const sub = document.getElementById('sub');

    username.value = userinfo[index].name;
    email.value = userinfo[index].email;
    sub.value = userinfo[index].sub;
}

/*
* ymlファイルからユーザー情報を読み込んでプルダウンを生成する。
*/
$(function(){
    $.get('js/login-user.yml')
    .done(function (data) {
        userinfo = jsyaml.load(data);

        let pulldown = document.getElementById('userinfo');
        document.createElement('option');
        for(let i = 0; i < userinfo.length; i++){
            let option = document.createElement('option');
            option.innerHTML = userinfo[i].name;
            pulldown.appendChild(option);
        };
    });
});

HTML

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ログイン画面</title>
    <script th:src="@{/webjars/jquery/jquery.min.js}"></script>
    <script th:src="@{/webjars/js-yaml/dist/js-yaml.min.js}"></script>
    <script type="text/javascript" th:src="@{/js/userinfo.js}"></script>
</head>
<body>
    <form action="#" th:action="@{/login}" method="post">
        <h1>Login</h1>
        
        <div><select name="userinfo" id="userinfo" onchange="getUserInfo()"></select></div>
        
        <p>ユーザ名 : <input type="text" name="username" th:value="${username}" id="username"/></p>
        <p>メールアドレス : <input type="text" name="email" th:value="${email}" id="email"/></p>
        <p>ユーザ識別子 : <input type="text" name="sub" th:value="${sub}" id="sub"/></p>
        <p><input type="submit" value="ログイン" /></p>
    </form>
</body>
</html>

フォームログイン画面を表示するコントローラを実装する

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class LocalLoginController {

    /**
     * フォーム認証のログイン画面を表示する。<br>
     * 
     * @return フォーム認証のログイン画面
     */
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login() {
        return "form-login";
    }
}

ユーザ情報を構築するサービスを実装する

今回はパスワードを固定値にして認証したかったので、以下のように、固定値「password」をエンコードした値でユーザ情報を構築している。

デフォルトでは、ログで出力されたパスワードを入力する必要がある。以下は出力されるログのサンプル。

2024-02-16 15:27:46.393 WARN  org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.getOrDeducePassword <restartedMain> --- 

Using generated security password: a4c4a5a0-0fb8-4565-89d3-b3fdb9ea8d64

This generated password is for development use only. Your security configuration must be updated before running your application in production.
import java.util.Collections;

import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;

public class LocalUserDetailService implements UserDetailsService {

    /** パスワードのエンコーダー */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * {@inheritDoc} <br>
     * パスワードを固定値にしたユーザーを返却する。<br>
     */
    @Override
    public UserDetails loadUserByUsername(String username) {
        if (username == null) {
            throw new UsernameNotFoundException("ユーザー名が入力されていません");
        }

        String password = passwordEncoder.encode("password");
        return new User(username, password, Collections.emptySet());
    }
}

フォーム認証成功時にユーザ情報を構築するハンドラを実装する

フォーム認証完了後に、ログイン画面で入力された値を使用して、ユーザオブジェクトを構築してスレッドローカルに格納するクラス。

スレッドローカルに格納することで、ユーザ情報をコントローラの引数などで手軽に取得できるようになる。

import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.web.authentication.AbstractAuthenticationTargetUrlRequestHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

public class LocalAuthenticationSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements AuthenticationSuccessHandler {

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        handle(request, response, authentication);

        // リクエストからユーザー情報を取り出す
        String email = request.getParameter("email");
        String username = request.getParameter("username");
        String sub = request.getParameter("sub");
   
        // 取り出したユーザー情報を使用してOidcUserを作成する
        Map<String, Object> map = new HashMap<>();
        map.put(StandardClaimNames.SUB, sub);
        map.put(StandardClaimNames.EMAIL, email);
        map.put(StandardClaimNames.NAME, username);

        OidcIdToken oidcToken = new OidcIdToken("tokenvalue", Instant.now().minusSeconds(10), Instant.now(), map);
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority("role");

        List<SimpleGrantedAuthority> list = new ArrayList<>();
        list.add(authority);

        DefaultOidcUser user = new DefaultOidcUser(list, oidcToken);

        OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, null, "authorizedClientRegistrationId");
        
        // 作成したユーザオブジェクトをスレッドローカルに格納する
        SecurityContextHolder.getContext().setAuthentication(token);
    }
}

今回はSpringSecurityのバージョンによらない部分のソースの実装をしていきました。
次回の記事で、SpringSecurityの有効化とJavaConfigの設定をしていきます。