java, spring

[Spring] 스프링 시큐리티, 커스텀 테이블, 커스텀 UserDetailsService 활용

isaac.kim 2021. 10. 3.
728x90
반응형

[Spring] 스프링 시큐리티, 커스텀 테이블, 커스텀 UserDetailsService 활용

 

이전 글

2021.10.01 - [Spring] - [Spring] 스프링 시큐리티 로그인 (커스텀 데이터베이스 사용)

2021.09.30 - [Spring] - [Spring] JDBC를 이용하는 간편 인증/권한 처리

2021.09.27 - [Spring] - CSRF(Cross-site request forgery) 공격과 토큰 / 로그인 처리 / 로그아웃 처

2021.09.27 - [Spring] - [Spring] 스프링 시큐리티를 사용하는 커스텀 로그인 페이지

2021.09.25 - [Spring] - [Spring] 스프링 시큐리티 간단한 로그인과 로그아웃 처리

2021.09.24 - [Spring] - [Spring] Spring Web Security의 설정

2021.09.24 - [Spring] - [Spring] Spring Web Security

 

JDBC를 이용하는 방식도 데이터베이스를 처리해서 편리하게 사용할 수 있지만 약간의 아쉬움은 사용자의 여러 정보들 중에서 제한적인 내용만을 이용한다는 단점이 있습니다. 스프링 시큐리티에서 username이라고 부르는 사용자의 정보만을 이용하기 때문에 실제 프로젝트에서 사용자의 이름이나 이메일 등의 자세한 정보를 이용할 경우에는 충분하지 못하다는 단점이 있습니다.

 

이 문제를 해결하기 위해 직접 UserDetailsService를 구현하는 방식을 이용하는 것이 좋습니다. 흔히 커스텀 UserDetailsService라고 하는데, 이를 이용하면 원하는 객체를 인증과 권한 체크에 활용할 수 있기 때문에 많이 사용됩니다.

 

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

 

loadUserByUsername(java.lang.String username)

 

loadUserByUsername( )이라는 메서드의 반환 타입인 UserDetails 역시 인터페이스로 유저의 정보와 권한 정보 등을 담는 타입입니다. UserDetails 타입은 getAuthorities( ), getPassword( ), getUserName( ) 등의 여러 추상 메서드를 가지고 있어서, 개발 전에 이를 직접 구현할 것인지 UserDetails 인터페이스를 구현해둔 스프링 시큐리티의 여러 하위 클래스를 이용할 것인지 판단해야 합니다.

가장 일반적으로 많이 사용되는 방법은 여러 하위 클래스들 중에서 org.springframework.security.core.userdetails.User 클래스를 상속하는 형태입니다. 예제는 커스텀 UserDetailsService를 이용하기 위해 MyBatis를 이용하는 MemberMapper와 서비스를 작성하고, 이를 스프링 시큐리티와 연결해서 사용하는 방식으로 진행합니다.

 

회원 도메인, 회원 Mapper 설계

이전 글에서 만든 테이블을 MyBatis를 이용하는 코드로 처리합니다. com.project.domain 패키지에 MemberVO와 AuthVO 클래스를 설계합니다.

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

AuthVO는 tb_member_auth의 컬럼을 그대로 반영해서 userid, auth를 지정합니다.

 

MemberMapper

Member 객체를 가져오는 경우에 tb_member와 tb_member_auth를 조인해 처리할 수 있는 방식으로 MyBatis의 ResultMap이라는 기능을 사용합니다.

 

하나의 MemberVO 인스턴스는 내부적으로 여러 개의 AuthVO를 갖는 1 : N 관계 입니다. 즉 하나의 데이터가 여러 개의 하위 데이터를 포함하고 있는 것을 의미합니다. MyBatis의 ResultMap을 이용하면 하나의 쿼리로 MemberVO와 내부의 AuthVO의 리스트까지 다음과 같이 처리할 수 있습니다.

 

MyBatis를 이용하기 위한 MemberMapper 인터페이스를 com.project.mapper 패키지에 추가합니다.

다음 'src/main/resources' 밑에 com/project/mapper 폴더 구조를 작성해 MemberMapper.xml을 작성합니다.

MemberMapper.xml

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

1 : N 의 데이터를 memberMap이라는 이름을 가지는 <resultMap>은 <result>와 <collection>을 이용해서 바깥쪽 객체(MemberVO의 인스턴스)와 안쪽 객체들(AuthVO의 인스턴스들)을 구성할 수 있습니다. MyBatis에서는 이처럼 하나의 결과에 부가적으로 여러 개의 데이터를 처리하는 경우 1:N의 결과를 처리할 수 있는 '<resultMap>' 태그를 지원합니다.

 

