はじめに
必要最低限のログイン機能のみを実装しています。ソースファイルはgithubに載せています。
環境構築
pleiadesからpleiades All in One EclipseのJavaを使ってインストールしました。Java開発に必要なものをまとめてインストールできてとても便利なのでおすすめです。
開発環境
- Eclipse
- Java17
- Maven
- Spring Tools
- Spring boot
- Spring Security
- PostgreSQL 16
データベース作成
PostgreSQLで以下のカラムを持つテーブルを作成します。
下記を入力して▶を押すとテーブルが作成できます。テーブル名xxxxは好きなものに変えてください。今回のソースコードではtestにしています。
下記のユーザーをテーブルに登録しておきます。
id | password |
---|---|
user | $2a$10$cFLRib1xp1u9JqeWuNawbe0NpLaTQP4/Klx6.QCEhNE7D/RryTRC. |
INSERT INTO xxxx (id, password)
VALUES ('user', '$2a$10$cFLRib1xp1u9JqeWuNawbe0NpLaTQP4/Klx6.QCEhNE7D/RryTRC.');
新規プロジェクト作成
左上 ファイル(F)>新規(N)>Springスターター・プロジェクト(Spring Initializr)を開き、下記の設定で[次へ]をクリック。
プロジェクトの実行
pom.xmlを開き、下記を追加します。
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
次に、application.propertiesに内容を追加します。
上4行はPostgreSQLに接続するための設定です。
パスワードは自分で設定したものに変更します。
それ以外はデフォルトであれば下記のように設定されています。
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=postgresのパスワード
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
次に、プロジェクトを右クリック>実行>5 Maven Installをクリックして実行します。
コンソールで下記のようにBUILD SUCCESSと表示されれば成功です。
次に、プロジェクトを右クリック>実行>9 Spring Bootアプリケーションをクリックして実行します。
http://localhost:8080/ にアクセスするとデフォルトのログイン画面が表示されます。
今後認証の設定を行うことでログインする必要はなくなりますが、こちらの記事にUsernameとPasswordの設定方法を記載しています。
ログイン機能の実装
ここから、ログイン機能の実装をしていきます。
プロジェクト構成
プロジェクトには、デフォルトでフォルダが作成されていますが、フォルダごとに格納するファイルが決まっています。以下のサイトが参考になります。
https://qiita.com/aaaaaayyymmm/items/f5458d2302c11202136d
View
トップページ
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<header>
会員登録
<a href="/loginPage">ログイン</a>
</header>
<h1>Index</h1>
</body>
</html>
ログイン用ページ
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>ログイン</title>
</head>
<body>
<h1>ログイン</h1>
<div th:if="${param.error}" > <!-- リクエストパラメータの判定 -->
<div>
UserNameまたは、Passwordが違います。 <!-- 認証エラーメッセージ -->
</div>
</div>
<form method="post" th:action="@{/authenticate}"> <!-- ログイン処理のパスを指定 -->
<label>UserName:</label>
<input type="text" id="userName" name="userName"> <!-- ユーザ名の入力部 -->
<br>
<label>Password:</label>
<input type="password" id="password" name="password"> <!-- パスワードの入力部 -->
<br>
<input type="submit" id="submit" value="ログイン">
</form>
</body>
</html>
ログイン後ページ
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Home</title>
</head>
<body>
<h1>Home</h1>
<form method="post" th:action="@{/logout}">
<input type="submit" value="ログアウト">
</form>
</body>
</html>
Controller
Mapping情報を受け取ってページを表示します。
package com.bootsample.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class IndexController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String helloWorld(Model model) {
return "index";
}
}
package com.bootsample.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/loginPage")
String loginPage() {
return "login";
}
}
package com.bootsample.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/home")
public String home() {
return "home";
}
}
Config
package com.bootsample.config;
//import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 認可の設定
http.authorizeHttpRequests(authz -> authz
.requestMatchers("/").permitAll() //ページは全ユーザからのアクセスを許可
.requestMatchers("/loginPage").permitAll() //loginPageは、全ユーザからのアクセスを許可
.anyRequest().authenticated() //上記ページ以外は認証を求める
);
// ログイン設定
http.formLogin(login -> login //フォーム認証の有効化
.loginPage("/loginPage") //ログインフォームを表示するパス
.loginProcessingUrl("/authenticate") //フォーム認証処理のパス
.usernameParameter("userName") //ユーザ名のリクエストパラメータ名
.passwordParameter("password") //パスワードのリクエストパラメータ名
.defaultSuccessUrl("/home") //認証成功時に遷移するデフォルトのパス
.failureUrl("/loginPage?error=true") //認証失敗時に遷移するパス
);
// ログアウト設定
http.logout(logout -> logout
.logoutSuccessUrl("/loginPage") //ログアウト成功時に遷移するパス
.permitAll() //全ユーザに対してアクセスを許可
);
return http.build();
}
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Entity
package com.bootsample.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "test")
public class MyUser {
@Id
private String id;
private String password;
}
Repository
package com.bootsample.repository;
import org.springframework.stereotype.Repository;
import com.bootsample.entity.MyUser;
@Repository
public interface UserDao {
MyUser findUserByUserName(String userName);
}
package com.bootsample.repository;
import java.util.Map;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import com.bootsample.entity.MyUser;
@Repository
public class UserDaoImpl implements UserDao {
private final JdbcTemplate jdbcTemplate;
public UserDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* userNameを検索条件にSELECT文を実行して、DBに登録されているユーザを検索する
* @param userName
* @return User
*/
@Override
public MyUser findUserByUserName(String userName) {
String sql = "SELECT id, password FROM test WHERE id = ?";
//ユーザを一件取得
Map<String, Object> result = jdbcTemplate.queryForMap(sql, userName);
// Entityクラス(User型)に変換
MyUser user = convMapToUser(result);
return user;
}
/**
* SQL SELECT文を実行した結果(Map<String, Object>)をUser型に変換する
* @param Map<String, Object>
* @return User
*/
private MyUser convMapToUser(Map<String, Object> map) {
MyUser user = new MyUser();
user.setId((String) map.get("username"));
user.setPassword((String) map.get("password"));
return user;
}
}
Service
package com.bootsample.service;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import com.bootsample.entity.MyUser;
public class AccountUserDetails implements UserDetails {
private final MyUser myUser;
public AccountUserDetails(MyUser myUser) {
this.myUser = myUser;
}
public MyUser getUser() { // --- (1) Entityである MyUserを返却するメソッド
return myUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { // --- (3) ユーザに与えられている権限リストを返却するメソッド
return AuthorityUtils.createAuthorityList("ROLE_USER");
}
@Override
public String getPassword() { // --- (4) 登録されているパスワードを返却するメソッド
return this.myUser.getPassword();
}
@Override
public String getUsername() { // --- (5) ユーザ名を返却するメソッド
return this.myUser.getId();
}
@Override
public boolean isAccountNonExpired() { // --- (6) アカウントの有効期限の状態を判定するメソッド
return true;
}
@Override
public boolean isAccountNonLocked() { // --- (7) アカウントのロック状態を判定するメソッド
return true;
}
@Override
public boolean isCredentialsNonExpired() { // --- (8) 資格情報の有効期限の状態を判定するメソッド
return true;
}
@Override
public boolean isEnabled() { // --- (9) 有効なユーザかを判定するメソッド
return true;
}
}
package com.bootsample.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.bootsample.entity.MyUser;
import com.bootsample.repository.UserDaoImpl;
@Service
public class AccountUserDetailsService implements UserDetailsService {
@Autowired
private UserDaoImpl userDaoImpl;
public UserDetails loadUserByUsername(String userName)
throws UsernameNotFoundException{
if(userName == null || "".equals(userName)) {
throw new UsernameNotFoundException(userName + "is not found");
}
try {
MyUser myUser = userDaoImpl.findUserByUserName(userName);
if (myUser != null) {
return new AccountUserDetails(myUser); // --- (2) UserDetailsの実装クラスを生成
} else {
throw new UsernameNotFoundException(userName + "is not found");
}
} catch(EmptyResultDataAccessException e) {
throw new UsernameNotFoundException(userName + "is not found");
}
}
}