로그아웃 이벤트 프린트를 위한 코드
@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에서 TokenBasedRemeberMeServices
와 PersistenceTokenBasedRememberMeServices
로 나뉨
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이 발생하게 됨