Spring legacy

Spring Security - UserDetailsService

voider 2020. 9. 13. 19:35

UserDetailsService

JDBC를 이용한 인증방식의 단점은 사용자의 여러 정보들 중에서 제한적인 내용만을 이용한다는 점이다.
스프링 시큐리티에서는 username이라고 하는 정보만을 이용하므로 이름이나 이메일 등 자세한 정보를 이용할 경우 충분하지 못하다.
UserDetailsService이스를 구현하는 방식으로 이러한 문제를 해결할 수 있다.

UserDetailsServiceloadUserByUserName이라는 하나의 메서드만 가지고 있다.
loadUserByUserName()UserDetails를 반환하는데, UserDetails는 사용자의 정보와 권한 정보를 담는 인터페이스다. UserDetailsgetAuthorities(), getPassword(), getUserName() 등 여러 추상 메서드를 가지고 있다. 따라서 이것을 직접 구현할 것인지, UserDetails를 구현한 스프링 시큐리티의 여러 하위 클래스를 이용할 것인지 판단해야 한다.
가장 일반적으로 org.springframework.security.core.userdetails.User클래스를 상속한다.

MyBatis를 이용하는 MemberMapper와 서비스를 만들고, 스프링 시큐리티와 연결해서 사용하는 방식을 연습해본다.

회원 도메인

MemberVO.java

@Data 
public class MemberVO { 
    private String userid;
    private String userpw;
    private String username;
    private boolean enabled;
    private Date regdate;
    private Date moddate;
    private List<AuthVO> authList;
}

AuthVO.java

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

회원 Mapper

  • 회원에 대한 정보는 MyBatis를 이용해서 처리하므로 해당 매퍼를 작성한다.
  • tbl_member와 tbl_member_auth테이블에 데이터를 추가/조회할 수 있도록 작성.
  • Member객체를 가져오는 경우, 한 번에 두 개의 테이블을 join해서 처리할 수 있는 방식으로,
  • MyBatis의 ResultMap 기능을 사용한다.
    하나의 MemberVO 인스턴스는 내부적으로 여러 개의 AuthVO를 가진다. (admin계정이 ROLE_MANAGE와 ROLE_ADMIN 권한을 가지는 것)
    이것을 1:N. 1대 N의 관계라고 한다. 하나의 데이터가 여러 하위 데이터를 포함하는 것을 말한다.
    MyBatis의 ResultMap을 이용하면 하나의 쿼리로 MemberVO와 그 하위의 AuthVO의 리스트까지 처리할 수 있다.

MemberMapper.java

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

}

MemberMapper.xml

[http://mybatis.org/dtd/mybatis-3-mapper.dtd">](http://mybatis.org/dtd/mybatis-3-mapper.dtd">) 
<mapper namespace="com.coco.mapper.MemberMapper"> 
  <resultMap type="com.coco.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="moddate" column="moddate"/> 
    <collection property="authList" resultMap="authMap"> 
    </collection> 
  </resultMap> 
  <resultMap type="com.coco.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, moddate, auth 
    FROM tbl\_member mem 
    LEFT OUTER JOIN 
    tbl\_member\_auth auth on mem.userid = auth.userid 
    WHERE mem.userid = #{userid}
  </select>
</mapper>

MemberMapper 테스트

xml설정까지 마쳤으면 MemberMapper가 제대로 작동하는지 테스트한다.

TEST

@Autowired
MemberMapper memberMapper; 
    @Test
    public void testRead() { 
        MemberVO memberVO = memberMapper.read("admin20"); 
        assertNotNull(memberVO); 
        log.info(memberVO);
        memberVO.getAuthList()
            .forEach(authVO -> log.info(authVO));
}

결과 잘 나오는 지 확인.

INFO : com.coco.mapper.MemberMapperTests - MemberVO(userid=admin20, userpw=$2a$10$qV6CGQKkzYCiHh40tvP7l./UfPFQc80xHC2eu4JSA/Yfqv/6he4PW, username=관리자20, enabled=false, regdate=Wed Sep 09 15:16:04 KST 2020, moddate=Wed Sep 09 15:16:04 KST 2020, authList=\[AuthVO(userid=admin20, auth=ROLE\_ADMIN)\]) INFO : com.coco.mapper.MemberMapperTests - AuthVO(userid=admin20, auth=ROLE\_ADMIN)

CustomUserDetailsService 만들기

UserDetailsService를 구현하는 클래스를 만든다. 그 다음, MemberMapper타입의 인스턴스를 주입받아서 기능을 구현한다.

위에서 말했듯이 UserDetailsService가 가지고 있는 추상 메서드 loadByUsername()UserDetails를 반환한다.
따라서 MemberVO의 인스턴스를 UserDetails로 변환하는 작업을 할 필요가 있다.

  1. MemberVO가 UserDetails 인터페이스를 구현한다.

    • 나쁜 방법은 아니나, 기존의 클래스를 건드려야 한다는 단점이 있음.
  2. CustomUser클래스를 만들어서 따로 처리한다.

    • 기존의 클래스를 건드리지 않고, 확장하는 방식으로 처리할 수 있음.

2번 방법으로 처리한다.

CustomUser.java

@Getter
public class CustomUser extends User {
    private static final long serialVersionUID = 1L;

    private MemberVO member;

    public CustomUser(String username, String password, boolean enabled, boolean accountNonExpired,
                boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); 
    }

    public CustomUser(MemberVO memberVO) { 
        super(memberVO.getUserid(), memberVO.getUserpw(), memberVO.getAuthList().stream() .map(auth -> new                     SimpleGrantedAuthority(auth.getAuth())) .collect(Collectors.toList())); this.member = memberVO; }
    }

CustomUserDetailsService.java

@RequiredArgsConstructor 
@Log4j
public class CustomUserDetailsService implements UserDetailsService {
    private final MemberMapper memberMapper; 

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.warn("Load User By User Name : " + username); //username은 userid를 의미한다.
        MemberVO memberVO = memberMapper.read(username);

        return memberVO == null ? null : new CustomUser(memberVO);
    }
}

화면에서 로그인 테스트 진행하면 통과하는 것을 확인할 수 있다.