[Spring Security] RemeberMe 토큰 저장

WOOK JONG KIM·2022년 12월 2일
0

패캠_java&Spring

목록 보기
80/103
post-thumbnail

로그아웃 이벤트 프린트를 위한 코드

 @Bean
   public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
       return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher(){
           @Override
           public void sessionCreated(HttpSessionEvent event) {
               super.sessionCreated(event);
               System.out.printf("===>> [%s] 세션 생성됨 %s \n", LocalDateTime.now(), event.getSession().getId());
           }

           @Override
           public void sessionDestroyed(HttpSessionEvent event) {
               super.sessionDestroyed(event);
               System.out.printf("===>> [%s] 세션 만료됨 %s \n", LocalDateTime.now(), event.getSession().getId());
           }

           @Override
           public void sessionIdChanged(HttpSessionEvent event, String oldSessionId) {
               super.sessionIdChanged(event, oldSessionId);
               System.out.printf("===>> [%s] 세션 아이디 변경  %s:%s \n",  LocalDateTime.now(), oldSessionId, event.getSession().getId());
           }
       });
   }

loginForm.html 코드 일부

<label>
                    <input type="checkbox" name="remember-me"> 로그인 기억하기
</label>

remeber-me가 true로 서버에 올라오게 되면 UserNamePasswordAuthenticationFilter(remeberme filter보다 앞단)에서 Rememberme check가 들어왔기 때문에 인증에 성공하게 되면 RemebermeAuthenticationService에게 인증이 Successful 했다는 것을 notify

-> 이때 쿠키에 RememberMe 쿠키를 저장해서 응답이 가게 됨

decode

user1:1671173917935:cf525f4aba306beb079

세션이 만료된 후 다른 페이지로 접속하고자 할 때 SecurityContextPersistenceFilter가 동작을 하지 않을 것이기에 RemebermeAuthenticationFilter 쪽에서 Request를 캐치 하게 됨

RemembermeAuthenticationFilter.java

Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);

remebermeServices에 autoLogin을 요청하게 됨

이후 상위 클래스인 AbstractRememberMeServices의 autoLogin 메서드

	@Override
	public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
		String rememberMeCookie = extractRememberMeCookie(request);
		if (rememberMeCookie == null) {
			return null;
		}
		this.logger.debug("Remember-me cookie detected");
		if (rememberMeCookie.length() == 0) {
			this.logger.debug("Cookie was empty");
			cancelCookie(request, response);
			return null;
		}
		try {
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
			this.userDetailsChecker.check(user);
			this.logger.debug("Remember-me cookie accepted");
			return createSuccessfulAuthentication(request, user);
		}
        ...
        
}

try 문 안의 remebermeCookie는 클라이언트에서 올라온 값

코드 처럼 Base 64로 decode를 진행 후 값

processAutoLoginCookie에서 TokenBasedRemeberMeServicesPersistenceTokenBasedRememberMeServices로 나뉨

processAutoLoginCookie

if (isTokenExpired(tokenExpiryTime)) {
			throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
					+ "'; current time is '" + new Date() + "')");
		}
		// Check the user exists. Defer lookup until after expiry time checked, to
		// possibly avoid expensive database call.
		UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
		Assert.notNull(userDetails, () -> "UserDetailsService " + getUserDetailsService()
				+ " returned null for username " + cookieTokens[0] + ". " + "This is an interface contract violation");
                
    ....
    
    String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
				userDetails.getPassword());
                
    // 이후 코드 밑에 계속

만료 시간 체크 이후 UserId를 기반으로 UserDetailsPrincipal를 가져와서 서명을 진행하게 됨

if (!equals(expectedTokenSignature, cookieTokens[2])) {
			throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
					+ "' but expected '" + expectedTokenSignature + "'");
		}
		return userDetails;

이때 같은 서버에서 서명을 하였기에, 클라이언트에서 올라왓을때의 서명 값과 같을 것

이후 AutoLogin 메서드 코드에서의 userDetailsChecker 메서드를 통해 UserDetails를 체크 후 createSuccessfulAuthentication이라는 통행증이 발급 됨

이후 RemembermeAuthenticationFilter의 RememberMeAuth는 True가 됨

이런 원리로 재로그인없이도 메인페이지 들어갈 수 있다
-> 세션이 유지되는 동안에는 세션 필터를 탐

하지만 로그인 하지않고 토크만 탈취해서 유저페이지같이 인증된 페이지 접속이 가능함..(양쪽에서 접속도 가능 , 유저 + 탈취자)
-> 이를 해결할려면 비밀번호 바꿔야 함(서명 값이 달라짐)
-> 매번 바꾸는것이 불편하기에 PersistenceTokenBased에서는 토큰을 서버에 저장하는 방식 사용


PersistenceTokenBasedRemebermeService 사용 예시

Security.config

...
				.logout(logout->
                        logout.logoutSuccessUrl("/"))
                .exceptionHandling(error->
                        error.accessDeniedPage("/access-denied")
                )
                .rememberMe(r -> r.rememberMeServices(rememberMeServices()))
                ;
                
                // configure에 우리가 사용하고자 하는 remembermeServices 지정
                
                
                
    @Bean
    PersistentTokenRepository tokenRepository(){
    JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();

        // 데이터 소스 주입해줘야 함
        repository.setDataSource(dataSource);
        
        // 테이블이 없으면 테이블 생성하도록
        try{
            repository.removeUserTokens("1"); // 테이블이 없으면 에러 뜨게 끔
        }catch(Exception ex){
            repository.setCreateTableOnStartup(true);
        }
        return repository;
    }
    @Bean
    PersistentTokenBasedRememberMeServices rememberMeServices(){
        PersistentTokenBasedRememberMeServices service = new PersistentTokenBasedRememberMeServices("hello",
                spUserService, tokenRepository());
        return service;
    }

코드 설정 후부터는 token을 Db에 저장하게됨
-> h2 console을 보면 Persistent_Logins라는 테이블이 생길 것!

세션이 만료된 후 다시 메인으로 갈려할 때 series와 token값이 쿠키에 묻어서 올라감

코드에서 series값을 이용하여 db에 있는 remembermeToken을 select 해옴

local에서 올라온 토큰값과 디비의 토큰값이 같은지 확인 함
-> 연속된 토큰을 발급하기 위해

확인되면 새로운 토큰을 발급하여 Db에 업데이트 시켜주고 , addCookie를 통해 브라우저에 있는 토큰도 해당 시리즈의 새로운 토큰 값으로 내려줌
-> 유저는 자동 로그인 가능

토큰 값을 갱신하는 이유
-> 악의적인 사용자가 토큰값을 탈취해서 접속할려 할 시 서명이 유효하기에 들어올 수 있다, 그러나 탈취자가 디비의 토큰값 까지 바꿔 놓았기 때문에 기존 사용자는 재로그인을 할려고 했을 때 db와 로컬의 토큰 불일치가 발생하게 됨
-> 시리즈의 사건을 생긴걸로 서버가 인지하여 유저에게 발급된 모든 RemembermeToken을 삭제하면서 Exception이 발생하게 됨

profile
Journey for Backend Developer

0개의 댓글