【Spring Boot】Spring Securityの使用方法をサンプルコードで解説!!

この記事は約29分で読めます。
SpringSecurityアイキャッチ

このページでは、Spring Securityの実装例をサンプルコードのみで
ご紹介していきます。

今回ご紹介するコードは、以下のGitHubにも載せています。
エディターで見れる方は、こちらをダウンロードして見たほうが見やすいかもしれません。

GitHub - tkmttkm/SpringSecurity
Contribute to tkmttkm/SpringSecurity development by creating an account on GitHub.

このページでは、詳細な説明もすべて、コードにコメントで説明していきます。
Spring Securityを用いて、ログイン認証を
データベースのデータを元に行なっていくコードとなっております。

環境・定義

build.gradle

plugins {
	id 'java'
	id 'war'
	id 'org.springframework.boot' version '3.2.4'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	//データベース接続はSpring Data JPAを用います
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	
	//Spring Security	
	implementation 'org.springframework.boot:spring-boot-starter-security'
	
	//htmlにはThymeleaf使用
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	
	//GetterやSetterの生成のためにlomboc使用
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	
	//DBはh2を使用
	runtimeOnly 'com.h2database:h2'
	
	//コンソールに取得データや発行したSQLを表示するために使用
	runtimeOnly 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4:1.16'
	
	annotationProcessor 'org.projectlombok:lombok'
	providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2'
}

tasks.named('test') {
	useJUnitPlatform()
}

application.properties

spring.application.name=SpringSecurity

#DBの接続方法やコンソールに発行したSQLや取得したデータを表示するための設定
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy

#h2の設定
spring.datasource.url=jdbc:log4jdbc:h2:mem:hogedb
spring.datasource.username=sa
spring.datasource.password=

#初期読み込みにてDBやテーブルを作成
spring.jpa.hibernate.ddl-auto=validate
spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.data-locations=classpath:data.sql

log4jdbc.log4j2.properties

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator

schema.sql

CREATE TABLE LOGIN_USER
(
  USER_ID INT NOT NULL,
  PASSWORD VARCHAR(MAX) NOT NULL,
  FIRST_NAME VARCHAR(10) NOT NULL,
  LAST_NAME VARCHAR(10) NOT NULL,
  BIRTH_DAY INT NULL,
  AUTH BIT NOT NULL,
  PRIMARY KEY(USER_ID)
);

data.sql

INSERT INTO LOGIN_USER(USER_ID, PASSWORD, FIRST_NAME, LAST_NAME, BIRTH_DAY, AUTH)
VALUES(1,'PASS','テスト', '太郎', 20240101, 1);
INSERT INTO LOGIN_USER(USER_ID, PASSWORD, FIRST_NAME, LAST_NAME, BIRTH_DAY, AUTH)
VALUES(2,'PA','テスト', '二郎', 20240101, 0);
INSERT INTO LOGIN_USER(USER_ID, PASSWORD, FIRST_NAME, LAST_NAME, BIRTH_DAY, AUTH)
VALUES(3,'pasword','テスト', '三郎', 20240101, 1);
INSERT INTO LOGIN_USER(USER_ID, PASSWORD, FIRST_NAME, LAST_NAME, BIRTH_DAY, AUTH)
VALUES(4,'pass','テスト', '花子', 20250101, 0);

message.properties

error.login.failure=認証に失敗しました。idとパスワードを再度ご確認ください。

ファイル階層

  • /
    • src/
      • main/
        • java/
          • com/example/demo/
            • config/
              • Constants.java
              • CustomAuthenticationFailureHander.java
              • SecurityConfig.java
            • controller/
              • LoginController.java
            • entity/
              • UserEntity.java
            • repository/
              • JPARepository.java
            • service/
              • CustomUserDetailService.java
            • utils/
              • CutomUserDetailsService.java
            • …..
        • resources/
          • application.properties
          • data.sql
          • log4jdbc.log4j2.properties
          • message.properties
          • shema.sql
    • build.gradle
    • ……

