
그래서 session 을 사용하건 사용하지 않건
같은 Authentication과 uthenticationProvider 를 사용할 수 있습니다.

스프링의 인증 유지
session 을 이용하는 것이 개발자에게는
여러모로 편리합니다.
이후 인증은 서버가 메모리를 소모해서 세션객체를 가지고 유지시켜주는 작업을 하도록 할 수 있습니다.
세션은 브라우저의 쿠키에 JSESSIONID 값을 심어놓고, 브라우저와 서버가 이 값을 주고 받으면서 세션을 보장받을 수 있습니다.
그런데 서버의 세션 정책과 스프링의 인증 체계가 서로 맞물려 동작하도록 하려면
인증을 보조해 주는 다른 필터들의 도움을 받아야 합니다.
SecurityContextPersistenceFilter ,
RememberMeAuthenticationFilter,
AnonymousAuthenticationFilter
AuthenticationProvider가 여권에 도장을 찍어주듯이 통행증을 찍어주면,
그 출입증을 가지고 SecurityContextHolder에 통행증이 저장되고 정상적으로 request 요청이 수행됩니다.
준비사항
## build.gradle
## 보통 자동로그인이 되는 경우가 많아서 developmentOnly 를 끈다.
____________________________________________________________
configurations {
//developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
}
## application.yml
____________________________________________________________
server:
port: 9056
servlet:
session:
timeout: 60s
spring:
thymeleaf:
prefix: classpath:/templates/
cache: false
check-template-location: true
h2:
console:
enabled: true
path: /h2-console
datasource:
url: jdbc:h2:file:~/dev/temp/h2db/rememberme;
driverClassName: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
generate-ddl: true
hibernate:
ddl-auto: update
각각의 모듈에서 스캔하는 코드를 적어 준것을 통합한다.
UserDetailsTestApplication.java 기존코드
______________________________
@SpringBootApplication(scanBasePackages = {
"com.sp.fc.user",
"com.sp.fc.web"
})
@EntityScan(basePackages ={
"com.sp.fc.user.domain"
})
@EnableJpaRepositories(basePackages= {
"com.sp.fc.user.repository"
})
public class UserDetailsTestApplication {
public static void main(String[] args) {
SpringApplication.run(UserDetailsTestApplication.class, args);
}
}
UserDetailsTestApplication.java 코드 변경됨
____________________________________________________________
@SpringBootApplication(scanBasePackages = {
"com.sp.fc.config",
"com.sp.fc"
})
public class UserDetailsTestApplication {
public static void main(String[] args) {
SpringApplication.run(UserDetailsTestApplication.class, args);
}
}
comp/user-admin/src/main/java/com/sp/fc/config/UserAdminModule.java
____________________________________________________________
package com.sp.fc.config;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@ComponentScan("com.sp.fc.user")
@EntityScan(basePackages = {
"com.sp.fc.user.domain"
})
@EnableJpaRepositories(basePackages = {
"com.sp.fc.user.repository"
})
public class UserAdminModule {}
Security Filter Chain

