(旧)【SpringBoot】Spring Security を用いてDBにアクセスして認証を行う

Spring Securityの設定方法についての備忘です。
※本記事は、Spring Security 5.7以前の書き方で行っています。5.7以降での書き方との差分は[**こちら**](https://qiita.com/MMK3/items/589509a4e235209bff6c)の記事に記載しています。

## 構成
構成は下記のとおりです
※このあと記述するコードの部分のみを抜粋しております。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711483/863539e7-8d51-a414-b79a-860c0665b080.png)

## Spring Security の設定を行うConfigクラスの作成
#### ✓ ポイント
- ``WebSecurityConfigurerAdapter``クラスを継承してSpringSecurityの設定を行うクラスを自作する。
- ``@EnableWebSecurity`` アノテーションをクラスにつけることで、WebSecurity関連の設定であることを示す。
- ``configure(HttpSecurity http)``のメソッドをオーバーライドして、URLごとにセキュリティを決める。

~~~java:SecurityConfig.java
package com.example.config;

import文 省略

@EnableWebSecurity // WebSecurity関連の設定であること示すアノテーション
public class SecurityConfig extends WebSecurityConfigurerAdapter{ // WebSecurityConfigurerAdapterクラスを継承する

    // config(HttpSecurity http)をオーバーライドして、主にURLごとに異なるセキュリティ設定を行う
    @Override
	protected void config(HttpSecurity http)throws Exception{ 
      http.authorizeRequests()
           // URLごとに、認証の要・不要の定義を行う
          .antMatchers("/login/**", "/img/**", "/css/**", "/js/**").permitAll() // ログイン画面、imageフォルダ、cssフォルダ、jsフォルダは認証が不要である
          .anyRequest().authenticated() //上記以外は認証が必要である
      
      // ログイン関連の設定
          .and()
          .formLogin() //認証方式は、フォーム認証であること
          .usernameParameter("name") // usernameのパラメータを独自定義（デフォルトは、username）
          .passwordParameter("password") // passwordのパラメータを独自定義（デフォルトはpassword）
          .loginProcessingUrl("/login").permitAll() // ここで指定したURLがリクエストされるとログイン認証の処理を行う
          .loginPage("/login") // ログインページを定義（これで自作したページをログインページとして使用できる）
          .failureUrl("/login?failed") // ログインに失敗したときのURL（デフォルトは?error）
          .defaultSuccessUrl("/top") // ログインに成功したときのURL

      // ログアウト関連の定義
          .and()
          .logout()
          .logoutUrl("/logout") // ログアウトURLの定義
          .logoutSuccessUrl("/login") // ログアウト成功後の画面（今回はログインページに遷移するよう定義）
          ;

    }

}

~~~

## entityの作成
User情報を格納しているテーブルに対応するentityクラスを作成。
ここでは、usersテーブルに対応するuserクラスを作成する。

~~~java:User.java
package com.example.entity;

import文省略

public class User {
	
	public String id;
	public String name;
	public String password;
	public String authority;
	
	getterやsetter等は省略

}

~~~

## UserDetailsの作成
UserDetailsとは、ユーザー名、パスワード、権限などの情報を保持するもの

