https://icanchangeworld.tistory.com/170
SpringApplication.run 이후 초기화
=> 스프링 시큐리티 강의를 듣다가 서블릿 필터 단에서 동작하는 시큐리티 빈 추가 키워드가 나와 정리해본다.1. SpringApplication.run(...) 호출 시 public ConfigurableApplicationContext run(String... args) { Startup s
icanchangeworld.tistory.com
=> 이 포스팅에서 스프링 시큐리티는 결국 DelegatingFilterProxy의 delegate 필드 FilterChainProxy부터 시작함을 알 수 있었다.
=> 이제, 시큐리티 필터에 대해 알아보도록 하자.
1. FilterChainProxy
1-1. Firewall
1-2. VirtualFilterChain 생성 후 필터 doFilter()
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.currentPosition == this.size) {
this.originalChain.doFilter(request, response);
} else {
++this.currentPosition;
Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
if (FilterChainProxy.logger.isTraceEnabled()) {
String name = nextFilter.getClass().getSimpleName();
FilterChainProxy.logger.trace(LogMessage.format("Invoking %s (%d/%d)", name, this.currentPosition, this.size));
}
nextFilter.doFilter(request, response, this);
}
}
=> 이것이 시큐리티 필터 체인의 정수이다.
=> FilterChainProxy의 VirtualFilterChain 정적 클래스 doFilter 코드이다.
=> 순서를 이제 doFilter되는 필터들의 순서를 살펴보자.
2. SecurityContextHolderFilter
=> 일단 중요하게 알아둬야하는 필터이다.
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
} else {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//핵심 로직===================================================
Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
this.securityContextHolderStrategy.setDeferredContext(deferredContext);
chain.doFilter(request, response);
//===========================================================
} finally {
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
}
=> SecurityContext를 제공하는 defferedContext가 보인다.
=> ContextRepository와 SecurityContext를 알아보자.
2-1. SecurityContext
=> SecurityContext는 하나의 클라이언트에 할당되는 컨텍스트이다.
=> SecurityContext는 2개의 메서드가 있는데, getAuthentication()과 setAuthentication()이다.
=> Authentication은 보통 토큰이라하는데, 인증 시 인증 정보를 저장한다.
=> 시큐리티는 필터단에 있어, 모든 요청 시 모든 필터가 doFilter()를 거치게 되는데,
=> 즉, 로그인을 하게 되면, Authenication 토큰이 생성되고, 이를 통해 다음 인증 시 자동으로 넘어갈 수 있고, 인가 시 이 Authentication의 인증 정보로 권한을 확인하고 권한에 따른 원하는 서비스를 이용할 수 있다.
=> 이러한 Authentication 토큰을 저장하는 부분이 SecurityContext이다.
2-2. SecurityContextRepository
=> SecurityContextHolderFilter의 핵심 로직에서 SecurityContext를 받아오는 것을 볼 수 있다.
=> 이때, SecurityContextRepository.loadDefferedContext()가 있음을 볼 수 있는데,
=> 이 SecurityContextRepository를 살펴보면 2가지가 있다.
=> 우리는 보통 3가지의 인증 방식을 쓸 수 있다.
=> 1. JWT를 이용한 Stateless방식
=> 2. Session에 SecurityContext를 저장하는 방식
=> 3. RequestAttribute에 SecurityContext를 저장하는 방식
=> 이때, SecurityContextRepository를 살펴보면 Session방식과 RequestAttribute 방식의 Repository가 있는 것이다.
=> 즉, 매 요청 시 Session 방식일 경우 : Session에서 SecurityContext를 받아와 이 안에 있는 Authentication 토큰을 이용하는 것이다.
=> 즉, 매 요청 시 RequestAttribute 방식일 경우 : Request에서 SecurityContext를 받아와 이 안에 있는 Authentication 토큰을 이용하는 것이다.
=> JWT 방식에서는 두개의 Repository가 필요 없으니, 둘 다 비활성화 해준다.
**만약, SecurityContext가 없다면, 새로 만든다.**
**만약, SecurityContext에 AuthenticationToken이 없다면, Login으로 Redirect한다.**
**JWT 방식을 쓰는 경우 둘 다 Disable()해준다.**
@EnableWebSecurity
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
JwtAuthenticationFilter jwtFilter) throws Exception {
http
// 1) 세션을 전혀 생성·사용하지 않음
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 2) SecurityContext 저장소를 완전 비활성화
.securityContext(context ->
context.securityContextRepository(NoOpSecurityContextRepository.getInstance())
)
// 3) JWT 검증 필터 등록 (UsernamePasswordAuthenticationFilter 앞)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth ->
auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable());
return http.build();
}
}
JWT 방식의 경우이다.
=> 이렇게 SecurityContextHolderStrategy에 SecurityContext를 저장해놓고,
ThreadLocalSecurityContextHolderStrategy에 set해준다.
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
Assert.notNull(deferredContext, "Only non-null Supplier instances are permitted");
Supplier<SecurityContext> notNullDeferredContext = () -> {
SecurityContext result = (SecurityContext)deferredContext.get();
Assert.notNull(result, "A Supplier<SecurityContext> returned null and is not allowed.");
return result;
};
contextHolder.set(notNullDeferredContext);
}
=> ThreadLocalSecurityContextHolderStrategy에 저장했다.
=> SecurityContext가 저장된 contextHolder는 static이기 때문에,
=> ThreadLocalSecurityContextHolderStrategy 객체를 생성후 getContext() 메서드를 사용해주기만 하면
=> SecurityContext를 가져올 수 있다.
3. LogoutFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (this.requiresLogout(request, response)) {
Authentication auth = this.securityContextHolderStrategy.getContext().getAuthentication();
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Logging out [%s]", auth));
}
this.handler.logout(request, response, auth);
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
} else {
chain.doFilter(request, response);
}
}
=> 로그아웃 필터의 doFilter() 이다.
=> 우선, requiresLogout을 해준다.
=> requiresLogout에서는 "/logout" 과 일치하는 URL의 요청이 들어온다면, true를 반환한다.
=> requiresLogout이 ture일 시
=> securityContextHolderStrategy에서 getContext()해서 Authentication 토큰을 가져온다.
=> 이제 hadler.logout을 살펴보자.
3-1. handler.logout
3-1-1. SecurityContextLogoutHandler
=> 만약, 세션 방식을 쓰고 있다면, 세션을 삭제한다.
=> SecurityContextHolder와 SecurityContextRepository를 비운다.
3-1-2. TokenBasedRememberMeService
=> RememberMe가 설정되어있다면,
=> 쿠키의 RememberMe 토큰을 제거한다.(세션 방식)
=> rememberMe는 다른 포스팅에서 알아본....까??
3-1-3. CsrfLogoutHandler
=> 이또한 Csrf 토큰을 지우는 것이리라
3-1-4. LogoutSuccessEventPublishingLogoutHandler
=> 로그아웃시 이벤트 설정해준다.(뭐 상속받아 커스텀 받아, 등록해주면 될 듯하다.)
**JWT에서 로그아웃은??**
=> 클라이언트에서 JWT 토큰을 삭제해주자.
=> 뭐 블랙리스트(레디스나 DB에 JWT고유 ID 등록해 로그인 못하게 하거나 등등 방식이 있을 수도??)
=> 그냥 클라이언트에서 토큰을 삭제시키는 것이 편할 듯 하다.
4. OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFIlter
=> OncePerReuqestFilter를 상속한다.
=> OAuth2를 이용할 때, 활성화된다.
=> 책임 연쇄 패턴으로 인해, OncePerRequestFilter의 doFilter()로 먼저 들어온 후 doFilterInternal()을 통해 호출된다.
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
//핵심 부분입니다용~
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return; //return 시 OncePerRequestFIlter의 doFIlter()로 되돌아감
}
} catch (Exception var12) {
AuthenticationException wrappedException = new OAuth2AuthorizationRequestException(var12);
this.authenticationFailureHandler.onAuthenticationFailure(request, response, wrappedException);
return;
}
try {
filterChain.doFilter(request, response);
} catch (IOException var10) {
throw var10;
} catch (Exception var11) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var11);
ClientAuthorizationRequiredException authzEx = (ClientAuthorizationRequiredException)this.throwableAnalyzer.getFirstThrowableOfType(ClientAuthorizationRequiredException.class, causeChain);
if (authzEx != null) {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request, authzEx.getClientRegistrationId());
if (authorizationRequest == null) {
throw authzEx;
}
this.requestCache.saveRequest(request, response);
this.sendRedirectForAuthorization(request, response, authorizationRequest);
} catch (Exception var9) {
AuthenticationException wrappedException = new OAuth2AuthorizationRequestException(var11);
this.authenticationFailureHandler.onAuthenticationFailure(request, response, wrappedException);
}
} else if (var11 instanceof ServletException) {
throw (ServletException)var11;
} else if (var11 instanceof RuntimeException) {
throw (RuntimeException)var11;
} else {
throw new RuntimeException(var11);
}
}
}
=> 핵심 부분을 보자.
=> resolve()는 DefaultOAuth2AuthorizationRequestResolver의 메서드이다.
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
if (registrationId == null) {
return null;
} else {
String redirectUriAction = this.getAction(request, "login");
return this.resolve(request, registrationId, redirectUriAction);
}
}
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) {
if (registrationId == null) {
return null;
} else {
String redirectUriAction = this.getAction(request, "authorize");
return this.resolve(request, registrationId, redirectUriAction);
}
}
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
if (registrationId == null) {
return null;
} else {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new InvalidClientRegistrationIdException("Invalid Client Registration with Id: " + registrationId);
} else {
OAuth2AuthorizationRequest.Builder builder = this.getBuilder(clientRegistration);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
builder.clientId(clientRegistration.getClientId()).authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()).redirectUri(redirectUriStr).scopes(clientRegistration.getScopes()).state(DEFAULT_STATE_GENERATOR.generateKey());
this.authorizationRequestCustomizer.accept(builder);
return builder.build();
}
}
}
**첫번째 resolve() 메서드**
=> resolve()들이다.
=> 일단, OAuth2를 활성화하면, 뭐,,, 구글 로그인하기, 카카오톡 로그인하기 이런 버튼이 나올 것이다.
=> 이때, 버튼을 클릭하게 되면, request가 백엔드 스프링 시큐리티 로 날라오게 되고, 그 request의 형식을 아래와 같다.
=> request에서 registrationId를 뽑아 온 것이 첫 번째 resolve()의 registrationId이다.
=> 이후 두번째 resolve() 메서드로 출발
**두번째 resolve() 메서드**
위와 같다 파라미터만 좀 다르다 결국 3번쨰 resolve()메서드에서 중요한 일이 일어난다~
**세번째 resolve() 메서드**
=> 뭐, Google 로그인이면, resolve(request, Google, login) 이런식으로 호출될 것이다.
=> 아래의 코드는 설정해둔 YML에서 내용들을 뽑아오는 메서드이다.
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
spring:
security:
oauth2:
client:
registration:
google:
client-id: your-google-id
client-secret: your-google-secret
scope: openid,profile,email
github:
client-id: your-github-id
client-secret: your-github-secret
scope: read:user
=> 발가락이 닮았다....는 헛소리고, yml의 내용과 같다.
=> 이 정보들을 조합해 구글 로그인창을 띄워줄 정보들을 만들고 return한다.
=> 다시 doFilterInternal로 돌아가자.
tr{
//핵심 부분입니다용~
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return; //return 시 OncePerRequestFIlter의 doFIlter()로 되돌아감
}
=> 너무 길어졌기 때문에, 친절하게 다시 가져왔다.
https://accounts.google.com/o/oauth2/v2/auth
?response_type=code
&client_id=googleId
&scope=openid%20profile%20email
&state=RANDOM_STATE_STRING
&redirect_uri=http://localhost:8080/login/oauth2/code/google
=> resolve()에서 대충 이런식으로 빌드된 형식이 return 되고 이것을 이용해 client에게 sendRedirect해준다.
=> 이때, send 한 내용을 세션에 저장해둔다. (원래의 요청 정보가 있어야, 로그인 성공 시 비교해볼 수 있기 때문인것같음)
=> 그럼, 로그인 페이지가 리다이렉트 되어 뜨는 것이다.
5. OAuth2LoginAuthenticationFilter extends AbstractAuthenticationFilter
=> 위에서 인증이 성공하면, 요청을 다시 보낸다.
=> AbstractAuthenticationProcessingFilter의 doFilter()에서 attemptAuthentciation()을 호출한다.
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_request");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error("authorization_request_not_found");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
String registrationId = (String)authorizationRequest.getAttribute("registration_id");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
OAuth2Error oauth2Error = new OAuth2Error("client_registration_not_found", "Client Registration not found with Id: " + registrationId, (String)null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
//핵심 로직인디용~띠용~
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)).replaceQuery((String)null).build().toUriString();
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
//===============================================
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
//핵심 로직 두번째 인디용~띠용~
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(authenticationDetails);
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken)this.getAuthenticationManager().authenticate(authenticationRequest);
OAuth2AuthenticationToken oauth2Authentication = (OAuth2AuthenticationToken)this.authenticationResultConverter.convert(authenticationResult);
Assert.notNull(oauth2Authentication, "authentication result cannot be null");
oauth2Authentication.setDetails(authenticationDetails);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(authenticationResult.getClientRegistration(), oauth2Authentication.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
//===============================================
return oauth2Authentication;
}
}
}
}
젠장 이게 뭔가??
=> request가 이런식으로 온다 : GET /login/oauth2/code/{registrationId}?code=…&state=…
=> 이 요청에는 당연히 인증 성공의 response가 들어있겠다.
=> 속지마라. request라 해도 OAuth2 성공 Reponse다.
=> 뭐 OAuth2 로그인 성공 Response(request의 가면을 쓴)를 이러쿵 저러쿵 해석(파싱)하는 내용이 진행되고,
5-1. 사용자 정보 API 접근, Details 받아오기
=> Convert()에서 이 인증 성공 Reponse들을 갖고 구글 사용자 정보 API에서 사용자 Details를 받아온다.
5-2. AuthentcationToken을 생성해 Context에 저장
=>
6. UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username.trim() : "";
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
//핵심 로직이여유
return this.getAuthenticationManager().authenticate(authRequest);
//================
}
}
=> AbstractAuthenticationProcessingFilter의 doFilter()를 통해 attempAuthentication() 메서드가 호출된다.
=> 일반적으로, OAuth2.0이나, jwt방식을 이용하지 않으면 Default로 사용되는 필터이다.
=> request에서 username과 password를 받아온다.
=> request에서 getParameter로 "username","password"를 받아오기 때문에, 이 필터에서 제대로 값을 가져오기 위해서는
=> 폼 로그인 창의 id와 pw입력란의 파라미터 명을 "username","password"로 정확하게 맞춰주어야 한다.
=> username과 password를 저장한 UsernamePasswordAuthenticationToken을 만든다.
=> 마지막 return을 보면 AuthenticationManager를 얻어와 authenticate(UsernamePasswordAuthenticationToken)를 호출하는 것을 볼 수 있다.
6-1. ProviderManager implements AuthenticationManager와 AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider
=> ProviderManager는 어떤 Provider를 사용할 것인지를 받아온다.
=> 받아온 Provider의 authenticate()를 호출한다.
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = this.determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//핵심로직1==========================
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
//==================================
} catch (UsernameNotFoundException var6) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw var6;
}
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
=> 위를 보면, retrieveUser()를 호출하는 것을 볼 수 있다.
=> AbstractUserDetailsAuthenticationProvider를 상속하고 있는 DaoAuthenticationProvider의 retrieveUser()를 호출하는 것.
6-2. DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider => UserDetailsService, UserDetails
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//핵심 로직~~
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
//=================================
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
=> retreiveUser()는 내부에서 loadUserByUsername()를 호출한다.
=> loadUserByUsername()이 있는 구현체가 InMemoryUserDetailsManager이고 이 구현체는 UserDetailsManager, UserDetailsService를 구현하고 있는데, 우리는 UserDetailsService를 커스텀함으로 DB와 연동할 수 있다.
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = (UserDetails)this.users.get(username.toLowerCase(Locale.ROOT));
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
return (UserDetails)(user instanceof CredentialsContainer ? user : new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities()));
}
}
=> 이 메서드 안에서 UserDetails를 생성해서 return해주면 된다.
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
default boolean isAccountNonExpired() {
return true;
}
default boolean isAccountNonLocked() {
return true;
}
default boolean isCredentialsNonExpired() {
return true;
}
default boolean isEnabled() {
return true;
}
}
=> 모든 UserDetails관련 로직이 끝나고 ProviderManager는 UserDetails를 받아 AuthenticationToken을 받는다.
=> 그 이후 AbstractAuthenticationProcessingFilter의 doFilter()로 다시 돌아와 ContextHolder에 저장하고 다음 필터로 넘어간다.
7. RememberMeAuthenticationFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (this.securityContextHolderStrategy.getContext().getAuthentication() != null) {
this.logger.debug(LogMessage.of(() -> {
return "SecurityContextHolder not populated with remember-me token, as it already contained: '" + String.valueOf(this.securityContextHolderStrategy.getContext().getAuthentication()) + "'";
}));
chain.doFilter(request, response);
} else {
//autoLogin을 호출 핵심=======
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
//============================
if (rememberMeAuth != null) {
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
this.sessionStrategy.onAuthentication(rememberMeAuth, request, response);
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(rememberMeAuth);
this.securityContextHolderStrategy.setContext(context);
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
this.logger.debug(LogMessage.of(() -> {
return "SecurityContextHolder populated with remember-me token: '" + String.valueOf(this.securityContextHolderStrategy.getContext().getAuthentication()) + "'";
}));
this.securityContextRepository.saveContext(context, request, response);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(this.securityContextHolderStrategy.getContext().getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
} catch (AuthenticationException var6) {
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), var6);
this.rememberMeServices.loginFail(request, response);
this.onUnsuccessfulAuthentication(request, response, var6);
}
}
chain.doFilter(request, response);
}
}
=> RememberMeFilter이다.
=> 날 기억해 달라 함은 자동 로그인이 되시겠다.
=> autoLogin()을 호출하면, AbstractRememberMeService로 들어간다.
7-1. AbstractRememberMeService => TokenBasedRememberMeService
=> 로직이 길지만, 핵심로직은 그다지 길지 않아 설명으로 대신한다.
=> AbstractRememberMeService에서는 토큰을 쿠키에서 받아온다.
=> 그후, 디코딩하고, TokenBasedRememberMeServices의 processAutoLoginCookie()를 호출한다.
=> String[] cookieToken의 형태로 받아오는데,
=> 0번째 인덱스 : username을 저장
=> 1번쨰 인덱스 : 만료 시간을 저장
=> 2번째 인덱스 : 토큰 시그니처(알고리즘) 저장
=> 3번째 인덱스 : 토큰 시그니처(알고리즘) 저장
=> 이런 식으로 쿠키안에 토큰으로 담아주나보다.
// 1) 만료 시각 계산 (예: 현재 시각 + validitySeconds)
long expiryTime = System.currentTimeMillis() + (validitySeconds * 1000);
// 2) 서명 생성
// signature = MD5(username + ":" + expiryTime + ":" + password + ":" + key)
String data = username + ":" + expiryTime + ":" + userPassword + ":" + rememberMeKey;
String signature = DigestUtils.md5Hex(data);
// 3) 원본 토큰 문자열
String tokenValue = username + ":" + expiryTime + ":" + signature;
// 4) Base64 인코딩
String cookieValue = Base64.getEncoder()
.encodeToString(tokenValue.getBytes(StandardCharsets.UTF_8));
// 5) 쿠키에 설정
Cookie rememberMeCookie = new Cookie("remember-me", cookieValue);
rememberMeCookie.setMaxAge((int)validitySeconds);
response.addCookie(rememberMeCookie);
http
.rememberMe(rem -> rem
// 1) 시그니처 key (반드시 설정)
.key("changeThisSecretKey")
// 2) UserDetailsService: 쿠키 검증 후 사용자 조회
.userDetailsService(userDetailsService)
// 3) 토큰 유효기간 (예: 14일)
.tokenValiditySeconds(14 * 24 * 60 * 60)
// 4) (선택) 쿠키 이름·파라미터 이름 변경
.rememberMeCookieName("remember-me-cookie")
.rememberMeParameter("remember-me")
// 5) (선택) 보안을 위해 HTTPS일 때만 쿠키 전송
.useSecureCookie(true)
// 6) (선택) 영속 토큰 방식 사용하려면 tokenRepository 지정
.tokenRepository(persistentTokenRepository())
);
/** PersistentTokenBasedRememberMeServices 를 위한 토큰 저장소 빈 */
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
repo.setDataSource(dataSource);
// 첫 실행 시 테이블 생성이 필요하면 주석 해제
// repo.setCreateTableOnStartup(true);
return repo;
}
**이런식으로 쿠키에 넣어주면 알아서 자동 로그인이 되나보다.**
**영속 방식일 경우(뭐, 토큰을 데이터베이스에 저장하고, 쓸 수 있다.)**
**영속 방식에 대해 한 번 찾아보자 궁금하면**
8. AnonymousAuthenticationFilter
=> 끝이 다와간다 굳이 하지않겠다. 귀찮다.
9. OAuth2AuthorizationCodeGrantFilter
10. ExceptionTranlationFilter
11. AuthorizationFilter
'스프링 시큐리티' 카테고리의 다른 글
SpringApplication.run 이후 초기화 (0) | 2025.04.26 |
---|---|
Authentication 흐름 2(AnonymousAuthenticationFilter) (1) | 2024.08.06 |
FilterChain과 책임 연쇄 패턴 (1) | 2024.08.05 |
Authentication 흐름 1(SecurityContextHolderFilter) (0) | 2024.08.03 |
DelegatingFilterProxy와 흐름 (0) | 2024.07.20 |