spring securityを試す

こんにちわ、猫好きリーマンのほげPGです。
コテツ(飼い猫)が旅立って、もうすぐ一年になります。最近、子どもが『コテツの音が聞こえる』と言っていて…ちゃんと覚えていてくれるのは嬉しいです。
今回は以下の構成を試します。
springboot 2.7 + spring security 5 + mybatis + logback
springboot が2なのはtomcat9向けの構成としているためです。
JavaEEとJakartaEEの境目なのでspringbootを3にするとtomcat9では動かなくなります。
1 全体

1 リクエスト
1.1 spring-securityの設定に基づきLoginControllerの/loginへ
1.2 テンプレートauth.htmlからhtmlを返却
2 username, passwordを送信
2.1 LoginService.loadUserByUsername()が呼ばれる
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(requests -> requests
.antMatchers("/static/**", "/login*", "/logout*","/hoge").permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/login")
.successForwardUrl("/login-success")
.failureForwardUrl("/login-failure")
.permitAll())
.logout(logout -> logout
.deleteCookies()
.invalidateHttpSession(true)
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.permitAll())
.exceptionHandling(handling -> handling
.accessDeniedPage("/error/403"));
return http.build();
}
2.2 UserRepositoryのgetを呼ぶ
2.3 UserMapperを通してusersテーブルからデータ取得
2.4 spring-securityでパスワードチェックを行い、不一致ならng、一致ならokへ
2.5 認証OK時は1のリクエストにリダイレクト
2詳細
spring-security定義
WebSecurityConfig.java
認証関連リクエスト処理
LoginController.java
@Controller
@Slf4j
public class LoginController {
@Data
public static class LoginForm {
private String username;
private String password;
}
@ModelAttribute("loginForm")
public LoginForm loginForm() {
return new LoginForm();
}
@GetMapping("/login")
public String login(Model model) {
log.debug("called.");
return "login";
}
@PostMapping("/login-failure")
public String loginFailure(
HttpSession session,
HttpServletRequest request,
Model model,
@ModelAttribute("loginForm") LoginForm form,
BindingResult result) {
log.debug("called. {}", form);
AuthenticationException authExcp = (AuthenticationException) request.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
log.debug("cause: " + authExcp);
if (authExcp instanceof InternalAuthenticationServiceException iase) {
result.rejectValue("username", "system.error", new Object[] {iase}, null);
} else {
result.rejectValue("username", "auth.failed", new Object[] {authExcp.getMessage()}, null);
}
return "login";
}
private RequestCache requestCache = new HttpSessionRequestCache();
@PostMapping("/login-success")
public String loginSuccess(
HttpServletRequest request,
@Service
@Slf4j
@RequiredArgsConstructor
public class LoginService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.debug("called. {}", username);
User user = userRepository.findByName(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return LoginUser.create(user);
}
}
ログインユーザの取得
LoginService.java
ユーザ情報の取得
UserRepository.java
@Repository
@Slf4j
@RequiredArgsConstructor
public class UserRepository {
private final UsersMapper usersMapper;
@Transactional(readOnly = true)
public User findByName(String name) {
List<User> userList = usersMapper.select(name);
if (userList.isEmpty()) {
return null;
} else {
return userList.get(0);
}
}
}
@Mapper
public interface UsersMapper {
List<User> select(String name);
}
sqlマッパー
UsersMapper.java
Sql定義
UsersMapper.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="jp.co.ois.hoge.springsecurity.mapper.UsersMapper">
<select id="select" resultType="jp.co.ois.hoge.springsecurity.model.User">
SELECT
id, name, password, role
FROM
users
<where>
<if test="name != null">
AND name = #{name}
</if>
</where>
ORDER BY
id DESC
</select>
</mapper>
@Data
public class User {
private int id;
private String name;
private String password;
private String role;
}
User.java
LoginUser.java
@ToString
public class LoginUser implements UserDetails, CredentialsContainer {
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
@Getter
private final int id;
public LoginUser(String username, String password, Collection<? extends GrantedAuthority> authorities, int id) {
this.username = username;
this.password = password;
this.authorities = authorities;
this.id = id;
}
@Override
public void eraseCredentials() {
this.password = null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
public static LoginUser create(User user) {
List<GrantedAuthority> authorities = new ArrayList<>(1);
authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
return new LoginUser(user.getName(), user.getPassword(), authorities, user.getId());
}
@Override
public boolean isAccountNonExpired() {
return true;
ログイン画面
login.html
<!DOCTYPE html>
<!--/* ログイン */-->
<html xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
lang="ja">
<head>
<meta th:replace="~{common::meta_header}"/>
</head>
<body>
<header th:replace="~{common::header}"/>
<main class="mx-lg-4">
<section id="content" class="lh-1">
<div class="col-md-4">
<h3>ログイン</h3>
<form method="POST" th:action="@{/login}" th:object="${loginForm}" >
<div class="col-12 mt-3">
<label for="username" th:text="#{auth.username}" class="form-label">ユーザ名</label>
<input type="text" th:field="*{username}" class="form-control" required>
<span th:if="${#fields.hasErrors('username')}" class="text-danger" role="alert">
<strong th:errors="*{username}">エラーユーザ名</strong>
</span>
</div>
<div class="col-12 mt-3">
<label for="password" th:text="#{auth.password}" class="form-label">パスワード</label>
<input type="password" th:field="*{password}" class="form-control" required>
<span th:if="${#fields.hasErrors('password')}" class="text-danger" role="alert">
<strong th:errors="*{password}">エラーパスワード</strong>
</span>
</div>
<button th:text="#{auth.login}" class=" w-100 btn btn-primary btn-lg mt-4" type="submit">ログイン</button>
</form>
</div>
</section>
</main>
<footer th:replace="~{common::footer}"/>
</body>
</html>
共通ヘッダーなど
common.html
<!DOCTYPE html>
<!--/* 共通フラグメント定義 */-->
<html xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
lang="ja">
<head>
<!-- 共通メタ -->
<th:block th:fragment="meta_header">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" th:content="${_csrf.token}"/>
<title th:text="#{app.name}">タイトル</title>
<link rel="stylesheet" th:href="@{/static/css/bootstrap.min.css}">
<script th:src="@{/static/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/static/js/jquery-3.7.1.min.js}"></script>
</th:block>
</head>
<body>
<!-- 共通ヘッダー -->
<header class="bd-header bg-dark py-3 d-flex align-items-stretch border-bottom border-dark">
<div class="container-fluid d-flex align-items-center">
<h1 class="d-flex align-items-center fs-4 text-white mb-0">
<a class="logo" th:href="@{/}"><img th:src="@{/static/images/ois_logo.jpg}" height="30px"></a>
<div th:text="#{app.name}">hoge-springsecurity</div>
</h1>
<form id="logout-form" th:action="@{/logout}" method="post" class="ms-auto link-light">
<a id="logout" th:text="#{auth.logout}">Logout</a>
</form>
</div>
<script>
$(window).on("load", function() {
$("#logout").on('click', function(e) {
$('#logout-form').submit();
});
});
</script>
</header>
<main>
<div class="container">
</div>
</main>
@Controller
@Slf4j
public class HogeController {
@GetMapping("/")
public String home() {
log.debug("called.");
return "redirect:/top";
}
@GetMapping("/top")
public String top(@AuthenticationPrincipal LoginUser user) {
log.debug("called. {}", user);
return "top";
}
}
HogeController.java
top.java
<!DOCTYPE html>
<!--/* トップ */-->
<html xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
lang="ja">
<head>
<meta th:replace="~{common::meta_header}"/>
</head>
<body>
<header th:replace="~{common::header}"/>
<main class="mx-lg-4">
<section id="content" class="lh-1">
<div class="col-md-4">
<h3>トップ</h3>
</div>
</section>
</main>
<footer th:replace="~{common::footer}"/>
</body>
</html>
DB定義
-- データベース設定
USE hoge_db;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`password` varchar(255) DEFAULT NULL,
`role` varchar(100) NOT NULL,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
`updated` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `users_name_unique` (`name`)
) comment 'users' ;
-- 初期データ
INSERT INTO users ( name ,password ,role ,created ,updated ) VALUES ( 'ois' ,'{bcrypt}$2a$10$L9EzxwQQXT3vKNeGUGXLOeW30QmcbFiwNu7RnvAXdCc.rojQRSYbu' ,'ADMIN' ,now() ,now() );
pom.xml
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jp.co.ois</groupId>
<artifactId>hoge-springsecurity</artifactId>
<packaging>war</packaging>
<version>1.0.0-SNAPSHOT</version>
<name>hoge-springsecurity</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<properties>
<java.version>17</java.version>
<tomcat.version>9.0.108</tomcat.version>
</properties>
<dependencies>
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
3動作確認
起動させた後、以下URLを開く
http://localhost:9080/hoge-springsecurity

ユーザ名:ois、パスワード:yokohama を入力し、リターンキー押下

プロジェクト一式を添付しておきます。
