SpringSecurity6+MyBatisで独自ログイン認証(実装編)

技術

SpringSecurity6系は5.3までと比べるとSecurityConfigの書き方が変わってるので勉強を兼ねて独自のログイン画面を用意し認証機能をjavaで作成してみました。

またMyBatisとH2 Databaseを使うことでDB構築をせずとも、手軽にDBと連携できるようにしてみました。

なお、記載内容が多くなったため、記事を2つに分けてます。

前編は以下をご参照ください。

開発環境

私が構築した時の開発環境は以下の通りとなります。

環境バージョン
Java17
SpringBoot3.4.3
SpringSecurity6.4.3
thymeleaf3.1.3.RELEASE
MyBatis3.0.4
H2 Database2.3.232
lombok1.18.36

ソース構成

ソース構成は以下の通りです。

  • login-project
    • src
      • main
        • java
          • com
            • example
              • loginproject
                • config
                  • LoginUsersDataInitializer.java
                  • SecurityConfig.java
                  • SecurityProperties.java
                • controller
                  • LoginController.java
                • entity
                  • LoginUsers.java
                • mapper
                  • LoginUsersMapper.java
                • service
                  • LoginUsersDetailsService.java
          • LoginProjectApplication.java
        • resources
          • com
            • example
              • loginproject
                • mapper
                  • LoginUsersMapper.xml
          • static
            • css
              • style.css
            • templates
              • home.html
              • login.html
          • application.yml
          • schema.sql
    • pom.xml

SQLの作成

MyBatisのmapper.xmlを使ってSQLを使用します。

LoginUsersMapper.xmlを作成します。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.loginproject.mapper.LoginUsersMapper">
    <select id="findUserByUsername" parameterType="string" resultType="LoginUsers">
        SELECT username, password
        FROM login_users
        WHERE username = #{username}
    </select>
    <insert id="insertUser" parameterType="LoginUsers">
        INSERT INTO login_users (username, password, enabled)
        VALUES (#{username}, #{password}, true);
    </insert>
</mapper>

namespace=”com.example.loginproject.mapper.LoginUsersMapper”と指定することで後述のLoginUsersMapper.javaに用意されたメソッドとLoginUsersMapper.xmlに用意された各SQLのid名がリンクします。

リンクすることでLoginUsersMapper.javaからLoginUsersMapper.xmlのSQLが実行できるようになります。

resultType=”LoginUsers”はSELECTの結果をcom.example.loginproject.entity.LoginUsersクラスに設定するために指定してます。

parameterType=”LoginUsers”と指定することでLoginUsersクラスのusernameやpassword変数に格納した値がINSERT文のVALUES句のusernameやpasswordに設定されるようにしてます。

複数の値をSQLに渡す必要がある時に便利です。

対してparameterType=”string”は文字列型の値をusernameに設定できるようにしてます。

findUserByUsernameはログイン時などに入力したユーザー名が存在するかをチェックするためのSQLです。

insertUserはアプリ起動時にログインアカウントを生成するためのSQLです。

Entityクラスの作成

後述のMapperインターフェースで使用するEntityクラスを作成します。

LoginUsersクラスを作成します。

package com.example.loginproject.entity;

import lombok.Data;

@Data
public class LoginUsers {
    private String username;
    private String password;
}

Mapperインターフェースの作成

LoginUsersMapper.xmlを使うMapperインターフェースを作成します。

LoginUsersMapperクラスを作成します。

package com.example.loginproject.mapper;

import com.example.loginproject.entity.LoginUsers;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface LoginUsersMapper {
    LoginUsers findUserByUsername(String username);
    void insertUser(LoginUsers loginUsers);
}

MapperアノテーションによってMyBatisがLoginUsersMapperをMapperインターフェースとして自動的に検出します。

こうすることでLoginUsersMapper.javaを通じてLoginUsersMapper.xmlのSQLを使うことができます。

ログインアカウントを生成するクラスの作成

今回はdata.sqlを使ってログインアカウントを生成しません。

H2のDBにデータを生成するのはdata.sqlを使うのが一般的だと思います。

しかし、data.sqlを使ってデータを生成しても、BCryptPasswordEncoderを使っているためにハッシュが一致せずに正しいパスワードを入力してもエラーとなってしまいます。

そのためアプリ起動時にデータをインサートする処理を実行するためにLoginUsersDataInitializerクラスを作成します。

package com.example.loginproject.config;

import com.example.loginproject.entity.LoginUsers;
import com.example.loginproject.mapper.LoginUsersMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Component
public class LoginUsersDataInitializer {

    private final LoginUsersMapper loginUsersMapper;
    private final PasswordEncoder passwordEncoder;
    private final SecurityProperties securityProperties;

    @Bean
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public ApplicationRunner dataInitializer() {
        return args -> {
            if (loginUsersMapper.findUserByUsername(securityProperties.getDebugUser().getUsername()) == null) {
                LoginUsers loginUsers = new LoginUsers();
                loginUsers.setUsername(securityProperties.getDebugUser().getUsername());
                loginUsers.setPassword(passwordEncoder.encode(securityProperties.getDebugUser().getPassword()));
                loginUsersMapper.insertUser(loginUsers);
            }
        };
    }
}

dataInitializerメソッドではBeanアノテーションを付与することでBeanとしてApplicationRunnerを登録するようにしてます。

また以下の処理をApplicationRunnerとして返却することでApplicationRunnerクラスのrunメソッドの中身を以下の処理にしてます。

            if (loginUsersMapper.findUserByUsername(securityProperties.getDebugUser().getUsername()) == null) {
                LoginUsers loginUsers = new LoginUsers();
                loginUsers.setUsername(securityProperties.getDebugUser().getUsername());
                loginUsers.setPassword(passwordEncoder.encode(securityProperties.getDebugUser().getPassword()));
                loginUsersMapper.insertUser(loginUsers);
            }

Springはアプリ起動時にApplicationRunner.runを実行します。

そのため、アプリケーション起動時にBeanとして登録された上記のログインアカウントを生成するインサート処理が実行されるようになります。

以下はDBのトランザクション管理をするためのアノテーションです。

 @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)

