본문 바로가기
Spring공부/4-스프링시큐리티

커스텀 UserDetailsService

by 으노으뇨 2021. 10. 19.
728x90
반응형
SMALL

JDBC를 이용하는 방식을 DB를 처리해서 편리하게 사용할 수 있기는 하지만, 제한적인 내용만만을 이용한다는 단점이있다.

스프링 시큐리티에서 username이라고 부르는 사용자의 정보만을 이용하기 때문에 충분하지 실제 프로젝트에서 사용자

의 이름이나 이메일등의 자세한 정보를 이용할 경우에는 부족하다.

이런문제를 해결하기 위해서 직접 UserDeatilasService를 구현하는 방식을 이용한다.

흔히 커스텀 UserDetailsService를 구현하는데, 원하는 객체를 인증과 권한 체크에 활용 할 수 있다.

스프링 시큐리티의 UserDetailsService 인터페이스는 단 하나의 메서드만이 존재한다.

loadUserByUsername()이라는 메서드의 반환 타입인 UserDetails 역시 인터페이스로, 사용자의 정보와 권한

정보 등을 담는 타입이다. UserDetails역시 인터페이스로 사용자의 정보와 권한 정보를 담는 타입이다.

UserDetails타입은 getAuthorities(), ,getPassword(), getUserName()등의 여러 추상 메서드를 가지고 있어서, 개발 전에 

이를 직접 구현할 것인지, UserDetails인터페이스를 구현해둔 스프링 시큐리티의 여러 하위 클래스를 이용할 것인지 

판단만 하면된다.!


회원 객체도메인 Mapper설계

회원정보가 있는 테이블과 회원 권환이 있는 테이블을 MyBais를 이용하는 코드를 만들어야 한다.

org.study.domain패키지에 MemberVO와 AuthVO 클래스를 설계해주자

memberVO클래스에는

package org.study.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;

}

MemberVO클래스에 내부적으로 여러개의 사용자 권한을 가져올 수 있는 list배열도 생성한다.

authVO클래스에는

package org.study.domain;


import lombok.Data;

@Data
public class AuthVO {

  private String userid;
  private String auth;
  
}

member_auth 컬럼을 그대로 반영해서 userid, auth를 지정한다.


MemberMapper

회원에 대한 정보는 MyBatis를 이용해서 처리할 것이므로 MemeberMapper를 작성해서 tml_member와 tbl_member_auth테이블에 데이터를 추가하고, 조회할 수 있도록 작성한다. 

Member객체를 가져오는 경우에는 한 번에 멤버테이블과 멤버권한 테이블을 조인해서 처리 할 수 있는 방식으로

MyBatis의 ResultMap이라는 기능을 사용한다.

하나의 MemberVO인스턴스는 내부적으로 여러 개의 AuthVO를 가지는데 이를 흔히 1+N이라는 관계라고한다.

하나의 데이터가 여러개의 하위 데이터를 포함하고 있는 것이다. 

MyBatis의 ResultMap을 이용하면, 하나의 쿼리로 memberVO와 내부의 권한VO리스트까지 함께 처리할 수 있다.

MyBatis를 이용하기 위한 MemberMapper인터페이스를 mapper패키지에 추가하여 작성해주자!!

package org.study.mapper;

import org.study.domain.MemberVO;

public interface MemberMapper {

	public MemberVO read(String userid);
}

그리고 이것을 읽을 수 있도록 

MemberMapper.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="org.study.mapper.MemberMapper">


  <resultMap type="org.study.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.study.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 mem LEFT OUTER JOIN tbl_member_auth auth on mem.userid = auth.userid 
WHERE mem.userid = #{userid} 
  </select>

</mapper>

id가 read인 <select>태그는 resultMap속성을 지정합니다. 지정된 memberMap은 아래와 같은 쿼리의 결과를 처리한다.

위의 결과를 보면 오른쪽 끝의 AUTH의 값을 다르지만, 나머지 정보는 같이 것을 볼 수 있다. 

즉 회원정보는 memeberVO이고, authVO는 2개가 되어야 하는 결과이다. memberMap이라는 이름을 가지고

<resultMap>은 <result>와 <collection>을 이용해서 바깥쪽 객체(memberVO의 인스턴스)와 안쪽 객체들(AuthVO의 인스턴스들)을 구성 할 수 있다. 

MyBatis에서는 이처럼 하나의 결과에 부가적으로 여러개의 데이터를 처리하는 경우 1:N의 결과를 처리할 수 있는

<resultMap>태그를 지원한다.


MemberMapper테스트

memberMapper를 이용해서 memberVO를 구성하고 이를 스프링 시큐리티에서 사용할 것으로 연동하기 전에 MemberMapper가 정상적으로 동작하는지 확인한다.

생성된 프로젝트의 root-context.xml은 이전 예제에서 사용하던 파일을 재사용 하도록 한다.

MyBatis의 설정이나 트랜잭션의 설정 등에 주의해서 설정합니다.

쿼리 동작은 테스트 코드를 이용해서 올바른 결과가 나오는지 확인하자

