▸Spring Security

스프링 Security_Remember-me 커스터마이징 [1/3]

코데방 2020. 4. 22.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.1.1 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

이 글에서는 간단한 컨텍스트 설정을 통한 커스터마이징입니다. 아예 새로운 로직을 적용하기 위한 커스터마이징은 아래 링크를 참조하시면 됩니다.

 

[Spring Security] - 스프링 Security_Remember-me 커스터마이징 [2/3]

[Spring Security] - 스프링 Security_Remember-me 커스터마이징(커스텀 필터) [3/3]

 

 

Remember-me에 관련된 필터 및 클래스는 아래와 같습니다. 필터 종류에서 인터페이스를 찾아 클래스명에 커서를 두고 F4를 누르면 상속 구조(Type Hierachy)를 볼 수 있습니다. 하나씩 상속 구조를 따라가보고 코드를 보면서 구조를 파악해보면 됩니다.

 

* RememberMeServices : 최상위 인터페이스

* InitializingBean : Bean 초기화 시 실행되는 메소드를 가진 인터페이스

* LogoutHandler : 로그아웃 필터 동작 시 실행되는 인터페이스

* AbstractRememberMeServices : 변하지 않는 기본 로직의 뼈대

* TokenBasedRememberMeServices : 추상 클래스를 상속받아 실제 핵심 로직을 작성

 

 

 


 

 

실행되는 메소드를 알았으니 전부 다 새로 만들어도 무방합니다. 실제로 전 잠시 본래의 기능을 오해해서 왜 이렇게 만들었지 하면서 새로 만들었습니다만.. 아예 새로 만들고보니 원래 기능이 눈에 다 들어와서 오해였다는 것을 알게 됐습니다. 뻘짓 했네요.. 

 

AbstractRememberMeServices 클래스의 코드를 살펴보면 일단 쿠키 이름과 파라미터 이름 등은 디폴트가 상수로 정의되어 있고 setter 메소드를 제공하고 있습니다. 따라서 컨텍스트 설정 파일에서 직접 Bean에 대한 설정을 한다면 Property 값을 조정해줄 수 있다는 것을 알 수 있습니다.

 

제가 고치고 싶은 부분은 '쿠키 이름', '파라미터 이름', '쿠키 유효시간' 입니다. 아래 세 필드값이고 setter가 있으므로 Bean을 직접 설정해줍니다. 지정해주지 않으면 아래 코드대로 디폴트 상수값이 들어갑니다.

private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
private String parameter = DEFAULT_PARAMETER;
private int tokenValiditySeconds = TWO_WEEKS_S;

 

실제 구현체는 Abstract 클래스를 상속한 PersistentTokenBasedRememberMeServices 클래스입니다. 생성자를 확인해보면 아래와 같습니다. 생성자도 같이 넣어서 Bean을 등록해주면됩니다.

    public PersistentTokenBasedRememberMeServices
    (String key, UserDetailsService userDetailsService,
                       PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService);
        random = new SecureRandom();
        this.tokenRepository = tokenRepository;
    }

 

 

생성자 중 PersistentTokenRepository 타입의 실제 구현체는 jdbcTokenRepositoryImpl 클래스입니다. 아래와 같은 구조이기 때문에 생성자로 넣어주기 위해서는 해당 클래스 또한 Bean으로 직접 만들어서 등록해줘야 합니다. 당연한 말이지만 이 클래스에서 실제 DB 작업을 하기 때문에 dataSource도 Property로 넣어줘야 합니다. 코드를 보면 생성자 대신 setter 메소드가 있는 것을 확인할 수 있습니다. 클래스 구조만 알면 코드를 열어 보고 구조를 파악하면 됩니다.

 

 

 

위 내용들을 적용한 최종 설정은 아래와 같습니다. 

<!-- Remember-me Property 셋팅 -->
<bean id="jdbcTokenRepositoryImpl"
	class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
	<property name="dataSource" ref="dataSource" />
</bean>
<bean id="persistentTokenBasedRememberMeServices"
	class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
	<constructor-arg name="key" value="codevang" />
	<constructor-arg name="userDetailsService" ref="userLoginDetailsService" />
	<constructor-arg name="tokenRepository" ref="jdbcTokenRepositoryImpl" />
	<property name="cookieName" value="HSWEB_U_REMEMBER" />
	<property name="parameter" value="remember-me" />
	<property name="tokenValiditySeconds" value="60000" />
</bean>

 

 


 

 

 

이제 remember-me 설정에 만들어진 Bean을 등록해주면 됩니다. dataSource ref는 DAO 클래스에 직접 주입해줬으므로 삭제하면 됩니다. 

 

<!-- 자동 로그인(커스텀) -->
	<s:remember-me services-ref="persistentTokenBasedRememberMeServices" key="codevang"
		authentication-success-handler-ref="userLoginSuccessHandler" />

 

 

