티스토리 뷰

728x90

인증 API - Remember Me 인증과 RememberMeAuthenticationFilter

 

Remember Me 기능이란?

쇼핑몰 같은 사이트 들어가 보면 자동 로그인 또는 사용자 아이디 기억하기 이런 기능과 유사한 기능이라고 생각하면 된다.

사용자는 요청시에 Http Header에 Remember me 토큰을 보낸다.

 

(1) 세션이 만료되고 웹 브라우저가 종료된 후에도 애플리케이션이 사용자를 기억하는 기능이다.

(2) Remember Me 쿠키에 대한 Http 요청을 확인한 후 토큰 기반 인증을 사용해 유효성을 검사하고 토큰이 검증되면 사용자는 로그인된다.

tokenValiditySeconds는 토큰의 만료시간을 지정해줄 수 있다. 기본은 14일이다.

user계정을 rememberMe 인증 시 처리하는 과정이 있는데 그 처리를 위해서 필요한 API가 userDetailsService이다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .rememberMe()  //기본 파라미터명은 remember-me
        .tokenValiditySeconds(3600) //default는 14일
        .alwaysRemember(true) //리멤버 미 기능이 활성화되지 않아도 항상 실행
        .userDetailsService(new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                return null;
            }
        });
}

서버를 기동 하면 아래와 같은 remember-me 체크박스가 추가된 화면을 볼 수 있다.

우리가 이전에 배웠던 것을 생각해서 로그인 인증을 받았다고 생각해보자

(1) 인증에 성공했다는 것은 서버에 사용자의 세션이 생성되었다는 이야기이고 세션이 성공한 인증 객체를 담고 있다. Security Context에 인증된 정보가 저장되어 있다.

(2) 그리고 서버는 세션을 생성할 때 만든 ID (JSESSIONID)를 클라이언트에게 응답 헤더에 담아서 보내준다.

(3) 이후에 클라이언트가 서버에 요청을 할 때 JSESSIONID를 쿠키에 담아서 보내기 때문에 별다른 인증 없이 사용할 수 있는 것이다.

(4) 그런데 사용자가 COOKIE에서 JSESSIONID를 임의로 삭제하고 다시 재인증을 요청하면 인증이 풀렸기 때문에. 서버는 처음 접속한 사용자라고 생각하기 때문에 로그인 페이지로 튕겨버린다.

(5) 여기서 이제 스프링 부트 시큐리티에서 remember-me API를 사용해서 체크한 후에 로그인을 하면 서버가 remember-me라는 쿠키를 생성해서 발급해준다.

(6) 그리고 임의로 jsessionid를 삭제하고 다시 새로고침 또는 재인증을 하면 로그인 페이지로 튕기지 않는다.

(7) 왜? remember-me라는 쿠키 값이 있기 때문에 그렇다. 스프링 부트 시큐리티가 jsessionid가 없어도 remember-me 라는 쿠키값이 있는지 없는지 체크해서 다시 재인증을 받을 필요가 없는 것이다.

이 체크하는 클래스가 Remember Me 인증 필터 : RememberMeAuthenticationFilter 이다.

RememberMeAuthenticationFilter 가 실행되는 조건

1. Security Context의 인증 객체가 null 일 때

2. 사용자가 remember-me 쿠키를 가지고 요청을 하는 경우

RememberMeAuthenticationFilter 소스

public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {

	private ApplicationEventPublisher eventPublisher;

	private AuthenticationSuccessHandler successHandler;

	private AuthenticationManager authenticationManager;

	private RememberMeServices rememberMeServices;

	public RememberMeAuthenticationFilter(AuthenticationManager authenticationManager,
			RememberMeServices rememberMeServices) {
		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
		Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
		this.authenticationManager = authenticationManager;
		this.rememberMeServices = rememberMeServices;
	}

	@Override
	public void afterPropertiesSet() {
		Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
		Assert.notNull(this.rememberMeServices, "rememberMeServices must be specified");
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (SecurityContextHolder.getContext().getAuthentication() != null) {
			this.logger.debug(LogMessage
					.of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
							+ SecurityContextHolder.getContext().getAuthentication() + "'"));
			chain.doFilter(request, response);
			return;
		}
		Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
		if (rememberMeAuth != null) {
			// Attempt authenticaton via AuthenticationManager
			try {
				rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
				// Store to SecurityContextHolder
				SecurityContext context = SecurityContextHolder.createEmptyContext();
				context.setAuthentication(rememberMeAuth);
				SecurityContextHolder.setContext(context);
				onSuccessfulAuthentication(request, response, rememberMeAuth);
				this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'"));
				if (this.eventPublisher != null) {
					this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
							SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
				}
				if (this.successHandler != null) {
					this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
					return;
				}
			}
			catch (AuthenticationException ex) {
				this.logger.debug(LogMessage
						.format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
								+ "rejected Authentication returned by RememberMeServices: '%s'; "
								+ "invalidating remember-me token", rememberMeAuth),
						ex);
				this.rememberMeServices.loginFail(request, response);
				onUnsuccessfulAuthentication(request, response, ex);
			}
		}
		chain.doFilter(request, response);
	}
    
    ....

}

 

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함