Seojoo21 2022. 3. 14. 15:24

* 카테고리의 글은 <코드로 배우는 스프링  프로젝트에서  파트별로 진행하는 프로젝트의 흐름을 보기 위해  장의 내용을 간단히 요약한 것이다.

출처코드로 배우는 스프링  프로젝트 개정판구멍가게 코딩단남가람북스 

 

 

Ch33 커스텀 UserDetailsService 활용 

- JDBC를 이용하는 방식으로도 데이터베이스를 처리해서 편리하게 사용할 수 있기는 하지만 약간의 아쉬움은 사용자의 여러 정보들 중에서 제한적인 내용만을 이용한다는 단점이 있다.

- 스프링 시큐리티에서 username이라고 부르는 사용자의 정보만을 이용하기 때문에 실제 프로젝트에서 사용자의 이름이나 이메일 등의 자세한 정보를 이용할 경우에는 충분하지 못하다는 단점이 있다.

 

- 이러한 문제를 해결하기 위해서는 직접 UserDetailsService를 구현하는 방식을 이용하는 것이 좋다. 

- 흔히 커스텀 UserDetailsService라고 하는데, 이를 이용하면 원하는 객체를 인증과 권한 체크에 활용할 수 있기 때문에 많이 사용된다.

 

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

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

- UserDetails 타입은 getAuthorities(), getPassword(), getUserName() 등의 여러 추상 메서드를 가지고 있어서, 개발 전에 이를 직접 구현할 것인지 UserDetails 인터페이스를 구현해둔 스프링 시큐리티의 여러 하위 클래스를 이용할 것인지 판단해야 한다. 

- 가장 일반적으로 많이 사용되는 방법은 여러 하위 클래스들 중에서 org.springframework.security.core.userdetails.User 클래스를 상속하는 형태이다. 

- 예제는 커스텀 UserDetailsService를 이용하는 방식을 이용하기 위해서 MyBatis를 이용하는 MemberMapper와 서비스를 작성하고, 이를 스프링 시큐리티와 연결해서 사용하는 방식으로 진행한다. 

 

33.1 회원 도메인, 회원 Mapper 설계

- 예제를 위해서 앞에서 만든 tbl_member 테이블과 tbl_member_auth 테이블을 MyBatis를 이용하는 코드로 처리한다.

- org.zerock.domain 패키지에서 MemberVO 와 AuthVO 클래스를 처리한다.

 

 

- Member 클래스는 내부적으로 여러 개의 사용자 권한을 가질 수 있는 구조로 설계한다. 

- AuthVO는 tbl_member_auth의 칼럼을 그대로 반영해서 userid, auth를 지정한다. 

 

33.1.1 MemberMapper

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

- Member 객체를 가져오는 경우에는 한 번에 tbl_member와 tbl_member_auth를 조인해서 처리할 수 있는 방식으로 MyBatis의 ResultMap이라는 기능을 사용한다.

 

- 하나의 MemberVO 인스턴스는 내부적으로 여러 개의 AuthVO를 가지는데 이를 흔히 '1+N 관계'라고 한다. 즉 하나의 데이터가 여러 개의 하위 데이터를 포함하고 있는 것을 의미한다.

- MyBatis의 ResultMap을 이용하면 하나의 쿼리로 MemberVO와 내부의 AuthVO의 리스트까지 아래와 같이 처리할 수 있다.

 

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

- src/main/resources 밑에 org/zerock/mapper 폴더 구조를 작성하고 MemberMapper.xml을 작성한다. 

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

- 위의 결과를 자세히 보면 오른쪽 끝의 'AUTH' 값은 다르지만, 나머지 정보는 같은 것을 볼 수 있다. 즉 회원 정보는 MemberVO 하나이고, AuthVO는 2개가 되어야 하는 결과이다.

- memberMap이라는 이름을 가지는 <resultMap>은 <result>와  <collection>을 이용해서 바깥쪽 객체(MemberVO의 인스턴스)와 안쪽 객체들(AuthVO의 인스턴스들)을 구성할 수 있다.

- MyBatis에서는 이처럼 하나의 결과에 부가적으로 여러 개의 데이터를 처리하는 경우 1:N의 결과를 처리할 수 있는 <resultMap> 태그를 지원한다. 

 

33.1.2 MemberMapper 테스트

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

- 생성된 프로젝트의 root-context.xml은 이전 예제에서 사용하던 파일을 재사용하도록 한다. MyBatis의 설정이나 트랜잭션의 설정 등에 주의해서 설정한다. 

- 쿼리 동작은 테스트 코드를 이용해서 올바른 결과가 나오는지 확인해야 한다. src/test/java에 MemberMapperTests 클래스를 작성한다. 

- testRead()에서 "admin90"에 대한 정보를 조회한다. 정상적이라면 MemberVO와 내부의 AuthVO가 구성된 것을 확인할 수 있다. 

 

33.2 CustomUserDetailsService 구성

- MyBatis를 이용해서 MemberVO와 같이 회원을 처리하는 부분이 구성되었다면 이를 이용해서 스프링 시큐리티의 UserDetailsService를 구현하는 클래스를 직접 작성하도록 한다.

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

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

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

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

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

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

 

33.2.1 MemberVO를 UserDetails 타입으로 변환하기 

- 스프링 시큐리티의 UserDetailsService는 loadUserByUsername()라는 하나의 추상 메서드만을 가지고 있으며 리턴 타입은 org.springframework.security.core.userdetails.UserDetails라는 타입이다. 

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

- 예제는 UserDetails를 구현한 org.springframework.security.core.userdetails.User 클래스를 상속해서 CustomUser라는 클래스를 생성한다. 

- 물론 MemberVO 클래스를 직접 수정해서 UserDetails 인터페이스를 구현하도록 하는 방법도 나쁘다고 생각되지는 않지만, 가능하면 기존 클래스를 수정하지 않고 확장하는 방식이 낫다고 생각되기 때문에 org.zerock.security 패키지에 별도의 domain 패키지를 추가해서 CustomUser 클래스를 생성한다.

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

- 예제는 MemberVO를 파라미터로 전달해서 User 클래스에 맞게 생성자를 호출한다. 이 과정에서 AuthVO 인스턴스는 GrantedAuthority 객체로 변환해야 하므로 stream()과 map()을 이용해서 처리한다. 

- 변경 후에는 CustomUserDetailsService에서 CustomUser를 반환하도록 수정해본다.

- loadUserByUsername()은 내무적으로 MemberMapper를 이용해서 MemberVO를 조회하고, 만일 MemberVO의 인스턴스를 얻을 수 있다면 CustomUser 타입의 객체로 변환해서 반환한다. 브라우저에서 이를 테스트해 보면 로그인 시 CustomUserDetailsService가 동작하는 모습을 확인할 수 있다.