サンプルコード

config

SecurityConfig.java

package com.example.demo.config;

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.config.annotation.web.configuration.EnableWebSecurity;
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;

import lombok.RequiredArgsConstructor;

/**
 * @author Takumi
 * ログイン画面、ログアウト画面などを定義
 *
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

	//ログイン失敗時のハンドラークラス
	private final CustomAuthenticationFailureHandler failureHandler;

	@Bean
	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
				//ログイン設定をする
				.formLogin(login -> login
						//ログイン認証を発火するパス(ボタンを押した時に情報を渡すパス)
						.loginProcessingUrl(Constants.LOGIN_CERTIFICATION)
						//ログインページのパス
						.loginPage(Constants.LOGIN_PATH)
						//ログイン成功時のパス
						.defaultSuccessUrl(Constants.LOGIN_SUCCESS_PATH)
						//ログイン失敗時のハンドラー
						.failureHandler(failureHandler))
				//ログアウト設定
				.logout(logout -> logout
						//ログアウトするパス
						.logoutRequestMatcher(new AntPathRequestMatcher(Constants.LOGOUT_PATH))
						//ログアウト後のパス
						.logoutSuccessUrl(Constants.LOGIN_PATH)
						// セッションを無効にする
						.invalidateHttpSession(true)
						//特定のクッキーを削除
						.deleteCookies(Constants.JSESSIONID)
				)
				
				//認証なしでの許可パス設定
				.authorizeHttpRequests(authorize -> authorize
				.requestMatchers(new AntPathRequestMatcher(Constants.LOGIN_PATH)).permitAll()
				.requestMatchers(new AntPathRequestMatcher(Constants.LOGIN_CERTIFICATION)).permitAll()
				.requestMatchers(new AntPathRequestMatcher(Constants.CSS_PATH)).permitAll()
				.requestMatchers(new AntPathRequestMatcher(Constants.JS_PATH)).permitAll()
				.requestMatchers(new AntPathRequestMatcher(Constants.IMAGE_PATH)).permitAll()
				.requestMatchers(new AntPathRequestMatcher(Constants.H2_CONSOLE)).permitAll()
				.anyRequest().authenticated()
		);
		return http.build();
	}
	
	/**
	 * <pre>
	 * パスワードをハッシュ化する
	 * </pre>
	 * @return ハッシュ化されたパスワード
	 */
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

CustomAuthenticationFailureHandler.java

package com.example.demo.config;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NoArgsConstructor;

/**
 * ログイン認証失敗時のクラス
 * @author Takumi
 *
 */
@Component
@NoArgsConstructor
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

	public CustomAuthenticationFailureHandler(String failureUrl) {
		super(failureUrl);
	}
	
	/**
	 * <pre>
	 * ログイン認証失敗時のメソッド
	 * {@link Constants#LOGIN_PATH}?error=trueにリダイレクトする
	 * <pre>
	 */
	@Override
	public void onAuthenticationFailure(
			HttpServletRequest request,
			HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
	
		//指定したURLにリダイレクト
		super.setDefaultFailureUrl(Constants.LOGIN_PATH + "?" + Constants.ERROR + "=true");
		super.onAuthenticationFailure(request, response, exception);
	}
	
}

Constants.java

package com.example.demo.config;

/**
 * @author Takumi
 * 共通定数などの保存クラス
 *
 */
public class Constants {

	public static final String LOGIN_PATH = "/login";
	public static final String LOGIN_SUCCESS_PATH = "/loginSuccess";
	public static final String LOGIN_CERTIFICATION = "/loginCertification";
	public static final String LOGOUT_PATH = "/logout";

	public static final String CSS_PATH = "/css/**";
	public static final String JS_PATH = "/js/**";
	public static final String IMAGE_PATH = "/images/**";
	public static final String H2_CONSOLE = "/h2-console/**";

	public static final String TITLE_KEY = "title";

	public static final String LOGINPAGE_TITLE = "ログイン";
	public static final String LOGIN_SUCCESS_TITLE = "ログイン成功";
	
