읽은 책 정리/코드로 배우는 스프링 웹 프로젝트

[Spring] 33 커스텀 UserDetailsServices 활용

포포015 2021. 2. 12. 21:09

JDBC를 이용하는 방식으로도 DB를 처리해서 사용할수 있지만, 여러 정보들중 제한적인 내용만 이용 한다는 단점이 존재

 

이러한 문제를 해결하기위해 UserDetailsServices 인터페이스를 구현하는 방식을 이용하는것이 좋다.

( 원하는 객체를 인증과 권한 체크에 활용 할수 있기에 많이 사용된다)

 

UserDetailsServices 인터페이스는 단 하나의 메서드만 존재한다.

가장 일반적으로 많이 사용되는 방법은

하위 클래스 중에서 org.springframework.security.core.userdetails.User 클래스 상속 하는형태이다.

 

커스텀 UserDetailsService를 이용하는 방식을 이용하기 위해선,

MYbatis를 이용하는 MemberMapper와 서비스를 작성하고, 이를 시큐리티와 연결해서 사용하는 방식

 

회원 도메인, 회원 Mapper 설계

org.zerock.domain 패키지에서 아래의 2개의 클래스를 설계한다

1
2
3
4
5
6
7
8
9
10
11
12
13
@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; //여러개의 사용자 권한
}
 
cs
1
2
3
4
5
@Data
public class AuthVO {
//유저 아이디, 권한
    private String userid;
    private String auth;
cs

 

회원에 대한 정보는 마이바티스를 이용해 처리할것이므로 , MemberMapper를 작성해서

tbl_member와 tbl_member_auth 테이블에 데이터를 추가하고, 조회할수 있도록작성 할것이다.

 

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

마이바티스의 ResultMap 이라는 기능을 사용함.(하나의 쿼리로 MemberVO와 내부 AuthVO 리스트까지 같이처리가능)

 

마이바티스를 이용하기 위한 MemberMapper 인터페이스를 org.zerock.mapper 패키지를 작성해서 추가

(root -context에 마이바티스 빈등록되있는지확인.)

 

1
2
3
4
5
6
public interface MemberMapper {
 
    public MemberVO read(String userid);
    
}
 
cs

src/main/resoruces 밑에 패키지와 같은 폴더구조를 작성하고 MemberMapper.xml을 작성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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.zerock.mapper.MemberMapper">
 
    <resultMap type="org.zerock.domain.MemberVO" id="memberMap">
        <id property="userid" column="userid"/>
        <result property="userid" column="userid"/>
        <result property="userpw" column="userpws"/>
        <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.zerock.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>
cs

memberMap 이라는 이름을 가지는 <resultMap>은 <result>와 <collection>을 이용해서,

바깥쪽 객체(MemberVO의 인스턴스)와 안쪽 객체(AuthVO의 인스턴스들)을구성할수 있다. 어렵ㄷ...

 

무튼.. 마이바티스에서는 이처럼 하나의 결과에 부가적으로 여러개의 데이터를 처리하는경우

1:N 결과를 처리할수 있는 <resultMap>태그를 지원한다

 

MemberMapper 테스트

정상작동하는지, 테스트를 해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.zerock.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.SpringJUnit4ClassRunner;
import org.zerock.domain.MemberVO;
 
import lombok.Setter;
import lombok.extern.log4j.Log4j;
 
@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(authVO -> log.info(authVO));
    }
}
 
cs

위와 같이 "admin90"에 대한 정보를 조회함 (MemberVO와 , 내부의 AuthVO가 구성된것을 확인할수 있음)

 

 

CustomUserDetailService 구성

마이바티스를 이용해 MemberVO와 같이 회원을 처리하는 부분이 구성 되었다면,

이를 이용해서 시큐리티의 UserDetailsService를 구현하는 클래스를 직접 작성한다.

- 작성하려는 CustomUserDetailsService는 스프링 시큐리티의 UserDetailsService를 구현하고, 

 MemberMapper타입의 인스턴스를 주입받아 실제 기능을 구현한다.

 

 

org.zerock.security 패키지에 CustomUserDetailService 클래스를 작성한다.

( security.context.xml에 스프링 빈으로등록, authentication-provide 속성값을 빈으로 지정한다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package org.zerock.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.zerock.domain.CustomUser;
import org.zerock.domain.MemberVO;
import org.zerock.mapper.MemberMapper;
 
import lombok.Setter;
import lombok.extern.log4j.Log4j;
 
@Log4j
public class CustomUserDetailService implements UserDetailsService {
 
    @Setter(onMethod_ = @Autowired)
    private MemberMapper memberMapper;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        
        log.warn("load user by userName:" + username);
    
        MemberVO vo = memberMapper.read(username);
        
        log.warn("member mapper" + vo);
        
        return vo == null ? null : new CustomUser(vo);
    }
    
    
    
}
 
cs

loadUserByUsername은 내부적으로 MemberMapper를 이용해서 MemberVO를 조회하고,

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

 
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 커스텀 UserDetailsService 활용 -->
<bean id="customUserDetailService" class="org.zerock.security.CustomUserDetailService"></bean>
 
 <!--  시큐리티 동작하기 위해 필요한 존재 (인증과 권한)  --> 
<security:authentication-manager> 
 
<security:authentication-provider user-service-ref="customUserDetailService"> 
  
 <security:password-encoder ref="bcryptPasswordEncoder"/> 
</security:authentication-provider> 
 
</security:authentication-manager>
cs

 

MemberVO를 UserDetails 타입으로 변환하기

시큐리티는 UserDetailsService는 loadUserByUsername()라는 하나의 추상메서드만을 가지고 있으며,

모든 작업에 문제가 없다면 최종적으로 MemberVO의 인스턴스를 스프링 시큐티리의 UserDetails 타입으로 변환하는

작업을 처리해야한다.

 

예제는 UserDetails을 구현한 org.springframework.security.core.userdetails.User 클래스를 상속해서

CustomUser 라는 클래스를 생성한다

 

org.zerock.security 패키지에 별도의 domain 패키지를 추가해서 CustomUser 클래스를 생성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package org.zerock.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.zerock.domain.MemberVO;
 
import lombok.Getter;
import oracle.net.ano.SupervisorService;
 
@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;
    }
}
 
cs

CustomUser는 User클래스를 상속하기댸문에 , 부모 클래스의 생성자를 호출해야만 정상적인 객체 생성가능.

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

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

 

해당 위처리를 다하고, 테이블에 있는 계정을 로그인해보면 아래와같이 암호화처리된 아이디가 로그인이 된다