여기서 주의해야할 점은 디폴트가 아닌 직접 Bean을 생성해서 주입할 경우에는 꼭 "key"값을 Bean 생성 시 생성자로 넣어줬던 "Key"값과 동일하게 셋팅해줘야 한다는 것입니다.

 

코드와 여러 번의 테스트를 종합해볼 때 RememberMeAuthenticationToken 권한 객체를 생성할 때와 이를 검증할 때 Key값을 이용합니다. 디폴트 설정에서야 둘이 같은 Key를 공유할테니 key 설정을 굳이 해주지 않아도 문제가 없지만 위 과정과 같이 직접 Bean을 만들면서 key값을 주입해줄 경우 검증하는 쪽의 디폴트 설정과 key값이 달라 검증 실패가 됩니다.

 

정리하면 PersistentTokenBasedRememberMeServices 클래스 생성자의 Key는 권한 객체를 생성할 때 사용되고, <remember-me> 설정에서 제공하는 key는 이를 정상적인 권한 객체가 맞는지 검증할 때 사용됩니다. 그리고 만약 키값이 달라서 검증에 실패했을 때 실행되는 메소드가 loginFail() 입니다. 이 메소드에서는 이미 발급된 쿠키와 DB에 업데이트된 Token 값을 삭제하는 역할을 합니다.

 

 

 

RememberMeAuthenticationToken 클래스의 코드를 보면 equals() 메소드가 오버라이딩 되어 있는데, 이 때 Key값을 해쉬코드로 변환해서 사용하는 것을 확인할 수 있습니다. key값은 여기서만 사용됩니다. 

   public boolean equals(Object obj) {
        if (!super.equals(obj)) {
            return false;
        }

        if (obj instanceof RememberMeAuthenticationToken) {
            RememberMeAuthenticationToken test = (RememberMeAuthenticationToken) obj;

            if (this.getKeyHash() != test.getKeyHash()) {
                return false;
            }

            return true;
        }

        return false;
    }

 

 


 

 

어쨌든 이것으로 기존 Remember-me의 프로퍼티만 약간 변경해서 쿠키 이름, 파라미터 이름, 유효시간 등을 변경하는 작업을 마쳤습니다. 로직 자체를 바꾸고 싶다면 위에서 다룬 클래스를 대체할 커스터마이징 클래스를 작성해서 주입해주면 됩니다. 이 과정도 다음 글에서 정리하도록 하겠습니다. 

 

아래는 전체 설정코드입니다.

<?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:s="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">


	<s:http auto-config="true" use-expressions="true">

		<!-- 정적 리소스는 모두 접근 허용 -->
		<s:intercept-url pattern="/resources/**" access="permitAll" />

		<!-- 로그인된 상태에서는 로그인이나 회원가입 화면에 접근 못하도록 함 -->
		<s:intercept-url pattern="/loginView" access="isAnonymous()" />
		<s:intercept-url pattern="/registerUserView" access="isAnonymous()" />

		<!-- 관리자페이지는 관리자만 접근 허용 -->
		<s:intercept-url pattern="/admin/**" access="hasRole('admin')" />

		<!-- 로그인 설정 -->
		<s:form-login username-parameter="userId" password-parameter="userPw"
			login-processing-url="/loginAsk" login-page="/loginView"
			authentication-failure-handler-ref="userLoginFailHandler"
			authentication-success-handler-ref="userLoginSuccessHandler" />

		<!-- 자동 로그인(커스텀) -->
		<s:remember-me services-ref="persistentTokenBasedRememberMeServices" key="codevang"
			authentication-success-handler-ref="userLoginSuccessHandler" />

		<!-- 권한이 없어서 금지된 URI 접속할 때 보여줄 페이지(403 에러 페이지 대체) -->
		<s:access-denied-handler error-page="/" />

		<!-- 로그아웃 설정 -->
		<s:logout logout-url="/logoutAsk" logout-success-url="/" invalidate-session="true"
			delete-cookies="true" />
	</s:http>

	<!-- Remember-me Property 셋팅 -->
	<bean id="jdbcTokenRepositoryImpl"
		class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
		<property name="dataSource" ref="dataSource" />
	</bean>
	<bean id="persistentTokenBasedRememberMeServices"
		class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
		<constructor-arg name="key" value="codevang" />
		<constructor-arg name="userDetailsService" ref="userLoginDetailsService" />
		<constructor-arg name="tokenRepository" ref="jdbcTokenRepositoryImpl" />
		<property name="cookieName" value="HSWEB_U_REMEMBER" />
		<property name="parameter" value="remember-me" />
		<property name="tokenValiditySeconds" value="60000" />
	</bean>


	<!-- DB 연동 커스터마이징 Bean -->
	<s:authentication-manager>
		<s:authentication-provider ref="userLoginAuthenticationProvider">
		</s:authentication-provider>
	</s:authentication-manager>

	<!-- 패스워드 단방향 암호화 -->
	<bean id="passwordEncoder"
		class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />

</beans>
728x90

댓글

💲 추천 글