UserDetailsService
JDBC를 이용한 인증방식의 단점은 사용자의 여러 정보들 중에서 제한적인 내용만을 이용한다는 점이다.
스프링 시큐리티에서는 username이라고 하는 정보만을 이용하므로 이름이나 이메일 등 자세한 정보를 이용할 경우 충분하지 못하다.UserDetailsService
이스를 구현하는 방식으로 이러한 문제를 해결할 수 있다.
UserDetailsService
는 loadUserByUserName
이라는 하나의 메서드만 가지고 있다.loadUserByUserName()
는 UserDetails
를 반환하는데, UserDetails
는 사용자의 정보와 권한 정보를 담는 인터페이스다. UserDetails
는 getAuthorities()
, 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
로 변환하는 작업을 할 필요가 있다.
MemberVO가 UserDetails 인터페이스를 구현한다.
- 나쁜 방법은 아니나, 기존의 클래스를 건드려야 한다는 단점이 있음.
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);
}
}
화면에서 로그인 테스트 진행하면 통과하는 것을 확인할 수 있다.
'Spring legacy' 카테고리의 다른 글
스프링 시큐리티 어노테이션 (0) | 2020.09.13 |
---|---|
JSP에서 스프링 시큐리티 사용하기 (0) | 2020.09.13 |
Spring Security - 기존 테이블을 이용하는 인증방법 (0) | 2020.09.13 |
UriComponentBuilder (0) | 2020.09.12 |
Spring 게시판 구현 - Presentation Layer II (0) | 2020.09.12 |