**● Springが用意している標準の実装で問題ない場合**
┗：[Userクラスを継承してUserDetailsを作成する](#userクラスを継承してuserdetailsを作成する)

**● Springが用意している標準の実装では要件が満たせない場合**
┗：[UserDetailsインターフェイスを実装してUserDetailsを作成する](#UserDetailsインターフェイスを実装してUserDetailsを作成する)


### Userクラスを継承してUserDetailsを作成する
#### ✓ ポイント
- ``User（org.springframework.security.core.userdetails.User）``を継承することでUserDetailsの作成の際の記述を減らせる

~~~java:CustomUserDetails.java (Userクラスを継承)
package com.example.authentication;

// これを継承することで、UserDetails作成の際に記述量を減らせる
import org.springframework.security.core.userdetails.User;

その他inport文省略

public class CustomUserDetails extends User {

	// username,password,authoritiesを引数に取るコンストラクタを用意
	public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}

}
~~~

###  UserDetailsインターフェイスを実装してUserDetailsを作成する
#### ✓ ポイント
- ``UserDetails（org.springframework.security.core.userdetails.UserDetails）``を実装することでUserクラスを継承して作成するときよりも細やかなカスタマイズができる。

~~~java:CustomUserDetails.java (UserDetailsインターフェイスを実装)
package com.example.authentication;

public class CustomUserDetails implements UserDetails {

  // 先ほど作成したentity.Userをフィールドに持ってくる
  private final User user;
	
	public CustomUserDetails(User user) {
		super();
		this.user = user;
	}
	
	public User getUser() {
		return user;
	}

	// 権限を返す
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return AuthorityUtils.createAuthorityList("ROLE_" + user.getAuthority());
	}

	// passwordを返す
	@Override
	public String getPassword() {
		return user.getPassword();
	}

	// nameを返す
	@Override
	public String getUsername() {
		return user.getName();
	}

	// ユーザーのアカウントの有効期限が切れているかどうかを示す。今回は有効期限は設定しないので、デフォルトでtrueに設定。
	@Override
	public boolean isAccountNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}
	
	// ユーザーのアカウントがロックされているかかどうかを示す。今回はロックは設定しないので、デフォルトでtrueに設定。
	@Override
	public boolean isAccountNonLocked() {
		// TODO Auto-generated method stub
		return true;
	}
	
	// ユーザーの資格情報（パスワード）の有効期限が切れているかどうかを示す。今回は資格情報の有効期限は設定しないので、デフォルトでtrueに設定。
	@Override
	public boolean isCredentialsNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}
	
	// ユーザーが有効か無効かを示す。今回は有効か無効かは設定しないので、デフォルトでtrueに設定。
	@Override
	public boolean isEnabled() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public String toString() {
		return "CustomUserDetails [user=" + user + "]";
	}

}

~~~

## UserDetailsServiceの作成
UserDetailsServiceとは、認証に必要な情報をキーにして取得するDAOである。
一番ポピュラー(?)なやり方は、``UserDetailsService（org.springframework.security.core.userdetails.UserDetailsService）``インターフェイスを実装するやり方であるので、ここでもそのやり方を記載する。

#### ✓ ポイント
- ``UserDetailsService``インターフェイスの``loadUserByUsername(String username)``を実装する。ここではusernameをクレデンシャルとし、DBにusernameで検索することで認証を行う。
- 認証に成功した場合は、先ほど作成した``UserDetails``クラスをnewしてreturnする。
- 認証に失敗した際は、``UsernameNotFoundException``を投げる。

~~~java:CustomUserDetailsService
package com.example.authentication;

@Service
public class CustomUserDetailsService implements UserDetailsService{
	
    // DBにアクセスするリポジトリを作成する。コードは後述する。
	@Autowired
	private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByName(username); // usernameをクレデンシャルとし、DBにusernameで検索する。
		if(user == null) {
			throw new UsernameNotFoundException("user not found");
		}
		CustomUserDetails customUserDetails = new CustomUserDetails(user); // 先ほど作成したUserDetailsクラスをnewしてreturnする
		return customUserDetails;
		
	}

}

~~~

#### ✓ 追加ポイント
``UserDetailsService``インターフェイスでは、認証に必要な情報を"名前"(username)として実装している。つまり、「usernameの情報だけに基づいて検索」するためのものとして存在している。しかし多くの場合は、パスワードもクレデンシャルとして検索したいだろう。そういう場合は``UserDetailsService``インターフェイスを用いるのは適さなく、``AuthenticationProvider`` を直接実装する方がよい。

