独自ログイン機能（UserDetailsService編）

独自のログイン機能でユーザー情報を取得するための実装です。かなりハマりましたので備忘録としての記録。

O/R ｍapperとしてMyBatisを利用しております。

Configurer設定についてはこちら→

# UserDetailsServiceインターフェースを継承したクラスを作成

```java
package io.post.novel.service;

import org.springframework.beans.factory.annotation.Autowired;
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 io.post.novel.mapper.LoginMapper;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	
	@Autowired
	LoginMapper loginMapper;
	

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		return loginMapper.identifyUser(username);
	}

}
```

- **＠Service**アノテーションを付ける。
- `@Autowired
	LoginMapper loginMapper;`にて依存性注入をしておく。

## UserDetails loadUserByUsername(String username)メソッド

- 取得した認証情報をUserDtailsに渡して認証するためのメソッド。
- `return`の `loginMapper.identifyUser(username);`はMyBatisで取得したユーザー情報のオブジェクトを渡す。正しくSQLで取得できていれば、このクラスの実装はこれだけ。

# MyBatisが取ってきた認証情報を格納するクラス

```java
package io.post.novel.auth;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Data;

@Data
public class UserForm implements UserDetails {
	
	private static final long serialVersionUID = 1L;
	
//DBのテーブル
  private long id;
	private String penName;//認証のID
	private String password;//認証のPW
	private List<String> roles;//ユーザーロール
	private boolean locked;//アカウントロック
	private boolean expired;//アカウント有効化

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		
		return roles.stream()
				.map(SimpleGrantedAuthority::new)
				.collect(Collectors.toList());
	}

	@Override
	public String getPassword() {
		
		return password;
	}

	@Override
	public String getUsername() {
		
		return penName;
	}

	@Override
	public boolean isAccountNonExpired() {
		
		return !expired;
	}

	@Override
	public boolean isAccountNonLocked() {
		
		return !locked;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		
		return !expired;
	}

	@Override
	public boolean isEnabled() {
		
		return !locked;
	}

}
```

## ユーザー情報を格納するUserFormクラス

- **UserDtailsインターフェースを継承**したクラス
- `import lombok.Data;`＠Dataアノテーションでゲッターセッターを自動生成してくれる
- このクラスの情報がセッションに保存されるため、個人情報保護の観点で、**ログイン認証に必要な最低限の情報**を持たせればよいと思います。(個人の感想)
- ユーザーロールは一人で２つ以上持っている可能性を考慮し**コレクション型でリストを格納するように実装**してます。

# MyBatisのMapperインターフェースの実装

```java
package io.post.novel.mapper;

import org.apache.ibatis.annotations.Mapper;

import io.post.novel.auth.UserForm;

@Mapper
public interface LoginMapper {

	public UserForm identifyUser(String username);
```

- **@Mapper**アノテーションを付ける
- 記述としては上記メソッド一つの実装のみ。
- **SQLの中身は同じパッケージ改装のxmlファイルに記述する**

![Untitled2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1614172/dcca6149-b304-56e6-4849-00f3d0083215.png)



# Mapper.xmlファイルの実装

```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 = "io.post.novel.mapper.LoginMapper">
	
	<select id = "identifyUser" parameterType = "java.lang.String" resultMap = "userDetails">
	<![CDATA[
		SELECT users.id,
			   users.pen_name,
			   users.password,
			   users.locked,
			   users.expired,
         user_roles.role_id,
			   role_master.role_name
		FROM   users
		INNER JOIN
				(
					SELECT users.id,
						@RN := @RN + 1 AS RN
					FROM	users,
							(SELECT @RN := 0)	RC
					WHERE	users.pen_name = #{penName}
					) RC
		ON	   users.id = RC.id
        INNER JOIN user_roles
        ON     user_roles.user_id = users.id
		INNER JOIN role_master
		ON     role_master.id = 1
        AND    role_master.id = user_roles.role_id
		WHERE users.pen_name = #{penName}
		AND RC.RN = 1
		
		]]>
		</select>
		
		<resultMap type="io.post.novel.auth.UserForm" id="userDetails">
			<result property = "id" column = "id" />
			<result property = "penName" column = "pen_name" />
			<result property = "password" column = "password" />
			<result property = "locked" column = "locked" />
			<result property = "expired" column = "expired" />
			<collection property="roles" ofType = "java.lang.String">
				<result column = "role_name"/>
			</collection>
		</resultMap>
</mapper>
```

- `<mapper namespace = "io.post.novel.mapper.LoginMapper">`で対応するMapperファイルを指定する。パッケージも含めて指定が必要なので注意。
- `<resultMap type="io.post.novel.auth.UserForm" id="userDetails">`でUserFormクラスのプロパティとDBのカラムを関連付けをする。type指定はパッケージも含めるので注意。
- `<select id = "identifyUser" parameterType = "java.lang.String" resultMap = "userDetails">`のid属性にMapperインターフェースで実装したメソッド名を指定する。**このメソッドとSQL一本が対応**する。resultMap属性でカラムの紐付けを行ったresultMapのidをしていして関連付ける。
- <![CDATAセクションで囲むと、その中を**文字列として認識するので<や>の記号をそのまま使える**。

### エラーが発生する場合

- SQLExceptionが発生している場合は、Mapperは動作しているので、カラムとプロパティの紐付けを見直したり、SQL文そのものがおかしい可能性あり。また外部キー制約などDB側の設定も確認をする。
- XMLファイル内のMySQL上で目的のレコードをとってこれるか試してみる。
- #{　　}で囲まれた変数に値が入っているかデバッグなどで確認する。