[프로젝트 리뷰] Spring Security 로그인 후 이전 페이지로 이동 - Referer, addFlashAttribute
개발환경
- STS 4.16.1
- Springboot 3.0.1 (springsecurity6)
- Thymeleaf
- Java 17
- JPA
- Gradle 7.6
관련 포스팅 - 소셜 로그인 구현
[프로젝트 리뷰] Spring Security OAuth2 소셜 로그인(구글, 네이버, 카카오) 구현
개발환경 STS 4.16.1 Springboot 3.0.1 (springsecurity6) Thymeleaf Java 17 JPA Gradle 7.6 사전설정 네이버 / 구글 / 카카오 OAuth2 API 설정은 따로 기술하지 않습니다. Gradle 의존성 추가 build.gradle dependencies{ implementation
rkgh17.tistory.com
전체 Repository
GitHub - rkgh17/SeoulWalk: 휴먼 스프링부트 프로젝트
휴먼 스프링부트 프로젝트. Contribute to rkgh17/SeoulWalk development by creating an account on GitHub.
github.com
해결 순서
- 로그인 시, 이전 페이지에 대한 url을 Session에 저장해준다.
- 로그인 성공 시, Session에 저장한 url을 사용하여 이전 페이지로 돌아간다.
- 로컬에서는 위 방법을 사용하여 문제가 없었으나 배포해보니 권한이 필요한 페이지에서 로그인 시 오류가 났다.
- 배포 서버에서는 url이 계속 null값으로 나옴 -> addFlashAttribute를 사용해 url을 전달해주었다.
권한이 필요한 페이지에서의 로그인
위 화면에서 질문등록 버튼을 누를 시 아래와 같은 흐름으로 진행됨.
QuestionController.java
현재 사용자의 세션이 없으면 로그인 하지 않은 유저이므로, 이전 url값을 컨트롤러에 전달해 주어야 한다.
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@RequestMapping("board")
@Controller
public class QuestionController {
/*생략*/
// 질문 등록 메서드
@GetMapping("qna/create")
public String questionCreate(QuestionForm questionForm, HttpServletRequest request, RedirectAttributes re) {
if(userService.getSession() == null) {
// 회원이 아닐 시, 이전 uri를 flashattribute로 보내준다
String uri = request.getHeader("Referer");
re.addFlashAttribute("referer",uri);
return "redirect:/user/login";
}else {
return "bbs/bbsQnaForm";
}
}
/*생략*/
}
addFlashAttribute
스프링에서 객체를 전달할 때 사용하는 방법 중 하나.
전달할 때 String으로 단순 문자열로 전달 할 수도 있는데, 여기서는 Object로 전달해 주었다.
처음에 model.addAttribute를 사용하였는데, 로그인 화면으로 이동시에 url에 이전 페이지 url이 표시되었기 때문에 보기에 좋지 않았다....
addFlashAttribute는 표시되지 않았기에 사용하였다.
UserController.java
전달받은 url을 세션에 넣어준다.
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Controller
@RequestMapping("user")
public class UserController {
@GetMapping("login")
public String login(HttpServletRequest request) {
/* 로그인 성공 시 이전 페이지로 이동 */
String uri = request.getHeader("Referer");
// 이전 uri가 null이다 -> 배포 서버에서 나타나는 오류?
if (uri==null) {
// null일시 이전 페이지에서 addFlashAttribute로 보내준 uri을 저장
Map<String, ?> paramMap = RequestContextUtils.getInputFlashMap(request);
uri = (String) paramMap.get("referer");
// 이전 url 정보 담기
request.getSession().setAttribute("prevPage", uri);
}else {
// 이전 url 정보 담기
request.getSession().setAttribute("prevPage", uri);
}
return "login";
}
@GetMapping("logout")
public String logoutpage() {
return "logout";
}
@GetMapping("logout/do")
public String logout(HttpServletRequest request, HttpServletResponse response) throws Exception{
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return "redirect:/main";
}
}
권한이 필요한 페이지에서 온 경우, 이전 컨트롤러에서 전달받은 url을 Session의 prevPage attribute로 저장해준다.
권한이 필요하지 않은 페이지에서의 로그인도 마찬가지로 이전 url을 전달해준다.
다시한번 강조하지만, 권한이 필요한 페이지에서의 로그인 요청 시 request.getHeader("Referer")값이 null로 나왔기 때문에 이러한 방식을 사용한 거지, 더 좋은 방법이 분명 있을것이다!!
더 근본적인 해결책을 찾아 포스팅하는 그날까지..
CustomAuthSuccessHandler.java
로그인 성공 시 동작하는 SimpleUrlAuthenticationSuccessHandler를 상속받아 구현
SimpleUrlAuthenticationSuccessHandler 공식문서
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@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 {
// 이전 url로 redirect
String prevPage = (String) request.getSession().getAttribute("prevPage");
redirectStrategy.sendRedirect(request, response, prevPage);
}
}
로그인 성공 시 onAuthenticationSuccess 메서드 동작
Session에 넣어주었던 prevPage attribute를 받아서 redirectStrategy를 통해 로그인 성공 후 결정된 url로 redirect한다.
소셜 로그인으로 인가를 처리하고 있기에, 로그인 실패에 대한 상황은 고려하지 않았다.
SecurityConfig.java
스프링 시큐리티 설정
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.human.seoulroad.user.CustomOAuth2UserService;
import com.human.seoulroad.user.CustomAuthSuccessHandler;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
public class SecurityConfig{
@Autowired
private CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeHttpRequests().requestMatchers(
new AntPathRequestMatcher("/**")).permitAll()
// h2콘솔관련
.and()
.csrf().ignoringRequestMatchers(
new AntPathRequestMatcher("/h2-console/**"))
.and()
.headers()
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
// 로그인/로그아웃 관련
.and()
.logout()
.logoutUrl("/user/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.and()
.oauth2Login()
//.defaultSuccessUrl("/")
.successHandler(new CustomAuthSuccessHandler()) // 반드시 추가!!
.userInfoEndpoint()
.userService(customOAuth2UserService)
;
return http.build();
}
}
생각해볼 점
세션 탈취해서 로그인하기(세션 하이재킹) feat. 쿠키와 세션 실습
HttpSession session = request.getSession(); 자바에서 클라이언트의 세션을 얻기 위해 위의 코드를 많이 사용한다. 세션이 존재하지 않으면 초기에는 WAS 컨테이너에서 Session Id를 발급해준다. 그리고 Session
stir.tistory.com