1
1

Spring Boot 3 サインイン/サインアウト/サインアップのみ行うサンプル

Last updated at Posted at 2024-06-08

はじめに

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>
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1