MemberMapper 테스트

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

 

src/test/java에 com.project.mapper 패키지 생성 후 MemberMapperTests 클래스를 작성합니다.

MemberMapperTests 클래스

testRead( )에서 'admin99'에 대한 정보를 조회합니다. 정상적이면 MemberVO와 내부의 AuthVO가 구성된 것을 확인할 수 있습니다.


CustomUserDetailsService 구성

MyBatis를 이용해서 MemberVO와 같이 회원을 처리하는 부분이 구성되었다면 이를 이용해서 스프링 시큐리티의 UserDetailsService를 구현하는 클래스를 직접 작성하도록 합니다. 작성하려는 CustomUserDetailsService는 스프링 시큐리티의 UserDetailsService를 구현하고, MemberMapper 타입의 인스턴스를 주입받아서 실제 기능을 구현합니다.

com.project.security 패키지에 CustomUserDetailsService 클래스를 작성합니다.

CustomUserDetailService 클래스

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

 

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security
	 http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/beans
		 http://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<bean id="customAccessDenied" class="com.isaac.security.CustomAccessDeniedHandler"></bean>
	<bean id="customLoginSuccess" class="com.isaac.security.CustomLoginSuccessHandler"></bean>
	<!-- <bean id="customPasswordEncoder" class="com.isaac.security.CustomNoOpPasswordEncoder"></bean> -->
	<bean id="bcryptPasswordEncoder" 
		class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
	
	<bean id="customUserDetailsService"
		class="com.isaac.security.CustomUserDetailsService"></bean>
	
	<security:http auto-config="true" use-expressions="true">
		<security:intercept-url pattern="/sample/all" access="permitAll"/>
		<security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/>
		<security:intercept-url pattern="/sample/admin" access="hasRole('ROLE_ADMIN')"/>
		<!-- <security:access-denied-handler error-page="/accessError"/>  -->
		<security:access-denied-handler ref="customAccessDenied"/>
		<!-- <security:form-login/>  -->
		<security:form-login login-page="/customLogin" 
			authentication-success-handler-ref="customLoginSuccess"/>
		<!-- <security:csrf disabled="true"/>  -->
		<security:logout logout-url="/customLogout" invalidate-session="true"/>
	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider
			user-service-ref="customUserDetailsService">
			<security:password-encoder ref="bcryptPasswordEncoder"/>
		</security:authentication-provider>
	</security:authentication-manager>
	
</beans>

변경된 부분은 authentication-provider 속성의 값을 작성한 CustomUserDetailsService로 지정한 부분입니다. 프로젝트를 실행하고 다음과 같은 화면에서 로그인을 시도했을 때 지정된 로그가 출력되고, 의존성 주입 등이 정상적으로 처리되었는지 확인합니다.

 

MemberVO를 UserDetails 타입으로 변환하기

스프링 시큐리티의 UserDetailsService는 loadUserByUsername( )라는 하나의 추상 메서드만을 갖고 있으며 리턴 타입은 org.springframework.security.core.userdetails.UserDetails라는 타입입니다. 모든 작업에 문제가 없다면 최종적으로 MemberVO의 인스턴스를 스프링 시큐리티의 UserDetails 타입으로 변환하는 작업을 처리해야 합니다. UserDetails를 구현한 org.springframework.security.core.userdetails.User 클래스를 상속해서 CustomUser라는 클래스를 생성합니다.

com.project.security 패키지에 domain 패키지를 추가해서 CustomUser 클래스를 생성합니다.

CustomUser 클래스

CustomUser는 org.springframework.security.core.userdetails.User 클래스를 상속하기 때문에 부모 클래스의 생성자를 호출해야만 정상적인 객체를 생성할 수 있습니다. MemberVO를 파라미터로 전달해서 User 클래스에 맞게 생성자를 호출합니다. 이 과정에서 AuthVO 인스턴스는 GrantedAuthority 객체로 변환해야 하므로 stream( )과 map( )을 이용해서 처리합니다.

 

변경 후에는 CustomUserDetailsService에서 CustomUser를 변환하도록 수정해 봅니다.

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


스프링 시큐리티에서 커스텀 테이블, 커스텀 UserDetailsService 활용하는 방법에 대해 알아보았습니다.

728x90
반응형