LoginSignup
3
2

SpringSecurity+BCrypt+PostgreSQL でログイン機能実装

Posted at

はじめに

必要最低限のログイン機能のみを実装しています。ソースファイルはgithubに載せています。

環境構築

pleiadesからpleiades All in One EclipseのJavaを使ってインストールしました。Java開発に必要なものをまとめてインストールできてとても便利なのでおすすめです。

開発環境

  • Eclipse
  • Java17
  • Maven
  • Spring Tools
  • Spring boot
  • Spring Security
  • PostgreSQL 16

データベース作成

PostgreSQLで以下のカラムを持つテーブルを作成します。
image.png

pgAdmin4でQuery Toolを開きます。
image.png

下記を入力して▶を押すとテーブルが作成できます。テーブル名xxxxは好きなものに変えてください。今回のソースコードではtestにしています。
image.png

下記のユーザーをテーブルに登録しておきます。

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)を開き、下記の設定で[次へ]をクリック。
image.png

下画像の依存関係にチェックを入れて[完了]をクリック。
image.png

プロジェクトの実行

pom.xmlを開き、下記を追加します。

		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>

次に、application.propertiesに内容を追加します。
上4行はPostgreSQLに接続するための設定です。
パスワードは自分で設定したものに変更します。
それ以外はデフォルトであれば下記のように設定されています。

application.properties
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をクリックして実行します。
image.png

コンソールで下記のようにBUILD SUCCESSと表示されれば成功です。
image.png

次に、プロジェクトを右クリック>実行>9 Spring Bootアプリケーションをクリックして実行します。
image.png

http://localhost:8080/ にアクセスするとデフォルトのログイン画面が表示されます。
image.png
今後認証の設定を行うことでログインする必要はなくなりますが、こちらの記事にUsernameとPasswordの設定方法を記載しています。

ログイン機能の実装

ここから、ログイン機能の実装をしていきます。

プロジェクト構成

image.png
プロジェクトには、デフォルトでフォルダが作成されていますが、フォルダごとに格納するファイルが決まっています。以下のサイトが参考になります。
https://qiita.com/aaaaaayyymmm/items/f5458d2302c11202136d

View

トップページ

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
   <header>
	   会員登録
	   <a href="/loginPage">ログイン</a>
   </header>
   <h1>Index</h1>
</body>
</html>

ログイン用ページ

login.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>

ログイン後ページ

home.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情報を受け取ってページを表示します。

IndexController.java
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";
    }
}
LoginController.java
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";
    }
}
HomeController.java
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

SecurityConfig.java
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

MyUser.java
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

UserDao.java
package com.bootsample.repository;

import org.springframework.stereotype.Repository;

import com.bootsample.entity.MyUser;

@Repository
public interface UserDao {
    MyUser findUserByUserName(String userName);
}
UserDaoImpl.java
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

AccountUserDetails.java
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;
    }
}
AccountUserDetailsService.java
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");
    	}
        
    }
}
3
2
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
3
2