このページでは、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
- …..
- config/
- com/example/demo/
- resources/
- application.properties
- data.sql
- log4jdbc.log4j2.properties
- message.properties
- shema.sql
- java/
- main/
- build.gradle
- ……
- src/
サンプルコード
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_ID | PASSWORD |
---|---|
1 | PASS |
2 | PA |
3 | password |
4 | pass |
今回は、USER_IDが1で認証します。
余談ですが、このプロジェクトでは、権限によって動作を変更することは特にしていませんが、
プロジェクトによっては、ユーザーの権限により、操作できることとできないことを分岐します。
ユーザー名、パスワードを入力し、ログインボタンを押下。
すると、ログイン成功画面へ遷移します。
ログアウト
ログイン成功後、ログアウトボタンを押下すると、
ポップアップが出てくるので、OKを押下すると、
はじめのログイン画面に戻ります。
ログイン認証失敗
パスワードを間違えるなどしてログインボタンを押下すると、
このようにメッセージが表示されます。