package org.study.mapper;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.study.domain.MemberVO;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringRunner.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(authVO -> log.info(authVO));

	}

}

id이름인 read를 불러와 admin90을 불러오는 테스트이다.

결과는 이상없다 그리고 

info에서 보자면 memeberVO와 authVO를 함꼐 불러온 로깅을 볼 수 있다.


CustomUserDetailsService구성

MyBatis를 이용해서 memberVO와 같이 회원을 처리하는 부분이 구성되었다면, 이를 이용해서 스프링 시큐리티의

UserDeailsService를 구현하는 클래스를 직접 작성하도록 할 수 있다.

CustomUserService는 스프링 시큐리티의 UserDetailsService를 구현하고, MembertMapper타입의 인스턴스를 주입받아서 실제 기능을 구현한다.

 org.study.security패키지에 CustomUserDetailsService클래스를 작성한다.

package org.study.security;

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.study.mapper.MemberMapper;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Log4j
public class CustomUserDetailsService implements UserDetailsService {

	@Setter(onMethod_ = { @Autowired })
	private MemberMapper memberMapper;

	@Override
	public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {

		log.warn("Load User By UserName : " + userName);

		return null;
	}

}

작성하는 클래스는 스프링 시큐리티를 통해서 테스트를 진행한 후 추가로 채우고, 우선은 로그만을 기록해서 정상적으로 동작하는지 만을 확인한다.

CustomUserDeailsService클래스는 security-context.xml을 이용해서 스프링의 빈으로 등록한다.

<bean id="customLogoutSuccess"
	class="org.study.security.CustomLogoutSuccessHandler"></bean>
	<security:authentication-manager>
		<security:authentication-provider
			user-service-ref="customUserDetailsService">
			<security:password-encoder
				ref="bcryptPasswordEncoder" />
		</security:authentication-provider>
	</security:authentication-manager>

변경된 부분은 authentication-provider 속성의 값을 작성한 CustomUserDetailsService로 지정한 부분이다.

톰캣을 실행하고 아래와 같은 화면에서 로그인을 시도했을 때 지정된 로그가 출력되고, 의존성 주입 등이 정상

적으로 처리되었는지 확인해보자.

프로젝트를 실행하고 화면에서 로그인을 시도했을 때 지정된 로그가 출력되고, 의존성 주입이 정상적으로 처리되었다.

그러나 로그창을 보니 UserDatilesService returned null, 즉 반환값이 비었다는 것이다.

이제 이이것을 MemberVo를 UsersDetails타입으로 변환하는 것을 익혀야 한다.


MemberVO를 UserDatails타입으로 변환하기

스프링 시큐리티의 UserDetailsService는 loadUserMyUsername()이라는 하나의 추상 매서드만을 가지고 있다.

리턴타입은 UserDetils라는 타입이다. 모든 작업에 문제가 없다면,

최종적으로 MemberVO의 인스턴스를 스프링시큐리티의 UserDetails타입으로 변환하는 작업을 처리한다.

UserDetails 타입으로 변환하는 작업을 구현하도록 하자.

package org.study.security.domain;

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

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.study.domain.MemberVO;

import lombok.Getter;

@Getter
public class CustomUser extends User {

	private static final long serialVersionUID = 1L;
	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;
	}
}

MemberVO클래스를 직접 수정해서 UserDetails인터페이스를 구현하도록 하는 방법도 있지만,

기존의 클래스를 수정하지 않고 확장하는 방식으로 공부를 해보자 . 그래서 새로운 security패키지에 domain패키지를 추가해서 CustomUser 클래스를 생성한다.

CustomUser는 org.springframework.security.core.userdetails.User; 클래스를 상속하기 때문에 부모 클래스의 생성자를 호출해야만 정상적인 객체를 생성할 수 있다.

예제는 MemberVO를 파라미터로 전달해서 User클래스에 맞게 생성자를 호출한다.

이 과정에서 AuthVO인스턴스는 GreantedAuthority객체로 변환해야 하므로 stream()과 map()을 이용해서 처리한다.

변경 후에는 CustomUserdetailsService에서 CustomUser를 반환하도록 수정해야한다.

@Log4j
public class CustomUserDetailsService implements UserDetailsService {

	@Setter(onMethod_ = { @Autowired })
	private MemberMapper memberMapper;
	@Override
	public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
		log.warn("Load User By UserName : " + userName);
		// userName means userid
		MemberVO vo = memberMapper.read(userName);
		log.warn("queried by member mapper: " + vo);
		return vo == null ? null : new CustomUser(vo);
	} 

}

loadUserByUsername()은 내부적으로 MemeberMapper를 이용해서 MemberVO()를 조회하고, 만일 MemberVO의 

인스턴스를 얻을 수 있다면 CustomUser타입의 객체로 변환해서 반환한다. 

브라우저에서 이를 테스트 해보면 로그인시 CustomUserDetailsService가 동작하는 모습을 보게된다.

728x90
반응형
LIST

댓글