- HeaderWriterFilter : Http 해더를 검사한다. 써야 할 건 잘 써있는지, 필요한 해더를 더해줘야 할 건 없는가?
- CorsFilter : 허가된 사이트나 클라이언트의 요청인가?
- CsrfFilter : 내가 내보낸 리소스에서 올라온 요청인가?
- LogoutFilter : 지금 로그아웃하겠다고 하는건가?
- UsernamePasswordAuthenticationFilter : username / password 로 로그인을 하려고 하는가? 만약 로그인이면 여기서 처리하고 가야 할 페이지로 보내 줄께.
- ConcurrentSessionFilter : 여거저기서 로그인 하는걸 허용할 것인가?
- BearerTokenAuthenticationFilter : Authorization 해더에 Bearer 토큰이 오면 인증 처리 해줄께.
- BasicAuthenticationFilter : Authorization 해더에 Basic 토큰을 주면 검사해서 인증처리 해줄께.
- RequestCacheAwareFilter : 방금 요청한 request 이력이 다음에 필요할 수 있으니 캐시에 담아놓을께.
- SecurityContextHolderAwareRequestFilter : 보안 관련 Servlet 3 스펙을 지원하기 위한 필터라고 한다.(?)
- RememberMeAuthenticationFilter : 아직 Authentication 인증이 안된 경우라면 RememberMe 쿠키를 검사해서 인증 처리해줄께
- AnonymousAuthenticationFilter : 아직도 인증이 안되었으면 너는 Anonymous 사용자야
- SessionManagementFilter : 서버에서 지정한 세션정책을 검사할께.
- ExcpetionTranslationFilter : 나 이후에 인증이나 권한 예외가 발생하면 내가 잡아서 처리해 줄께.
- FilterSecurityInterceptor : 여기까지 살아서 왔다면 인증이 있다는 거니, 니가 들어가려고 하는 request 에 들어갈 자격이 있는지 그리고 리턴한 결과를 너에게 보내줘도 되는건지 마지막으로 내가 점검해 줄께.
- 그 밖에... OAuth2 나 Saml2, Cas, X509 등에 관한 필터들도 있습니다.
- 필터는 넣거나 뺄 수 있고 순서를 조절할 수 있습니다. (이때 필터의 순서가 매우 critical 할 수 있기 때문에 기본 필터들은 그 순서가 어느정도 정해져 있습니다.)
1. SecurityContextPersistenceFilter


