はじめに
Spring boot3でサインイン/サインアウト/サインアップを行うだけの最小単位の機能を持つプロジェクト作成する。
環境
- Java 21
- Spring Framework 6.1.8
- Spring boot 3.3.0
- Spring Security 6.3.0
- thymeleaf 3.1.2
- lombok 1.18.32
- mysql-connector-j 8.3.0
- MySQL 8.4.0
その他
- サインイン時の認証方法はユーザーパスワード方式(DB認証)とする
DB
usersテーブル
usersテーブル
CREATE TABLE users (
id int NOT NULL AUTO_INCREMENT,
user_name varchar(64) NOT NULL,
user_id varchar(64) NOT NULL,
password varchar(256) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY UK_01 (user_id)
)
パッケージ(com.example.sample)
Controller
IndexController.java
IndexController.java
package com.example.sample.controller;
@Controller
public class IndexController {
@GetMapping("/")
public String get() {
return "index";
}
}
SignInController.java
SignInController.java
package com.example.sample.controller;
@Controller
public class SignInController {
@GetMapping("/signin")
public String get( SigninForm signinForm ) {
SecurityContext securityContext = SecurityContextHolder.getContext();
if(securityContext != null) {
Authentication authentication = securityContext.getAuthentication();
if(authentication != null) {
if( authentication instanceof UsernamePasswordAuthenticationToken) {
return "redirect:/";
}
}
}
return "signin";
}
@GetMapping(value="/signin", params="error")
public String getError(SigninForm signinForm ) {
return "signin";
}
@GetMapping(value="/signin", params="signout")
public String getSignout(SigninForm signinForm ) {
return "signin";
}
}
SignOutController.java
SignOutController.java
package com.example.sample.controller;
@Controller
@RequiredArgsConstructor
public class SignOutController {
@GetMapping("/signout")
public String get() {
return "signout";
}
}
SignUpController.java
SignUpController.java
package com.example.sample.controller;
@Controller
@RequiredArgsConstructor
public class SignUpController {
private final UsersService service;
@GetMapping("/signup")
public String get(SignupForm signupForm) {
SecurityContext securityContext = SecurityContextHolder.getContext();
if(securityContext != null) {
Authentication authentication = securityContext.getAuthentication();
if(authentication != null) {
if( authentication instanceof UsernamePasswordAuthenticationToken) {
return "redirect:/";
}
}
}
return "signup";
}
@PostMapping("/signup")
public String post(Model model, @Valid SignupForm signupForm, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
return "signup";
}
if( !signupForm.getPassword().equals(signupForm.getPasswordRetype())) {
model.addAttribute("msg", "パスワードが一致しません。");
return "signup";
}
UserEntity userEntity = service.getByUserId(signupForm.getUserId());
if( userEntity != null ) {
model.addAttribute("msg", "ユーザーIDはすでに使用されています。");
return "signup";
}
userEntity = service.save(signupForm);
return "redirect:/signin";
}
}
Entity
UserEntity.java
UserEntity.java
package com.example.sample.entity;
@Entity
@Table(name="users")
@Getter
@Setter
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id")
private Integer id;
@Column(name="user_name")
private String userName;
@Column(name="user_id")
private String userId;
@Column(name="password")
private String password;
}
Form
SigninForm.java
SigninForm.java
package com.example.sample.form;
@Data
public class SigninForm {
@NotBlank(message="入力必須です")
@Size(max = 64)
private String userId;
@NotBlank(message="入力必須です")
@Size(max = 256)
private String password;
}
SignupForm.java
SignupForm.java
package com.example.sample.form;
@Data
public class SignupForm {
@NotBlank(message="入力必須です")
@Size(max = 64)
private String userName;
@NotBlank(message="入力必須です")
@Size(max = 64)
private String userId;
@NotBlank(message="入力必須です")
@Size(max = 256)
private String password;
@NotBlank(message="入力必須です")
@Size(max = 256)
private String passwordRetype;
}
Repository
UsersRepository.java
UsersRepository.java
package com.example.sample.repository;
public interface UsersRepository extends JpaRepository<UserEntity, Integer> {
UserEntity findByUserId(String userId);
}
Security
SampleSecurityConfig.java
SampleSecurityConfig.java
package com.example.sample.security;
@Configuration
@EnableWebSecurity
public class SampleSecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/signin", "/signup").permitAll()
.anyRequest().authenticated()
);
http.formLogin(login -> login
.loginPage("/signin")
.defaultSuccessUrl("/")
.loginProcessingUrl("/signin")
.failureUrl("/signin?error")
.usernameParameter("userId")
.passwordParameter("password")
);
http.logout(logout -> logout
.logoutUrl("/signout")
.logoutSuccessUrl("/signin?signout")
);
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
SampleUserDetails.java
SampleUserDetails.java
package com.example.sample.security;
public class SampleUserDetails implements UserDetails {
private UserEntity userEntity;
public SampleUserDetails(UserEntity userEntity) {
this.userEntity = userEntity;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.userEntity.getPassword();
}
@Override
public String getUsername() {
return this.userEntity.getUserId();
}
public UserEntity getUserEntity() {
return this.userEntity;
}
}
SampleUserDetailsService.java
SampleUserDetailsService.java
package com.example.sample.security;
@Service
@RequiredArgsConstructor
public class SampleUserDetailsService implements UserDetailsService {
private final UsersRepository repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if( username == null || username.isEmpty() ) {
throw new UsernameNotFoundException("user not found.");
}
UserEntity userEntity = repository.findByUserId(username);
if( userEntity == null) {
throw new UsernameNotFoundException("user not found.");
}
return new SampleUserDetails(userEntity);
}
}
Service
UsersService.java
UsersService.java
package com.example.sample.service;
@Service
@RequiredArgsConstructor
public class UsersService {
private final UsersRepository repository;
private final PasswordEncoder passwordEncoder;
public UserEntity getByUserId(String userId) {
return repository.findByUserId(userId);
}
public UserEntity save(SignupForm signupForm) {
UserEntity userEntity = new UserEntity();
userEntity.setUserName(signupForm.getUserName());
userEntity.setUserId(signupForm.getUserId());
userEntity.setPassword( passwordEncoder.encode(signupForm.getPassword()));
return repository.saveAndFlush(userEntity);
}
}
sample/src/main/resources
templates
index.html
index.html
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<header>
<div>
ログインユーザー名:<span sec:authentication="name"></span>
</div>
<div>
<a th:href="@{/signout}" th:text="サインアウト"></a>
</div>
</header>
<main>
<h1>Index</h1>
</main>
<footer></footer>
</body>
</html>
signin.html
signin.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>サインイン</title>
</head>
<body>
<header></header>
<main>
<h1>サインイン</h1>
<div th:if="${param.error != null and session.containsKey('SPRING_SECURITY_LAST_EXCEPTION')}">
<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span>
</div>
<form method="post" th:action="@{/signin}" th:object="${signinForm}">
<div>
<label>ユーザーID</label>
<input type="text" th:field="*{userId}">
<div th:if="${#fields.hasErrors('userId')}" th:errors="*{userId}" ></div>
</div>
<div>
<label>パスワード</label>
<input type="password" th:field="*{password}">
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" ></div>
</div>
<input type="submit" value="サインイン">
</form>
<div>
<a th:href="@{/signup}">サインアップへ</a>
</div>
</main>
<footer></footer>
</body>
</html>
signout.html
signout.html
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity">
<head>
<meta charset="UTF-8">
<title>サインアウト</title>
</head>
<body>
<header>
<div>
ログインユーザー名:<span sec:authentication="name"></span>
</div>
<div>
<a th:href="@{/signout}" th:text="サインアウト"></a>
</div>
</header>
<main>
<h1>サインアウト</h1>
<form method="post" th:action="@{/signout}" >
<p><button>サインアウト</button></p>
</form>
</main>
<footer></footer>
</body>
</html>
signup.html
signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>サインアップ</title>
</head>
<body>
<header></header>
<main>
<h1>サインアップ</h1>
<div th:Text="${msg}"></div>
<form method="post" th:action="@{/signup}" th:object="${signupForm}">
<div>
<label>ユーザー名</label>
<input type="text" th:field="*{userName}">
<div th:if="${#fields.hasErrors('userName')}" th:errors="*{userName}"></div>
</div>
<div>
<label>ユーザーID</label>
<input type="text" th:field="*{userId}">
<div th:if="${#fields.hasErrors('userId')}" th:errors="*{userId}" ></div>
</div>
<div>
<label>パスワード</label>
<input type="password" th:field="*{password}">
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" ></div>
</div>
<div>
<label>パスワード(確認用)</label>
<input type="password" th:field="*{passwordRetype}">
<div th:if="${#fields.hasErrors('passwordRetype')}" th:errors="*{passwordRetype}" ></div>
</div>
<input type="submit" value="登録">
</form>
<div>
<a th:href="@{/signin}">サインインへ</a>
</div>
</main>
<footer></footer>
</body>
</html>
application.properties
application.properties
#
spring.application.name=sample
#
spring.datasource.url=jdbc:mysql://ホスト名:3306/DB名
spring.datasource.username=ユーザ名
spring.datasource.password=パスワード
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
#* spring.jpa.show-sql=true
#
spring.jpa.open-in-view=false
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.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sample</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</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.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>