발생한 문제 : 게시판 웹사이트에서 댓글 작성 페이지 -> 로그인 페이지로 redirect 된 후, 로그인 성공 시 댓글 작성 페이지로 되돌아오는 것이 아닌 index page ("/")로 돌아오게 된다.
해결 :
1. 스프링 시큐리티는 권한이 없는 페이지에 대해서 login form 페이지로 redirect 된다.
2. 이 때 이전 페이지에 대한 url을 Referer 헤더로 request에 가지고 있다.
3. Referer 헤더값(이전 페이지에 대한 url)을 Session에 저장한다.
4. 로그인 성공 시 동작하는 AuthenticationSuccessHandler를 상속받아 구현한다.
5. 이 때 Referer 헤더값을 Session에서 꺼내서 해당 url로 redirect한다.
위 순서대로 문제를 해결하였다.
아래는 3~5번에 대한 소스코드이다.
3. Referer 헤더값(이전 페이지에 대한 url)을 Session에 저장한다.
// UserController.java
@GetMapping("/login")
public String loginForm(
HttpServletRequest request,
Model model) {
....
/**
* 이전 페이지로 되돌아가기 위한 Referer 헤더값을 세션의 prevPage attribute로 저장
*/
String uri = request.getHeader("Referer");
if (uri != null && !uri.contains("/login")) {
request.getSession().setAttribute("prevPage", uri);
}
return "auth/loginForm";
}
loginForm에 진입 할 때, request.getHeader("Referer")를 통해 이전 페이지에 대한 uri 받아온다.
이 때 해당 uri가 null이라면 이전 페이지가 존재하지 않는다는 것이고, uri가 "/login"을 포함한다면 로그인 실패 등의 이유로 이전 페이지가 loginForm에 대한 uri인 것이므로 session에 해당 uri를 저장하지 않는다. (로그인 성공 후 다시 loginForm으로 이동할 필요는 없다)
uri가 null이 아니고 "/login"을 포함하지 않는다면 session에 해당 uri의 값을 저장한다.
4. 로그인 성공 시 동작하는 AuthenticationSuccessHandler를 상속받아 구현한다.
5. 이 때 Referer 헤더값을 Session에서 꺼내서 해당 url로 redirect한다.
// CustomAuthSuccessHandler.java
@Component
public class CustomAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final RequestCache requestCache = new HttpSessionRequestCache();
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
clearSession(request);
SavedRequest savedRequest = requestCache.getRequest(request, response);
/**
* prevPage가 존재하는 경우 = 사용자가 직접 /auth/login 경로로 로그인 요청
* 기존 Session의 prevPage attribute 제거
*/
String prevPage = (String) request.getSession().getAttribute("prevPage");
if (prevPage != null) {
request.getSession().removeAttribute("prevPage");
}
// 기본 URI
String uri = "/";
/**
* savedRequest 존재하는 경우 = 인증 권한이 없는 페이지 접근
* Security Filter가 인터셉트하여 savedRequest에 세션 저장
*/
if (savedRequest != null) {
uri = savedRequest.getRedirectUrl();
} else if (prevPage != null && !prevPage.equals("")) {
// 회원가입 - 로그인으로 넘어온 경우 "/"로 redirect
if (prevPage.contains("/auth/join")) {
uri = "/";
} else {
uri = prevPage;
}
}
redirectStrategy.sendRedirect(request, response, uri);
}
// 로그인 실패 후 성공 시 남아있는 에러 세션 제거
protected void clearSession(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
}
}
세션에 저장한 이전 페이지 uri를 사용하여 로그인 성공 시 해당 uri로 redirect 할 수 있게 해보자.
SimpleUrlAuthenticationSuccessHandler를 상속받아 구현한다.
AuthenticationSuccessHandler는 이름 그대로 인증에 성공 후 동작하는 핸들러로,
로그인 성공 시 onAuthenticationSuccess 메서드가 동작한다.
먼저, clearSession 메서드를 정의하여 로그인 성공 시 이전에 에러 세션(로그인 실패 기록)이 존재한다면 제거해주는 작업을 수행한다.
그 후 requestCache.getRequest를 통해 SavedRequest를 꺼내올 수 있는데,
SavedRequest는 우리가 위에서 세션에 이전 페이지의 uri를 세션에 저장하는 작업을 Spring Security가 수행한 것이라고 생각할 수 있다.
SavedRequest가 존재한다는 것은 *권한이 없는 페이지에 접근하여 Spring Security가 해당 작업을 Intercept해 이전 페이지에 대한 uri를 SavedRequest에 저장하고, login page로 redirect했음을 의미한다. 이 경우에는 Spring Security가 친절하게 SavedRequest에 uri를 저장해두었으므로 해당 uri를 사용하면 된다.
*ex) 사용자가 로그인 되어있지 않은 상태로 "글 작성" 버튼을 누르는 등 권한이 없는 페이지로 이동했을 때
SavedRequest가 존재하지 않는다는 것은 *사용자가 직접 로그인 페이지에 접근하여 로그인을 수행한 경우로, Spring Security가 해당 작업을 Intercept 하지 않았음을 의미한다. 그러므로 SavedRequest의 값이 null이고, 이전 페이지에 대한 uri를 SavedRequest에 저장하지 않는다. 이 때 우리가 세션에 저장한 uri를 사용하면 된다.
*ex) 사용자가 "로그인" 버튼을 눌러 직접 로그인 페이지로 이동했을 때
savedRequest의 존재여부에 따라 redirect 할 uri를 결정하게 되는데, savedRequest가 null이 아니라면savedRequest.getRedirectUrl()을 통해 Spring Security가 저장해놓은 이전 페이지의 uri를 사용한다.
savedRequest가 null이라면 세션의 uri 값에 따라 다시 한번 조건을 분기한다.
1. uri의 값이 존재하지 않는다면 -> uri = "/"
2. uri의 값이 존재하지만, "auth/join"을 포함하고 있다면 -> uri = "/" (회원가입 페이지 -> 로그인 페이지 순서로 이동한 것이므로 로그인 성공 후 회원가입 페이지로 다시 이동하게 하지 않고 index page로 이동)
3. uri의 값이 존재하고, "auth/join"을 포함하지 않는다면 -> uri = 세션에 저장한 이전 페이지 uri
redirectStrategy를 통해 로그인 성공 후 결정된 uri로 redirect한다.
** 해당 AuthenticationSuccessHandler를 꼭 WebSecurityConfigurerAdapter의 configure에서 추가해주어야 한다!
// SecurityConfig.java
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AuthenticationSuccessHandler authenticationSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/posts/**").hasRole(Role.USER.name()) // 등록, 수정, 삭제
.antMatchers("/posts/add/**").hasRole(Role.USER.name()) // 등록 Form
.antMatchers("/posts/{\\d+}/edit").hasRole(Role.USER.name()) // 수정 Form
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/auth/login")
.loginProcessingUrl("/auth/loginProc")
.successHandler(authenticationSuccessHandler) // 꼭 추가해주세요!
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/auth/logout"))
.invalidateHttpSession(true).deleteCookies("JSESSIONID")
.logoutSuccessUrl("/");
}
}
해결 후 동작 :
글 상세 페이지에서 직접 로그인 버튼을 통해 로그인 페이지로 접근하여 Spring Security가 인터셉트 할 수 없는 작업이었지만, AuthenticationSuccessHandler를 통해 로그인 성공 후 다시 이전 페이지인 글 상세 페이지로 redirect 되었다.
소스코드 : https://github.com/HunSeongPark/lolcruit
참고 : https://codevang.tistory.com/269
'Spring' 카테고리의 다른 글
외부 메서드, 내부 메서드에 대한 @Transactional 트랜잭션 적용 결과 테스트 (0) | 2023.06.22 |
---|---|
Jsoup 라이브러리를 통한 정적 페이지 크롤링 (0) | 2023.03.18 |
Proxy로 생성되는 Service와 의존관계를 갖는 @Repository, JpaRepository는 Proxy일까? (2) | 2022.09.09 |
JWT를 사용한 로그인 및 Refresh Token을 활용한 로그인 상태 유지 (9) | 2022.07.12 |
username만 매칭되면 user 세션 생성 되는 문제 해결 - AuthenticationProvider를 통한 password 기반 인증 (0) | 2022.05.30 |