본문 바로가기

Back-End/Spring

Spring Security - remember-me에 관하여 ( 로그인 유지하기 )

Remember-me architecture

로그인 기억하기를 구현할 때 굳이 전체 아키텍처를 이해하지 않아도 되지만, 커스터마이징이 많아질 수록 전체 흐름을 이해해야 구현하기 편해진다. 그래서 remember-me 전체 흐름도에 대해 알아보자. ( 해당 그림은 token 기반 remember-me의 흐름도다 ) 

스프링 시큐리티 필터들과 같이, RememberAuthenticationFilter가 FilterChainProxy에 포함되어 있다.  RememberAuthenticationFilter는 request를 보고 필요한 경우 특정 동작을 수행한다. RememberMeAuthenticationFilter인터페이스는 해당 사용자가 이미 로그인 한적이 있는지 확인하기 위해 RememberMeServices구현체를 사용한다.

RememberMeServices 인터페이스는 remember-me 쿠키를 가져오기 위해 HTTP request를 조회한다. 가져온 쿠키는 token 기반이든 persistent 기반이든 검증하기 위해서 사용 된다. 토큰이 check out 되면, 유저는 로그인이 된다.

 

Persistent 기반 remember-me는 어떻게 동작하는가?

persistent 기반 로그인 기억하기는 token 기반과 다르게 데이터베이스에 있는 token이 존재하는지 확인을 통해 유효성 검사를 한다. 각각의 persistent remember-me 쿠키들은 해당 내용을 포함한다.

  • Series identifier : 사용자의 초기 로그인을 식별하고, 원래 session에서 자동 로그인 한 경우 일관성을 유지하는 값이다.
  • Token value : remember-me 기능을 사용하여 로그인 한 경우 매번 갱신되는 고유한 값

  •  

Remember-me 와 user 라이프 사이클

RememberMeServices 구현체는 user 라이플 사이클에 따라 수행이 된다. ( authenticated user`s session )  remember-me 기능을 이해하기 위해서 언제 수행이 되는지 파악하는 것은 도움이 된다.

 

액션 어떻게 되냐? RememberMeServices 함수 실행
로그인 성공 remember-me 쿠키를 설정한다 loginSuccess
로그인 실패 쿠키를 취소한다. loginFailed
로그아웃 쿠키를 취소한다. logout

RememberMeServices가 user의 라이플사이클과 어떻게 연관되는지 이해하는 것은 custom authentication handler를 구현할 때 중요하다. 왜냐하면, 모든 인증 프로세서가 RememberMeServices들을 일관되게 처리하여 해당 기능의 유용함과 보안을 지켜줘야 한다.

 

Custom cookie 와 HTTP parameter 이름 정하기

기본 쿠키 이름이 remember-me로 정해져 있기 때문에 외부에서 Spring Security 사용하는 것을 알기 어렵게 하기 위해 cookie 이름이나 remember-me form 필드 checkbox 이름을 변경 할 수 있다.

 

        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        http.rememberMe()
               .key("jbcpCalendar")
               .rememberMeParameter("jbcpCalendar-remember-me")
               .rememberMeCookieName("jbcpCalendar-remember-me");
//src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        @Bean
        public RememberMeServices rememberMeServices
        (PersistentTokenRepository ptr){
           PersistentTokenBasedRememberMeServices rememberMeServices = new 
           PersistentTokenBasedRememberMeServices("jbcpCalendar", 
           userDetailsService, ptr);
           rememberMeServices.setParameter("obscure-remember-me");
           rememberMeServices.setCookieName("obscure-remember-me");
           return rememberMeServices;
        }

login.html에서 checkbox의 이름도 변경해줘야 한다.

//src/main/resources/templates/login.html

<input type="checkbox" id="remember" name=" obscure-remember-me" 
value="true"/>

이제 실험해보면 된다!

 

CookieTheftException

SEVERE: Servlet.service() for servlet [springMvcServlet] in context with path [/brate] threw exception
org.springframework.security.web.authentication.rememberme.CookieTheftException: Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
    at org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices.processAutoLoginCookie(PersistentTokenBasedRememberMeServices.java:102)

인증이 필요한 요청을 동시에 해서 먼저 처리된 요청은 remember-me token이 update 되면서 성공을 하고, 다른 요청은 update된 remember-me token과 cookie에 담아 보낸 remember-me cookie와 일치하지 않아 위와같은 exception이 발생한다.

 

그렇다면 어떻게 해결을 해야할까?

 

이슈 1 . permitAll() API 이지만 controller에서 인증 객체 받는 경우도 있다면?

제목으로 표현하려니 뭔가 이상한데 직접 문제에 대한 코드를 보자.

@GetMapping("/{goodsId}")
public ResponseEntity<ApiResponse> getGoodsById(
  @PathVariable(value = "goodsId") Integer goodsId,
  @CurrentUser WaugUserDetail waugUserDetail,
  @RequestParam(value = "locale") Locale locale,
  @RequestParam(value = "currency") String currency, HttpServletRequest request)
  throws Exception {
	// ....

해당 코드는 로그인을 하면 유저의 정보를 이용하여 특정 동작을 하고, 아닌 경우에는 없이 동작을 한다. 그렇기 때문에 Security 설정 부분에는 permitAll()로 설정을 해놓았다. 그러면서 Spring Security Filter들 ( FilterProxyChain ) 이 동작하지 않으면서, 로그인이 되어 있더라도 SecurityContextPersistenceFilter가 동작하지 않았다.

이전 request에서 생성된 인증 정보를 조회해오는 SecurityContextPersistenceFilter

그렇기 때문에 로그인이 되어 있더라도 @CurrentUser ( @Authentication... 그 어노테이션을 확장하였음 ) 에는 인증 정보가 담기지 않는 문제가 발생하였다.  

 

This is achieved without disabling the security filters
 – these still run, so any Spring Security related functionality will still be available.

알고보니 다른 개발자가 해당 api를 ignoring() 설정을 해놔서 타지 않는거 였다... 삽질 끝!

 

이슈 2. 특정 url 는 SecurityContextPersistenceFilter에 인증객체가 생성되어 있지 않는 문제

로그인 후에 특정 Url로 호출하는 경우에 SecurityContextPersistenceFilter 에서 이전에 인증된 정보를 가져오지 못하는 경우가 발생하였다. 때문에 이후에 RememberMeFilter쪽까지 영향이 가게 되었고 해당 이슈를 해결하기 위해 분석을 해봤다.

 

 

이건 login이 성공했을 때 내가 SecurityContextHolder에 Authentication을 설정해줬엉야 했는데 해주지 않아 생긴 문제 였다.

요약

Spring Security의 remember-me(로그인 유지하기) 의 기능에 대해 공부했다. 기본적인 설정부터 좀 더 보안적인 부분은 어떻게 신경써야 하는지에 대해 보았다. persistent 기반 로그인 기억하기를 어떻게 설정하고 어떤 부분이 token 기반 로그인 기억하기보다 나은지에 대해서도 살펴보았다. 

 

또 한번 스프링 시큐리티의 확장성에 대해 놀랐다....

'Back-End > Spring' 카테고리의 다른 글

Spring을 이용하여 개발할 때 고민, 클린코드 짜기, 코드리뷰 항목  (0) 2020.01.02
Spring @Order 어노테이션  (0) 2019.11.28
Jackson 관련  (0) 2019.10.11
AOP란  (0) 2019.06.20
Spring MVC에 관하여  (0) 2019.06.07