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

pom.xml

必要なライブラリを導入するためにpom.xmlを以下のように記述します。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>login-project</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>login-project</name>
    <description>login-project</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.4</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

application.ymlの作成

アプリの設定を行うapplication.ymlを以下のように記述します。

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
      path: /h2-console

logging:
  level:
    org.springframework.security: DEBUG

security:
  login-url: /login
  home-url: /home
  permit-urls:
    - /login
    - /css/**
    - /js/**
    - /images/**
  debug-user:
    username: user
    password: password
  h2-console-url: /h2-console/**

mybatis:
  type-aliases-package: com.example.loginproject.entity
  configuration:
    map-underscore-to-camel-case: true

datasourceプレフィックスのpasswordは未入力で大丈夫です。

以下はh2のDBの中身を見れるようにh2-consoleを有効化してます。

h2:
  console:
    enabled: true
    path: /h2-console

以下はデバッグ用にSpringSecurityのログレベルをDEBUGに指定してます。

org.springframework.security: DEBUG

securityプリフィックス以下にある値は後述で示すSecurityPropertiesクラスで使用してます。

例えばlogin-urlにはログイン用のURLを記載します。

permit-urlsには未認証でもアクセスできるURLを記載します。

CSSやJSや画像などの静的ファイルは未認証でもアクセスできないと使用できないため許可します。


type-aliases-packageで指定したパッケージ配下のクラスはパッケージ名を省略してMyBatisのXMLで使用できます。

map-underscore-to-camel-case: trueを指定することでデータベースのカラム名をキャメルケースに自動変換できるようにします。

mybatis:
  type-aliases-package: com.example.loginproject.entity
  configuration:
    map-underscore-to-camel-case: true

schema.sqlの作成

アプリケーション起動時にDBのテーブルが作成されるようにschema.sqlを作成します。

CREATE TABLE IF NOT EXISTS login_users (
    username VARCHAR(50) PRIMARY KEY,
    password VARCHAR(100) NOT NULL,
    enabled BOOLEAN NOT NULL
);

application.ymlを扱うクラスの作成

application.ymlにlogin-urlといったログイン用のURLなどを定数として定義しました。

それらの定数を扱うためのクラスであるSecurityPropertiesクラスを作成します。

package com.example.loginproject.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@Component
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
    private String loginUrl;
    private String homeUrl;
    private List<String> permitUrls;
    private DebugUser debugUser;

    private String h2ConsoleUrl;

    @Data
    public static class DebugUser {
        private String username;
        private String password;
    }
}

lombokのDataアノテーションによってgetterとsetterメソッドを自作しないで自動的に生成されるようにしてます。

ConfigurationPropertiesアノテーションによってapplication.yml上のsecurityプレフィックス配下にあるものをすべてこのクラスの変数に格納するようにしてます。

permit-urlsプレフィックスが以下のようにハイフンを使って複数値を定義しているため、List<String>でpermitUrlsを宣言してます。

  permit-urls:
    - /login
    - /css/**
    - /js/**
    - /images/**

debug-userプレフィックスが複数の項目を持つため、DebugUserというインナークラスを作成しています。

  debug-user:
    username: user
    password: password

SpringSecurityの設定

SpringSecurityの設定を行うクラスを作成します。

package com.example.loginproject.config;

import lombok.RequiredArgsConstructor;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@RequiredArgsConstructor
@Configuration
public class SecurityConfig {

    private final SecurityProperties securityProperties;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        final String X_FRAME_OPTIONS = "X-Frame-Options";
        final String X_FRAME_OPTIONS_SAMEORIGIN = "SAMEORIGIN";
        final String X_FRAME_OPTIONS_DENY = "DENY";

        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(securityProperties.getPermitUrls().toArray(String[]::new)).permitAll()
                        .requestMatchers(securityProperties.getH2ConsoleUrl()).permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(login -> login
                        .loginPage(securityProperties.getLoginUrl())
                        .defaultSuccessUrl(securityProperties.getHomeUrl(), true)
                        .permitAll()
                )
                .csrf(csrf -> csrf
                        .ignoringRequestMatchers(securityProperties.getH2ConsoleUrl())
                )
                .headers(headers -> headers
                        .addHeaderWriter((request, response) -> {
                            if (new AntPathRequestMatcher(securityProperties.getH2ConsoleUrl()).matches(request)) {
                                response.setHeader(X_FRAME_OPTIONS, X_FRAME_OPTIONS_SAMEORIGIN);
                            } else {
                                response.setHeader(X_FRAME_OPTIONS, X_FRAME_OPTIONS_DENY);
                            }
                        })
                );
        return http.build();
    }

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

RequiredArgsConstructorアノテーションはfinal修飾子が付与されたメンバ変数を引数に持つコンストラクタを自動生成します。

これによって以下のようなコンストラクタを作らずともSecurityPropertiesクラスをインジェクションできるようにし、SecurityConfigクラスでSecurityPropertiesクラスを使用可能としてます。

public SecurityConfig(final SecurityProperties securityProperties) {
    this.securityProperties = securityProperties;
}

以下によってapplication.ymlのpermit-urlsとh2-console-urlプレフィックスで定義したURLについてはログイン認証をせずにアクセスできるようにしてます。

.requestMatchers(securityProperties.getPermitUrls().toArray(String[]::new)).permitAll()
 .requestMatchers(securityProperties.getH2ConsoleUrl()).permitAll()

以下によってログイン画面はapplication.ymlのlogin-urlプレフィックスに定義したURLであることを指定してます。

.loginPage(securityProperties.getLoginUrl())

以下によってログイン後の画面はapplication.ymlのhome-urlプレフィックスに定義したURLであることを指定してます。

.defaultSuccessUrl(securityProperties.getHomeUrl(), true)

以下によってh2-consoleのみcsrfトークンを無効化してます。

.csrf(csrf -> csrf
        .ignoringRequestMatchers(securityProperties.getH2ConsoleUrl())
)

以下によってh2-consoleのみiframeを使用可能にしてます。

.headers(headers -> headers
        .addHeaderWriter((request, response) -> {
            if (new AntPathRequestMatcher(securityProperties.getH2ConsoleUrl()).matches(request)) {
                response.setHeader(X_FRAME_OPTIONS, X_FRAME_OPTIONS_SAMEORIGIN);
            } else {
                response.setHeader(X_FRAME_OPTIONS, X_FRAME_OPTIONS_DENY);
            }
        })
);

以下によってパスワードの暗号化にBCryptPasswordEncoderを使用するように指定してます。

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

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

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

コメント

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