	public static final String AUTH_USER = "USER";
	public static final String AUTH_ADMIN = "ADMIN";
	
	public static final String JSESSIONID = "JSESSIONID";
	
	public static final String ERROR = "error";
	
	
	//message.properties key
	public static final String ERROR_LOGIN_FAILURE = "error.login.failure";
}

contoller

LoginController.java

package com.example.demo.controller;

import java.util.Locale;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.demo.config.Constants;
import com.example.demo.config.SecurityConfig;

@Controller
public class LoginController {

	@Autowired
	private MessageSource messageSource;
	
	private static String LOGIN_PAGE = "/login";
	private static String LOGIN_SUCCESS_PAGE = "/success";
	private static String ERROR = "error";
	

	/**
	 * <pre>
	 * デフォルトのログインページを表示
	 * {@link SecurityConfig コンフィグクラス}のloginPageに設定したパス
	 * </pre>
	 * @param model
	 * @return login.html
	 */
	@GetMapping(Constants.LOGIN_PATH)
	public String showLoginPage(ModelMap model, @RequestParam(value = Constants.ERROR, required = false) boolean error) {
		//ログイン失敗時にメッセージを出す
		if(error) {
			model.put(ERROR, messageSource.getMessage(Constants.ERROR_LOGIN_FAILURE, null, "認証に失敗しました。idとパスワードを再度ご確認ください。", Locale.JAPAN));
		}
		
		model.put(Constants.TITLE_KEY, Constants.LOGINPAGE_TITLE);
		
		return LOGIN_PAGE;
	}
	
	/**
	 * <pre>
	 * ログイン成功時
	 * {@link SecurityConfig コンフィグクラス}のdefaultSuccessUrlに設定したパス
	 * </pre>
	 * @param model
	 * @return success.html
	 */
	@GetMapping(Constants.LOGIN_SUCCESS_PATH)
	public String loginSuccess(ModelMap model) {
		
		model.put(Constants.TITLE_KEY, Constants.LOGIN_SUCCESS_TITLE);
		
		return LOGIN_SUCCESS_PAGE;
	}
	
}

entity

UserEntity.java

package com.example.demo.entity;

import com.example.demo.config.Constants;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
 * エンティティクラス。LOGIN_USERをマッピング
 * @author Takumi
 *
 */
@Entity
@Table(name="LOGIN_USER")
@Getter
@NoArgsConstructor
public class UserEntity {
	
	@Id
	@Column(name="USER_ID")
	public Integer UserId;
	
	@Column(name="PASSWORD")
	public String Password;
	
	@Column(name="FIRST_NAME")
	public String FirstName;
	
	@Column(name="LAST_NAME")
	public String LastName;
	
	@Column(name="BIRTH_DAY")
	public Integer BirthDay;
	
	@Column(name="AUTH")
	public boolean Auth;
	
	/**
	 * <pre>
	 * ユーザー認証用の権限をテーブルデータにより返すメソッド
	 * </pre>
	 * @return ユーザー認証用の権限
	 */
	public String getUserAuth() {
		if(this.Auth) {
			return Constants.AUTH_ADMIN;
		} else {
			return Constants.AUTH_USER;
		}
	}
}

repository

JPARepository.java

package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.entity.UserEntity;

/**
 * Spring Data JPA を使用してDBから情報取得するクラス
 * @author Takumi
 *
 */
public interface JPARepository extends JpaRepository<UserEntity, Integer>{}

service

CustomUserDetailsService.java

package com.example.demo.service;

import java.util.Collections;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import org.springframework.stereotype.Service;

import com.example.demo.entity.UserEntity;
import com.example.demo.repository.JPARepository;
import com.example.demo.utils.CustomUserDetails;

import io.micrometer.common.util.StringUtils;
import lombok.RequiredArgsConstructor;

