커스텀 UserDetailsService 활용(최종적으로 사용해야 할 인증방식)

앞에서 다룬 과정들이 모두 이번 챕터를 구현하기 위해 알아본 정보였다.
궁극적으로 커스텀 UserDetailsService 를 활용하여 인증/권한 Provider 를 구성하는 것이 좋다.

[ Authentication Manager ]
        ↑
       상속
        ⎮
   [ Provider ]
        ↑
       상속
        ⎮
[ UserDetailsService ] <-- 구현 -- [ CustomUserDetailsService ]
                                         ⎮
                                        리턴
                                         ↓ 
                                   [ UserDetails ]<-- 구현 --[ Users ]
                                                             ↑
                                                ↖︎            상속
                                                상속         ⎮
                                                    ↖︎  [ CustomUsers ]

1. 영속계층 개발하기

중요한 것은, 스프링 시큐리티에서 username은 사용자의 ID를 의미한다.

테이블스키마

create table tbl_member(
	userid varchar(50) not null primary key,
    userpw varchar(100) not null,
    username varchar(100) not null,
    regdate timestamp default current_timestamp,
    updatedate timestamp default current_timestamp,
    enabled char(1) default '1'
);


create table tbl_member_auth(
	userid varchar(50) not null,
    auth varchar(50) not null
);

VO 객체 생성

. MemberVO

package org.example.domain;

import java.util.Date;
import java.util.List;

import lombok.Data;

@Data
public class MemberVO {
	
	private String userid;
	private String userpw;
	private String userName;
	private boolean enabled;
	
	private Date regDate;
	private Date updateDate;
	private List<AuthVO> authList;
	
}

. AuthVO

package org.example.domain;

import lombok.Data;

@Data
public class AuthVO {
	
		private String userid;
		private String auth;
}

MemberMapper 인터페이스 작성

package org.example.mapper;

import org.example.domain.MemberVO;

public interface MemberMapper {
	
	 public MemberVO read(String userid);
}

MemberMapper.xml 작성

tbl_member 와 tbl_member_auth 는 1:N 관계이다.
1:N 관계를 처리할 때는 아래 예제와 같이 resultMap을 사용하면 쉽다.

<?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="org.example.mapper.MemberMapper">	
	
	<resultMap type="org.example.domain.MemberVO" id="memberMap">
		<id property="userid" column="userid"/>
		<result property="userid" column="userid"/>
		<result property="userpw" column="userpw"/>
		<result property="userName" column="username"/>
		<result property="regDate" column="regdate"/>
		<result property="updateDate" column="updatedate"/>
		 <collection property="authList" resultMap="authMap" ></collection>
	</resultMap>
	
	<resultMap type="org.example.domain.AuthVO" id="authMap">
		<result property="userid" column="userid"/>
		<result property="auth" column="auth"/>
	</resultMap>
	
	<select id="read" resultMap="memberMap">
	SELECT	MEM.USERID, USERPW, USERNAME, ENABLED, REGDATE, UPDATEDATE, AUTH
	FROM	TBL_MEMBER AS MEM
	LEFT OUTER JOIN	TBL_MEMBER_AUTH AS AUTH 
	ON		MEM.USERID = AUTH.USERID
	WHERE	MEM.USERID = #{userid}
	</select>
</mapper>

TEST 코드 작성

. MemberMapperTests
. Run Junit 실행 하여, 결과를 확인한다.

package org.example.mapper;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class MemberMapperTests {

	@Setter(onMethod_= @Autowired )
	private MemberMapper mapper;
	
	@Test
	public void testRead() {
		MemberVO vo = mapper.read("admin90");
	
		log.info(vo);
		vo.getAuthList().forEach(auth -> log.info(auth));
	}
}

2. CustomUserDetailsService 구성하기

MemberMapper 를 CustomUserDetailsService 에 주입한다.
CustomUserDetailsService의 유일한 메소드 ‘loadUserByUsername(String username)' 는 ‘UserDetails' 객체를 리턴하므로,
우리는 ‘UserDetails' 인터페이스를 구현한 User 객체를 상속한 CustomUser 에 MemberVO를 담아서 리턴하도록 구현한다.

CustomUser 클래스 작성

package org.example.security.domain;

public class CustomUser extends User{
	
	private static final long serialVersionUID = 1L;
	
	// MemberVO 를 주입한다.
    private MemberVO member;

    // 상속을 받기 위해서는 상속자의 생성자를 호출하여야 한다.
	public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}
	
	public CustomUser(MemberVO vo) {
		super(vo.getUserid(), vo.getUserpw(), vo.getAuthList().stream().
				map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList()));
		
		this.member = vo;
	}
}

CustomUserDetailsService 클래스 작성

package org.example.security;

@Log4j
public class CustomUserDetailsService implements UserDetailsService {

	@Autowired
	private MemberMapper mapper;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		
		log.warn("Load User By UserName:" + username );
		
		MemberVO vo = mapper.read(username);
		
		return vo == null? null : new CustomUser(vo);
	}
}

테스트

탐캣을 실행하고 http://localhost:8080/sample/admin 으로 접속한다. admin 권한이 있는 사용자가 로그인 하면 admin 화면으로 가고 권한이 없는 사용자가 로그인 하면 홈화면(http://lacalhost:8080/)으로 이동한다.