はじめに
最近SpringBootを学び始めたプログラミング初学者です。
SpringSecurityを使用したDB認証を実装しようとしたのですが、思った以上に詰まってしまったので備忘録も兼ねて記事にしました。
本当に必要最小限の機能しか実装していないので、とりあえずSpringSecurityを使ったDB認証がしたい!という人向けの記事になるかと思います。
環境
- Eclipse 4.19.0
- SpringBoot 2.5.3
- Java11
- ビルドツール:Maven
- テンプレートエンジン:Thymeleaf
- O/Rマッパー:MyBatis
- DB:MySQL
SpringInitializrの設定
出来上がった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>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>Login6</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Login6</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
プロジェクトのフォルダ構成
-
UserModel.java
DBから取得したユーザ―情報を格納するEntityクラス
UserDetailsをimplementsする -
UserListMapper.java
Mapperインターフェース (MyBatisで使用するため詳細説明は省略)
-
UserListMapper.xml
Mapperインターフェースに対応するXMLファイル(MyBatisで使用するため詳細説明は省略)
-
UserService.java
サービスクラス
UserDetailsServiceをimplementsして実装する
-
MainController.java
コントローラークラス
-
Login6Application.java
SpringBootの起動クラス
-
WebSecurityConfig.java
SpringSecurityの設定を行うクラス
WebSecurityConfigurerAdapterをextendsして実装する
-
loginForm.html
ログイン認証を行う画面
-
success.html
ログイン認証に成功した時に遷移する画面
-
application.properties
DBへの接続情報を記述しているファイル
-
mybatis-config.xml
MyBatisの設定ファイル
-
pom.xml
プロジェクトのビルドに関する情報やライブラリの依存関係などが記述されているファイル
Login6Application.java
SpringBootの起動クラスです。
MyBatis用の設定を追記していること以外はデフォルトのままです。
package com.example;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
@SpringBootApplication
@MapperScan(basePackages="com.example.persistence")
public class Login6Application {
public static void main(String[] args) {
SpringApplication.run(Login6Application.class, args);
}
// MyBatisの設定
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// コンフィグファイルの読み込み
sessionFactory.setConfigLocation(new ClassPathResource("/mybatis-config.xml"));
return sessionFactory.getObject();
}
}
MainController.java
コントローラークラスです。
package com.example.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MainController {
@RequestMapping("/loginForm")
public String loginForm() {
return "loginForm";
}
@RequestMapping(value = "/loginForm", params = "error")
public String error(Model model) {
model.addAttribute("errorMsg", "ログイン認証に失敗しました");
return "loginForm";
}
@RequestMapping("/success")
public String success() {
return "success";
}
}
WebSecurityConfig.java
SpringSecurityの設定用クラスです。
詳細はコード内のコメントに記載していますが、このクラスでログイン認証の具体的な設定を行っています。
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.example.service.UserService;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
//アクセスポリシーの設定
http.authorizeRequests()
//指定したパターンごとに制限をかける
.antMatchers("/js/**", "/css/**").permitAll()//制限なし
.anyRequest().authenticated();//上記以外は制限あり
//フォーム認証の設定
http.formLogin()
/*ログインページの指定(指定なしの場合デフォルトのものが用意される)
コントローラークラスでこのURLをマッピングしておく*/
.loginPage("/loginForm")
/*ログイン処理のパス(このURLへリクエストが送られると認証処理が実行される)
ログインページのformタグのth:action属性にこのURLを指定しておく*/
.loginProcessingUrl("/login")
/*ログインページのログイン情報入力欄のname属性に以下の名称を指定する*/
.usernameParameter("user")
.passwordParameter("pass")
/*ログイン成功時に遷移するページ(trueで成功時は常にここに飛ぶ)
コントローラークラスでこのURLをマッピングしておく*/
.defaultSuccessUrl("/success", true)
/*失敗時の遷移先、アクセス制限は解除する
コントローラークラスでこのURLをマッピングしておく*/
.failureUrl("/loginForm?error").permitAll();
}
/**
* 認証するユーザー情報をデータベースからロードする処理
* @param auth 認証マネージャー生成ツール
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
///認証するユーザー情報をデータベースからロードする
//その際、パスワードはBCryptでハッシュ化した値を利用する
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
/**
* パスワードをBCryptでハッシュ化するクラス
* ハッシュ化するクラスも幾つか種類があるみたいです
* @return パスワードをBCryptで暗号化するクラスオブジェクト
*/
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
UserService.java
UserDetailsServiceをimplementsしているサービスクラスです。
実装する必要があるのはloadUserByUsernameメソッドだけで、ユーザーネーム(今回はid)を引数にユーザ情報を返す処理を記述します。
今回はO/RマッパーにMyBatisを使用しており、マッパーインタフェースのcertificateメソッドを呼んでいます。
package com.example.service;
import org.springframework.beans.factory.annotation.Autowired;
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;
import com.example.persistence.UserListMapper;
@Service
public class UserService implements UserDetailsService{
@Autowired
private UserListMapper userListMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userListMapper.certificate(username);
}
}
UserModel.java
DBから取得したユーザー情報を格納するEntityクラスです。
UserDetailsをImplementsして、UserDetailsに記述してあるメソッドを実装します。
package com.example.domain;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class UserModel implements UserDetails{
private String id;
private String pass;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//1つのユーザーがROLEを複数持つときに使うみたい
return null;
}
@Override
public String getPassword() {
//パスワードを返す
return pass;
}
@Override
public String getUsername() {
//ユーザー名(今回はid)を返す。
return id;
}
@Override
public boolean isAccountNonExpired() {
//ユーザアカウントが認証期限切れしていないかどうかを返す
//認証期限切れしている場合falseを返すが、今回は特に考慮していないので
//固定でtrueを返しておく(以下メソッドでも同様の理由でtrueを返している)
return true;
}
@Override
public boolean isAccountNonLocked() {
//ユーザアカウントがロックしていないかどうか
return true;
}
@Override
public boolean isCredentialsNonExpired() {
//ユーザアカウントの資格が認証期限切れしていないかどうか
return true;
}
@Override
public boolean isEnabled() {
//ユーザアカウントが無効になっていないか
return true;
}
}
UserListMapper.java
マッパーインターフェースクラスです。
MyBatisに関することなので説明は省略します。
package com.example.persistence;
import org.apache.ibatis.annotations.Param;
import com.example.domain.UserModel;
public interface UserListMapper {
public UserModel certificate(@Param("id") String id);
}
UserListMapper.xml
マッパーインターフェースクラスに対応する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.persistence.UserListMapper">
<select id="certificate" resultType="com.example.domain.UserModel">
SELECT id, pass FROM userlist WHERE id = #{id}
</select>
</mapper>
loginForm.html
ログイン認証を行う画面です。
UserIDとPasswordの入力欄のname属性と、formタグのth:action属性はWebSecurityConfig.javaで設定しているものを記述します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ログイン認証</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<span th:if="${errorMsg}" th:text="${errorMsg}"></span><br>
UserID:<input type="text" name="user" /><br>
Password:<input type="password" name="pass" /><br>
<input type="submit" value="認証"/>
</form>
</body>
</html>
success.html
ログイン認証が成功した時に遷移する画面です。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>認証成功</title>
</head>
<body>
認証成功!!!
</body>
</html>
application.properties
DBへの接続情報を記述しています。
# DBのドライバ設定
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 接続用URL
spring.datasource.url=jdbc:mysql://XXX/XXX
# ユーザ名
spring.datasource.username=XXX
# パスワード
spring.datasource.password=XXX
mybatis-config.xml
MyBatisの設定情報を記述しています。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC
"-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
</configuration>
テーブル定義
参考までにテーブル情報を載せておきます。
ちなみに、画面で入力されたパスワードはハッシュ化され、予めDBに登録してあるパスワード(のハッシュ値)と比較されます。
そのため、DBに格納するパスワードは予めハッシュ化しておく必要があります。
今回ハッシュ化に使用したBCryptPasswordEncoderクラスを利用して予めハッシュ化したパスワードをDBに登録しておくといいでしょう。
CREATE TABLE userlist (
id int NOT NULL,
pass varchar(60) DEFAULT NULL,
PRIMARY KEY (`id`)
)
参考にした記事
SpringSecurity Reference
spring boot security + DB認証を試した時のポイント
最初のSpring Security - フォーム認証&画面遷移
Spring Security 使い方メモ 認証・認可
Spring-Bootでログイン機能を実装してみる