https://spring.pleiades.io/spring-security/site/docs/5.2.7.RELEASE/reference/html/overall-architecture.html
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711483/822fa728-f845-ddc8-80a5-b63b21d63bd2.png)
***
**※UserDetailsServiceインターフェイスを用いらずに実装したものをこちら [**【Spring Boot】UserDetailsService を使用せずに Spring Security で認証を行う。**](https://qiita.com/MMK3/items/52625325642d827f7ed1)に記載しました。**
***

※しかし、usernameが正、passwordが不正の場合、``loadUserByUsername(String username)``を用いて実装していても、認証不可となってはじかれる。ではどこでパスワードの整合性をチェックしているのかというと、``DaoAuthenticationProvider``クラスの
``additionalAuthenticationChecks``メソッドにてチェック処理を行っているらしい。``DaoAuthenticationProvider``がUserを取得後にチェック処理を行っているようだ。


※参考
[UserDetailsServiceは誤解されている](https://yukihane.github.io/blog/202006/12/userdetailsservice-conidered-harmful/)
[UserDetailsServiceのloadUserByUsernameの存在意義がよくわからないです](https://ja.stackoverflow.com/questions/67596/userdetailsservice%E3%81%AEloaduserbyusername%E3%81%AE%E5%AD%98%E5%9C%A8%E6%84%8F%E7%BE%A9%E3%81%8C%E3%82%88%E3%81%8F%E3%82%8F%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84%E3%81%A7%E3%81%99)
[Spring Bootで、ユーザ名とパスワードを指定した認証処理の実装方法](https://qiita.com/pale2f/items/3fb28e76f969d7c18f06)
[usernameでレコードを取得して、その後どのようにしてパスワードの整合性を見ているのか](https://zenn.dev/misaka/books/9734700544990d/viewer/4f8a99#%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E3%81%AE%E6%95%B4%E5%90%88%E6%80%A7%E3%82%92%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B)

## DBにアクセスするリポジトリの作成
今回はMyBatisを用いて実装する。実装内容は、usernameに基づいて検索するだけの簡単なものである。

~~~java:UserRepository.java
package com.example.repository;

import文省略

@Mapper
public interface UserRepository {
	
	/**
	 * nameからuserを特定
	 * @return User
	 */
	public User findByName(String name);
	
}
~~~

~~~xml:UserRepository.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="com.example.repository.UserRepository">
<select id="findByName" parameterType="String" resultType="com.example.entity.User">
  SELECT id, name, password, authority
  FROM users
  WHERE name = #{name}
</select>
</mapper>
~~~

## Spring Security の設定を行うConfigクラスにパスワードのエンコード方式を設定
先ほど作成したConfigクラスにパスワードのエンコード方式を追加で設定する。
#### ✓ ポイント
- ``configure(AuthenticationManagerBuilder auth)``のメソッドをオーバーライドして、エンコード方式を設定。

~~~java:SecurityConfig.java
package com.example.config;

import文 省略

@EnableWebSecurity // WebSecurity関連の設定であること示すアノテーション
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Override
	protected void config(HttpSecurity http)throws Exception{
      http.authorizeRequests()
           // URLごとに、認証の要・不要の定義を行う
          .antMatchers("/login/**", "/img/**", "/css/**", "/js/**").permitAll() // ログイン画面、imageフォルダ、cssフォルダ、jsフォルダは認証が不要である
          .anyRequest().authenticated() //上記以外は認証が必要である
      
      // ログイン関連の設定
          .and()
          .formLogin() //認証方式は、フォーム認証であること
          .usernameParameter("name") // usernameのパラメータを独自定義（デフォルトは、username）
          .passwordParameter("password") // passwordのパラメータを独自定義（デフォルトはpassword）
          .loginPage("/login") // ログインページを定義（これで自作したページをログインページとして使用できる）
          .failureUrl("/login?failed") // ログインに失敗したときのURL（デフォルトは?error）
          .defaultSuccessUrl("/top") // ログインに成功したときのURL

      // ログアウト関連の定義
          .and()
          .logout()
          .logoutUrl("/logout") // ログアウトURLの定義
          .logoutSuccessUrl("/login") // ログアウト成功後の画面（今回はログインページに遷移するよう定義）
          ;

    }

    // ▼ ここが追加した部分 ▼
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 今回は模擬なので、何もしないパスワードエンコーダーであるNoOpPasswordEncoderを用いる。
		auth.userDetailsService(customUserDetailsService).passwordEncoder(NoOpPasswordEncoder.getInstance());
	}
}

~~~

## 終わりに
Spring Securityは裏でどういう仕組みで動いているのかわかりづらいものだと思うので、インプットした内容を備忘のためにここに記載しました。
上記の実装は5.7以降の書き方ではありません。5.7以降での書き方との差分は[**こちら**](https://qiita.com/MMK3/items/589509a4e235209bff6c)に掲載しております。
間違っている部分もあるかと思いますので、その際はご指摘頂ければと思います。