로그인 성공 후 로직 처리, AuthenticationSuccessHandler
AuthenticationSuccessHandler (spring-security-docs 6.2.1 API)
Strategy used to handle a successful user authentication. Implementations can do whatever they want but typical behaviour would be to control the navigation to the subsequent destination (using a redirect or a forward). For example, after a user has logged
docs.spring.io
위 그림처럼 스프링 시큐리티가 authentication (인증) 성공하면 AuthenticationSuccessHandler 가 호출된다.
실패하면 AuthenticationFailureHandler.
따라서 로그인 성공 후 하고 싶은 일을 우리가 구현할수 있다.
AuthenticationSuccessHandler.java
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
/**
* Strategy used to handle a successful user authentication.
* <p>
* Implementations can do whatever they want but typical behaviour would be to control the
* navigation to the subsequent destination (using a redirect or a forward). For example,
* after a user has logged in by submitting a login form, the application needs to decide
* where they should be redirected to afterwards (see
* {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be
* included if required.
*
* @author Luke Taylor
* @since 3.0
*/
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
* @param request the request which caused the successful authentication
* @param response the response
* @param chain the {@link FilterChain} which can be used to proceed other filters in
* the chain
* @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process.
* @since 5.2.0
*/
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
/**
* Called when a user has been successfully authenticated.
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException;
}
onAuthenticationSuccess() 메서드는 로그인 성공 이후 호출되므로 우리는 AuthenticationSuccessHandler를 구현하는 커스텀 클래스를 만들고 이걸 오버라이딩해서 원하는 로직을 작성해주면 된다.
로그인 이전 페이지로 돌아가기
유저가 로그인을 하면 로그인하기 전 페이지로 돌아가도록 하고 싶다.
생각해보면 크게 두 가지 경우가 있을수 있다.
- 유저가 직접 로그인 버튼을 클릭해서 로그인폼으로 이동 후 로그인 성공
- 유저가 권한이 없는 경로에 접근해서 스프링 시큐리티가 인터셉트 한 후에 로그인 페이지 요청으로 바꿔 서블릿에 전달
1) 직접 로그인 버튼 클릭해 로그인폼으로 이동한 경우
이 경우에는 기존에 유저가 있었던 페이지를 기억하고 있다가 onAuthenticationSuccess 메서드에서 그곳으로 리다이렉트 시키면 될것이다.
그렇게 하는 방법은 유저가 로그인 버튼을 눌러서 로그인폼에 접근할때 현재 페이지 정보를 세션에 저장하면 된다.
현재 페이지 정보는 request header의 referer 를 보면 된다.
referer 에는 현재 페이지로 이동하기 전에 있던 페이지의 url 이 담겨있다.
아래는 컨트롤러에서 로그인폼에 접근하는 경로 매핑이다.
@GetMapping("/login")
public String loginForm(Model model, HttpServletRequest request) {
model.addAttribute("menu", "login");
String prevPage = request.getHeader("Referer");
log.info("loginForm prevPage = {}", prevPage);
if(prevPage != null && !prevPage.contains("/login")) {
request.getSession().setAttribute("prevPage", prevPage);
}
return "login";
}
request.getHeader("Referer") 로 리퍼러를 꺼내서 로그를 찍어본다.
이런식으로 이전 페이지의 url 이 담겨있다.
이 uri을 세션의 어트리뷰트에 저장하면 된다.
(request의 어트리뷰트에 저장하면 클라이언트에 응답이 된후에는 해당 어트리뷰트는 사라지기 때문에 세션에 저장해야 한다)
2) 권한이 없는 경로 접근해서 스프링 시큐리티가 인터셉트한 경우
권한이 없는 경로에 접근하면 스프링 시큐리티가 요청을 낚아채서 로그인 페이지 요청으로 바꿔 서블릿에 전달한다.
이때 기존 클라이언트가 요청했던 정보를 세션에 저장한다.
따라서 이 정보를 가져와서 리다이렉트 시키면 된다.
스프링 시큐리티는 위와 같이 권한이 없는 경로에 접근시 HttpServletRequest 를 RequestCache 에 저장한다.
requestCache.getRequest() 로 기존의 요청인 HttpServletRequest를 가져올수 있다.
RequestCache.java
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.savedrequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* Implements "saved request" logic, allowing a single request to be retrieved and
* restarted after redirecting to an authentication mechanism.
*
* @author Luke Taylor
* @since 3.0
*/
public interface RequestCache {
/**
* Caches the current request for later retrieval, once authentication has taken
* place. Used by <tt>ExceptionTranslationFilter</tt>.
* @param request the request to be stored
*/
void saveRequest(HttpServletRequest request, HttpServletResponse response);
/**
* Returns the saved request, leaving it cached.
* @param request the current request
* @return the saved request which was previously cached, or null if there is none.
*/
SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
/**
* Returns a wrapper around the saved request, if it matches the current request. The
* saved request should be removed from the cache.
* @param request
* @param response
* @return the wrapped save request, if it matches the original, or null if there is
* no cached request or it doesn't match.
*/
HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response);
/**
* Removes the cached request.
* @param request the current request, allowing access to the cache.
*/
void removeRequest(HttpServletRequest request, HttpServletResponse response);
}
따라서 SavedRequest savedRequest = requestCache.getRequest() 로 savedRequest를 가져왔을때 null 이라면 스프링 시큐리티가 요청을 인터셉트한적이 없다는 뜻이다.
=> 즉 이 경우는 유저가 로그인 버튼을 눌러서 로그인폼으로 이동한 경우다.
null 이 아니라면 스프링 시큐리티가 인터셉트했다는 뜻이므로
=> 유저가 권한이 없는 경로에 접근해 스프링 시큐리티가 요청을 낚아채서 로그인 폼으로 보낸 경우다
CustomLoginSuccessHandler implements AuthenticationSuccessHandler
@Slf4j
@Component
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("onAuthenticationSuccess");
SavedRequest savedRequest = requestCache.getRequest(request, response);
// 접근 권한 없는 경로 접근해서 스프링 시큐리티가 인터셉트해서 로그인폼으로 이동 후 로그인 성공한 경우
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
log.info("targetUrl = {}", targetUrl);
redirectStrategy.sendRedirect(request, response, targetUrl);
}
// 로그인 버튼 눌러서 로그인한 경우 기존에 있던 페이지로 리다이렉트
else {
String prevPage = (String) request.getSession().getAttribute("prevPage");
log.info("prevPage = {}", prevPage);
redirectStrategy.sendRedirect(request, response, prevPage);
}
}
}
onAuthenticationSuccess 메소드를 보면 savedRequest가 null 인 경우와 아닌 경우 두가지 경우로 나뉘고 있다.
- savedRequest != null
- 접근 권한 없는 경로 접근해서 스프링 시큐리티가 인터셉트 후 로그인폼으로 보낸 경우
- savedRequest 는 기존 요청 (HttpSerletRequest) 정보가 담겨있다
- savedRequest.getRedirectUrl() 은 저장된 request의 url 이 담겨있다.
- savedRequest == null
- 로그인 버튼 눌러서 로그인폼으로 이동한 경우
- LoginController 에서 loginForm 에 접근 시 세션에 prevPage 를 저장해줬기 때문에 그곳으로 리다이렉트 하면 된다
SecurityConfig
AuthenticationSuccessHandler 를 구현하는 CustomLoginSuccessHandler 가 작동하려면 당연히 스프링 시큐리티의 필터체인에 적용해줘야 한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final PrincipalOAuth2UserService principalOAuth2UserService;
private final CustomLoginSuccessHandler customLoginSuccessHandler;
@Autowired
public SecurityConfig(PrincipalOAuth2UserService principalOAuth2UserService, CustomLoginSuccessHandler customLoginSuccessHandler) {
this.principalOAuth2UserService = principalOAuth2UserService;
this.customLoginSuccessHandler = customLoginSuccessHandler;
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(request -> request
// ...
)
// OAuth2 로그인 처리
.oauth2Login(oauth2Login -> oauth2Login
.loginPage("/login") // 로그인 필요 경로 요청 시 보낼 경로(로그인 페이지)
.userInfoEndpoint(userInfo -> userInfo
// PrincipalOAuth2UserService extends DefaultOAuth2UserService 가 회원가입 처리함
.userService(principalOAuth2UserService))
.successHandler(customLoginSuccessHandler) // 커스텀 로그인 성공 핸들러 등록
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/"))
.csrf(csrf -> csrf.disable());
return http.build();
}
}