独自ログイン機能(UserDetailsService編)
独自のログイン機能でユーザー情報を取得するための実装です。かなりハマりましたので備忘録としての記録。
O/R mapperとしてMyBatisを利用しております。
Configurer設定についてはこちら→
UserDetailsServiceインターフェースを継承したクラスを作成
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が取ってきた認証情報を格納するクラス
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アノテーションでゲッターセッターを自動生成してくれる
 
- このクラスの情報がセッションに保存されるため、個人情報保護の観点で、ログイン認証に必要な最低限の情報を持たせればよいと思います。(個人の感想)
 
- ユーザーロールは一人で2つ以上持っている可能性を考慮しコレクション型でリストを格納するように実装してます。
 
MyBatisのMapperインターフェースの実装
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ファイルに記述する
 

Mapper.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上で目的のレコードをとってこれるか試してみる。
 
- #{ }で囲まれた変数に値が入っているかデバッグなどで確認する。