/**
 * @author Takumi
 * ログイン認証時にユーザーIDより認証を行うクラス
 */
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

	private final JPARepository repository;	
	private final PasswordEncoder passwordEncoder;
	
	/**
	 * <pre>
	 * ユーザーIDからパスワードをテーブルから取得し、
	 * ログイン認証を行う
	 * </pre>
	 */
	@Override
	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		if(StringUtils.isBlank(userId)) {
			throw new UsernameNotFoundException(null);
		}
		
		Integer userID = Integer.parseInt(userId);
		//ユーザー情報の取得、認証
		List<UserEntity> allData = repository.findAll();
		for(UserEntity data: allData) {
			if(Integer.compare(userID, data.getUserId()) == 0) {
				String encodedPassword = passwordEncoder.encode(data.getPassword());
				String auth = data.getUserAuth();
				return new CustomUserDetails(userId, encodedPassword, toGrantedAuthorityList(auth));
			}
		}
		
		throw new UsernameNotFoundException(null);
	}
	
	/**
	 * <pre>
	 * Spring Securityの権限タイプをセットする
	 * </pre>
	 * @param auth_type
	 * @return 権限リスト
	 */
	private List<GrantedAuthority> toGrantedAuthorityList(String auth_type) {
		return Collections.singletonList(new SimpleGrantedAuthority(auth_type));
	}

}

utils

CustomUserDetails.java

package com.example.demo.utils;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

/**
 * ログイン認証をするクラス
 * @author Takumi
 *
 */
public class CustomUserDetails extends User {

	public CustomUserDetails(String user_no, String password, Collection<? extends GrantedAuthority> authorities) {
		super(user_no, password, authorities);
	}
	
}

templates

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
	<meta charset="UTF-8">
	<title th:text="${title}"></title>
</head>

<body>
	<!-- 認証エラーがあった際に表示 -->
	<div th:unless="${#strings.isEmpty(error)}">
		<div th:text="${error}" style="color: red;"></div>
	</div>
	
	<!--  -->
	<form method="post" th:action="@{loginCertification}">
		<!-- Spring Securityのユーザー名のname属性はデフォルトでusername -->
		<label for="username">ユーザー名</label>
		<input id="username" name="username" type="text"></input>
		
		<!-- Spring Securityのパスワードのname属性はデフォルトでpassword -->
		<label for="password">パスワード</label>
		<input id="password" name="password" type="password"></input>
		
		<button type="submit">ログイン</button>
	</form>
</body>

</html>

success.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${title}"></title>
</head>
<body>

<div>ログイン成功です!!!!おめでとう!!!!!</div>

<!-- ログアウトボタン -->
<div><a id="logout" th:href="@{logout}">ログアウト</a></div>
</body>

<script>

	const logout = document.getElementById("logout");
	
	//ログアウトボタン押下時の挙動
	logout.addEventListener("click", e => {
		e.preventDefault();
 		const result = window.confirm("ログアウトします。よろしいですか?");
 		if(result) {
			 window.location.href = e.target.href;
		 }
	});
</script>
</html>

画面イメージ

eclipseなどで実行し、
http://localhost:8080/login
にアクセスします。

ログイン

ログイン画面




このような画面が開かれます。

ユーザー名とパスワードに、data.sqlで登録した
データを入れます。

ユーザー名とパスワードは以下です。

USER_IDPASSWORD
1PASS
2PA
3password
4pass

今回は、USER_IDが1で認証します。

余談ですが、このプロジェクトでは、権限によって動作を変更することは特にしていませんが、
プロジェクトによっては、ユーザーの権限により、操作できることとできないことを分岐します。

ログイン画面入力




ユーザー名、パスワードを入力し、ログインボタンを押下。

ログイン成功画面

すると、ログイン成功画面へ遷移します。

ログアウト

ログイン成功後、ログアウトボタンを押下すると、

ログアウト




ポップアップが出てくるので、OKを押下すると、

ログイン画面

はじめのログイン画面に戻ります。

ログイン認証失敗

パスワードを間違えるなどしてログインボタンを押下すると、

ログイン失敗

このようにメッセージが表示されます。