SpringSecurity6系は5.3までと比べるとSecurityConfigの書き方が変わってるので勉強を兼ねて独自のログイン画面を用意し認証機能をjavaで作成してみました。
またMyBatisとH2 Databaseを使うことでDB構築をせずとも、手軽にDBと連携できるようにしてみました。
なお、記載内容が多くなったため、記事を2つに分けてます。
後編は以下をご参照ください。
開発環境
私が構築した時の開発環境は以下の通りとなります。
環境 | バージョン |
---|---|
Java | 17 |
SpringBoot | 3.4.3 |
SpringSecurity | 6.4.3 |
thymeleaf | 3.1.3.RELEASE |
MyBatis | 3.0.4 |
H2 Database | 2.3.232 |
lombok | 1.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
- config
- loginproject
- example
- LoginProjectApplication.java
- com
- resources
- com
- example
- loginproject
- mapper
- LoginUsersMapper.xml
- mapper
- loginproject
- example
- static
- css
- style.css
- templates
- home.html
- login.html
- css
- application.yml
- schema.sql
- com
- java
- main
- pom.xml
- src
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つに分けてます。
後編は以下をご参照ください。
コメント