propagation = Propagation.REQUIREDはすでにトランザクションが存在する場合はそのトランザクションを使い、そうでない場合は新しくトランザクションを作るようにする設定となります。

このため、トランザクションを無駄に作成することがないため、パフォーマンスが向上します。

また複数のトランザクションが作成されることにより部分的にコミットされてしまうといったことがありません。

rollbackFor = Exception.classはExceptionクラスとExceptionクラスのサブクラス(つまり何らかの例外)が発生したらロールバックを行うという設定になります。

認証用のサービスクラス作成

MyBatisを使う場合、UserDetailsServiceクラスを実装して独自の認証用サービスクラスを作成する必要があります。

そのため、LoginUsersDetailsServiceクラスを作成します。

package com.example.loginproject.service;

import com.example.loginproject.entity.LoginUsers;
import com.example.loginproject.mapper.LoginUsersMapper;
import lombok.RequiredArgsConstructor;
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.stereotype.Service;

@RequiredArgsConstructor
@Service
public class LoginUsersDetailsService implements UserDetailsService {

    private final LoginUsersMapper loginUsersMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LoginUsers loginUsers = loginUsersMapper.findUserByUsername(username);
        if (loginUsers == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }

        return User.builder()
                .username(loginUsers.getUsername())
                .password(loginUsers.getPassword())
                .build();
    }
}

以下の処理によってログイン画面で入力されたユーザー名がDBに存在するかチェックしてます。

        LoginUsers loginUsers = loginUsersMapper.findUserByUsername(username);
        if (loginUsers == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }

存在している場合、ログイン画面で入力されたユーザー名とパスワードを使ってUserDetailsインスタンスを生成します。

        return User.builder()
                .username(loginUsers.getUsername())
                .password(loginUsers.getPassword())
                .build();

生成されたUserDetailsインスタンスを使ってSpringが提供するAuthenticationManagerクラスがDBに登録されているパスワードと一致してるか認証を行います。

ログイン画面用のコントローラー作成

ログイン画面用のコントローラーを作成します。

package com.example.loginproject.controller;

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

@Controller
public class LoginController {

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

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

ログイン画面を表示するloginメソッドとログイン成功時の画面を表示するhomeメソッドを用意してます。

ログイン画面とログイン後の画面作成

ログイン画面とログイン後の画面を作成します。

まずはログイン画面であるlogin.htmlです。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ログイン - MyApp</title>
    <link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
<div class="login-container">
    <h2>ログイン</h2>
    <div th:if="${param.error}" class="error-message">
        ユーザー名またはパスワードが間違っています。
    </div>
    <form th:action="@{/login}" method="post">
        <div class="input-group">
            <label for="username">ユーザー名</label>
            <input type="text" id="username" name="username" required>
        </div>
        <div class="input-group">
            <label for="password">パスワード</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit" class="login-button">ログイン</button>
    </form>
</div>
</body>
</html>

ログイン画面で使用するCSSとしてstyle.cssを作成します。

body {
    font-family: Arial, sans-serif;
    background-color: #f4f4f4;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}
.login-container {
    background-color: #fff;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    width: 300px;
    text-align: center;
}
.login-container h2 {
    margin-bottom: 20px;
}
.input-group {
    margin-bottom: 15px;
    text-align: left;
}
.input-group label {
    display: block;
    margin-bottom: 5px;
}
.input-group input {
    width: 95%;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 5px;
}
.login-button {
    background-color: #007bff;
    color: #fff;
    border: none;
    padding: 10px;
    width: 100%;
    border-radius: 5px;
    cursor: pointer;
}
.login-button:hover {
    background-color: #0056b3;
}
.error-message {
    color: red;
    margin-bottom: 10px;
}

続いてログイン後の画面であるhome.htmlです。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Home</title>
</head>
<body>
    <h1>Welcome Home!</h1>
    <p>ログイン成功しました</p>
</body>
</html>

アプリ起動用のクラス作成

最後にアプリ起動用のクラスを作成します。

package com.example.loginproject;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LoginProjectApplication {

    public static void main(String[] args) {
        SpringApplication.run(LoginProjectApplication.class, args);
    }

}

ログイン画面へアクセス

ブラウザでhttp://localhost:8080/loginにアクセスすると以下のようなログイン画面が表示されます。

ユーザー名とパスワードはapplication.ymlの以下の値となります。

  debug-user:
    username: user
    password: password

そのため、ユーザー名にuser、パスワードにpasswordと入力し、ログインボタンを押下すればログインが成功します。

h2-consoleへアクセス

ブラウザでhttp://localhost:8080/h2-consoleへアクセスすると以下のような画面へアクセスできます。

Connectボタンを押すと以下のような画面が表示できます。

この画面では上記のようにSQLを実行でき、H2上の各テーブルのデータを見ることができるので便利です。

なお、記載内容が多くなったため、記事を2つに分けてます。

前編は以下をご参照ください。

コメント

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