SecurityContextRepository 에 저장된 SecurityContext 를 Request의 LocalThread에 넣어주었다가 뺏는 역할을 한다. doFilter 메소드를 따라가보면 알 수 있다. 세션에 SecurityContext를 보관했다가 다음 request에서 넣어줍니다.
한번 로그인을 하고나면, 이후에는 세션에 저장된 SecurityContext을 가져다가 SecurityContextHolder에 넣어주어 전반적인 요청에 SecurityContext를 사용할 수 있도록 해준다. SecurityContextRepository 저장소는 HttpSessionContextSecurityHolder를 저장한다. 작업이 끝나면 SecurityContextHolder를 clear해주기 떄문에 ThreadLocal에서만 안전하도록 보장을 해준다.
2. RememberMeAuthenticationFilter
인증 정보를 세션 관리하는 경우, 세션 timeout이 발생하게 되면,
remember-me 쿠키를 이용해 로그인을 기억했다가 자동으로 재로그인 시켜주는 기능
|
2-1. TokenBasedRememberMeServies
TokenBasedRememberMeServies 는 토큰 기반으로 브라우저에 저장한다. 서버에는 남기지 않는다. 이 방식이 기본 설정이다. 아이디:만료시간:비밀번호:인증키 형태로 포맷이 되어있다. 토큰을 탈취당하면 아이디, 만료시간, 비밀번호 등의 정보가 노출되며 탈취 당했는지 여부를 확인하기 힘들다. 탈취를 무효화 시키는 방법은 비밀번호 변경밖에 없으므로 보안에 취약하다.
|
TokenBasedRememberMeServies 테스트
## application.yml
## 세션 유효시간은 60초
server:
port: 9056
servlet:
session:
timeout: 60s
## server / src / main/ java/ con.sp.fc.web/config/SecurityConfig.java
## ServletListener로 세션의 생성, 만료, 아이디 변경등을 감지하는 동작들을 빈으로 등록
________________________________________________________________________________________
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final SpUserService spUserService;
public SecurityConfig(SpUserService spUserService) {
this.spUserService = spUserService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(spUserService);
}
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return roleHierarchy;
}
@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());
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(request->
request.antMatchers("/").permitAll()
.anyRequest().authenticated()
)
.formLogin(login->
login.loginPage("/login")
.loginProcessingUrl("/loginprocess")
.permitAll()
.defaultSuccessUrl("/", false)
.failureUrl("/login-error")
)
.logout(logout->
logout.logoutSuccessUrl("/"))
.exceptionHandling(error->
error.accessDeniedPage("/access-denied")
).rememberMe(); // .rememberMe(); 추가됨
;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.requestMatchers(
PathRequest.toStaticResources().atCommonLocations(),
PathRequest.toH2Console() ## h2 콘솔path를 웹 리소스 처럼 열어 줘야 한다.
)
;
}
}
이 토큰이 작동하는 방식을 확인하기 위해서
RememberMeAuthentiationFilter
TokenBasedRememberMeServices
PersistentTokenBasedRememberMeServices
## RememberMeAuthentiationFilter
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
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
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;
}
}
}
}
SecurityContextHolder에 인증정보가 없다면 autoLogin을 시도한다.
해당 메서드는 AbstractRememberMeServices 클래스에 정의되어 있다.
## AbstractRememberMeServices
## autoLogin메서드는 rememberMeCookie를 검사해서 없다면 null을 리턴하지만,
## 있다면 decode로 UserDetails를 획득한다.
## user는 아이디, 만료시간, 서명값 이 담긴 cookieTokens의 정보를 가지고 있다.
@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);
//아이디, 만료시간, 서명값은 String[] cookieTokens = decodeCookie(rememerMeCookie); 로
//얻었다. UserDetails를 리턴하는 유저 정보를 담는 정보들은 processAutoLoginCookie에서 만들어진다.
}
현재는 processAuttoLoginCookie 메서드는 현재 토큰 서비스가
TokenBasedRememerMeServices로 설정이 되어있어서 해당 클래스에서 수행된다.
( rememberme 토큰 방식에 따라 이 메서드부터 분기가 이루어진다. )
만약 설정을 바꿔서 PersistenceTokenBasedRememberMeServices 클래스로 rememberme를
구현하면 해당 클래스에서 진행한다.
TokenBaseRememberMeServices에 정의된 processAutoLoginCookie
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) {
if (cookieTokens.length != 3) {
throw new InvalidCookieException(
"Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
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");
// Check signature of token matches remaining details. Must do this after user
// lookup, as we need the DAO-derived password. If efficiency was a major issue,
// just add in a UserCache implementation, but recall that this method is usually
// only called once per HttpSession - if the token is valid, it will cause
// SecurityContextHolder population, whilst if invalid, will cause the cookie to
// be cancelled.
String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
userDetails.getPassword());
if (!equals(expectedTokenSignature, cookieTokens[2])) {
throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
+ "' but expected '" + expectedTokenSignature + "'");
}
return userDetails;
}
//isTokenExpired()로 토큰 만료시간을 확인하고 UserDetails를 획득한다.
//getUserDetailsService().loadUserByUsername(cookieTokens[0])를 통해
//로그인기록이 있는 아이디와 비밀번호를 얻을 수 있다.
## TokenBaseRememberMeServices에 정의된 loadUserByUsername 구현 방식
## 아이디만을 가지고 비밀번호를 얻을 수 있는 loadUserByUsername,
## 따라서 탈취당하면 계속 탈취한 아이디로 브라우저 사용이 가능하다
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (this.delegate != null) {
return this.delegate.loadUserByUsername(username);
}
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
for (AuthenticationManagerBuilder delegateBuilder : this.delegateBuilders) {
this.delegate = delegateBuilder.getDefaultUserDetailsService();
//getDefaultUserDetailsService()는 UserDetailsService를 구현한
//SpUserService로 인식된다.
if (this.delegate != null) {
break;
}
}
if (this.delegate == null) {
throw new IllegalStateException("UserDetailsService is required.");
}
this.delegateBuilders = null;
}
}
return this.delegate.loadUserByUsername(username); // username을 이용해서
// DB저장소에서 조회해온다.
}
## SpUserServices는 loadUserByUsername을 다음과 같이 정의한다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return spUserRepository.findUserByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
}
## loadUserByUsername으로 불러온 아이디와 비밀번호가 유효한지 토큰을 만들어서
## 쿠키에 있는 값과 비교한다.
protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return new String(Hex.encode(digest.digest(data.getBytes())));
}
catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("No MD5 algorithm available!");
}
}
//makeTokenSignature로 서명값 만든다.
//해당 메서드도 TokenBaseRememberMeServices 클래스에 정의되어 있다.
//각 remember-me 구현 방식에 따라 서명을 만드는 방식도 다르다.
//만들어진 서명값은 remember-me 토큰을 decode해서 나온 3번째 문자열과 같은 값을
//가져야만 최종적으로 인증이 된다.
//username:tokenExpiryTime:password:getKey()로 조합된 토큰을 생성한다.
//getKey()값은 현재 서버에서 만들어주는데, 가능한 지정해주는 것이 좋다.
//서버를 재시작해서 key값이 바뀌면 그동안 remember-me 설정이 모두 초기화된다.
//autoLogin이 정상적으로 끝나면 리턴받은 rememberAuth는 통행증이 발급(authenticated =true) 된다.
//따라서 사용자는 재로그인 없이 바로 권한확인 이후 원하는 페이지로 이동할 수 있다.
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
// autoLogin으로 리턴받은 rememberMeAuth 인증은 AuthenticationManager를 통해서
// 다시한번 인증을 완료하다.
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
// 마무리로 SecurityContextHolder에 저장하면 로그인 상태가 유지된다.
// 세션이 유효하면, remember-me 필터는 거치지 않고 세션 필터만 거친다.
// 필터가 탈취된다면, 탈취한 remember-me 쿠키값을 본인의 브라우저에 저장하고
// 로그인이 필요한 페이지로 이동을 하면된다.(makeTokenSignature에서 같은 서명값이 인증되서)
// 비밀번호를 바꾸면 해결할 수 지만, TokenBasedRememberMeServices는
// remember-me 쿠키값이 탈취되면 굉장히 보안에 취약하다는 사실을 알 수 있다.
// remember-me 토큰은 어떤 사용자가 요청하던지 똑같은 방식으로 서명을 만들기 때문에
// 같은 서명값이 있어서 유효한 사용자로 인식이 된다.
## PersistentTokenBasedRememberMeServices
hello는 서명값을 만들 때 key값으로 고정하며,
spUserService는 계정정보를 조회해 올 서비스이고
tokenRepository는 remember-me 토큰 정보를 저장할 저장소이다.
@Bean
PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices() {
return new PersistentTokenBasedRememberMeServices(
"hello", spUserService, tokenRepository());
}
__________________________________________________________
## serires와 token을 저장 할 tokenRepository를 빈으로 등록한다.(JPA 저장소를 만들어도 된다)
// JdbcTokenRepositoryImpl을 저장소로 사용한다.
// Jdbc기반의 영구적 로그인 토큰 저장소를 구현한 클래스이다.
@Bean
PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
// public class JdbcTokenRepositoryImpl extends JdbcDaoSupport
// implements PersistentTokenRepository {}
// public static final String CREATE_TABLE_SQL =
// "create table persistent_logins ("
// + "username varchar(64) not null, "
// + "series varchar(64) primary key, "
// + "token varchar(64) not null, "
// + "last_used timestamp not null)";
repository.setDataSource(dataSource);
// JdbcDaoSupport를 상속하기 때문에 Datasource를 주입
try {
repository.removeUserTokens("1");
// 처음 테이블을 만들 때는, 해당 변수를 true로 만들어주어야 하기 때문에
// try-catch에서 removeUserTokens()를 이용해 예외를 발생시키고
// createTableOnStartup을 true로 만들어준다.
} catch(Exception ex) {
repository.setCreateTableOnStartup(true);
// 최초에 테이블 생성을 위해 initDao를 실행하며
// createTableOnStartup이 true일 때, 테이블을 생성
// protected void initDao() {
// if (this.createTableOnStartup) {
// getJdbcTemplate().execute(CREATE_TABLE_SQL);
// }
// }
}
return repository;
}
## ememberMe 서비스를 persistentTokenBasedRememberMeServices()로 바꾼다.
.rememberMe(r->
r.rememberMeServices(persistentTokenBasedRememberMeServices())
);
deocde 한 remember-me 토큰이 2개로 나뉜다.
processAutoLoginCookie에서 PeristentTokenBaseRememberMeServices 클래스를 이용한다.
쿠키에 담긴 remember-me 토큰이 2개의 정보로 이루어져 있어서
서명 방식이 달라지기 때문이다.
첫번째 값을 Series, 두번째 값을 token이라고 한다.
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2 + " tokens, but contained '"
+ Arrays.asList(cookieTokens) + "'");
}
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
// Series 값으로 저장소에 저장되어 있는 token값을 조회
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
// No series match, so we can't authenticate using this cookie
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
}
// 현재 나의 토큰값과 저장소에 있는 토큰값이 같지 않으면,
// 저장소에 있는 토큰값을 삭제하고 예외를 발생시킨다
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete all logins for this user and throw
// an exception to warn them.
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage(
"PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
// 토큰 만료시간을 검사해서 만료되었다면 예외를 발생
if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'",
token.getUsername(), token.getSeries()));
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
generateTokenData(), new Date());
try {
// 새로 만든 토큰으로 저장소를 업데이트하고 쿠키에도 반영
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
addCookie(newToken, request, response);
}
catch (Exception ex) {
this.logger.error("Failed to update token: ", ex);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
// 로그인을 하면 다음과 같이 쿠키에서 전송한 remeber-me 토큰을 기반으로 series,
// token와 정보들을 이용해 서버 저장소에 저장된다.
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
series값을 통해 token값을 조회해서 현재 내가 가지고 있는 토큰값과 일치하는지 확인하는 과정이
중요하다. remember-me를 통해 세션을 다시 획득했을 때, token값과 series값은 계속 바뀐다.
따라서 다른 곳에서 탈취했을 때에 token값과 serires 값이 계속 바뀐다.
본래 계정에서 세션이 만료된 이후, 로그인하거나 로그인이 필요한 서비스를 요청하면
남아있는 remember-me 토큰의 token값과 series값과 저장소에 저장된 token을 찾아
비교하는 과정에서 정보가 다르면, 토큰이 탈취된 것으로 간주하여 모든 토큰관련 정보들이
삭제되고 처음부터 다시 로그인해야한다.
로그아웃 하면, remember-me 쿠키는 삭제된다.
2-2. PersistenceTokenBasedRememberMeServies
PersistenceTokenBasedRememberMeServies 서버에 토큰을 저장해서 이용한다. 보안에 좀 더 유리하다. 포맷은 series:token을 저장한다.(series는 사실상 key값이다.) 유저네임과 만료시간이 노출되지 않는다. series를 사용하는 이유는 다양한 브라우저로 로그인을 했을 떄, 브라우저마다 다른 값으로 인식하기 위해서이다. 브라우저에 로그인 할 때마다 series 값이 바뀌는데, 마지막에 로그인 한 계정의 series값을 기준으로 token값을 검사하여 탈취여부를 판단하므로 좀 더 안전하다.
|
2-3. 특징
- Remeberme로 로그인한 사용자는 UsernamePasswordAuthenticationToken 이 아닌 RememberMeAuthenticationToken 으로 서비스를 이용하는 것입니다. 같은 사용자이긴 하지만, 토큰의 종류가 다르게 구분되어 있습니다.
3. AnonymousAuthenticationFilter
- 로그인 하지 않은 사용자 혹은 로그인이 검증되지 않은 사용자는 기본적으로 AnonymousAuthenticationToken 을 발급해 줍니다. anonymous를 허용한 곳만 다닐수 있습니다.
- 익명사용자의 권한을 별도로 설정할 수 있고, 익명사용자에게 주는 principal 객체도 설계해서 줄 수 있습니다.