로그인 했는지 안했는지 판단하는 커스텀 애노테이션을 생성했다.
@interface를 사용하여 Custom Annotation 생성
**LoginCheck.java**
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCheck {
}
**CurrentUser.java**
@Target(ElementType.PARAMETER) //얘는 파라미터에서 꺼내올것임
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
기존 코드
**HomeController.java**
@GetMapping("/")
public String home(
//@SessionAttribute(name = SessionConstants.LOGIN_USER, required = false)
@CurrentUser
User loginUser, Model model) {
// 세션에 회원 데이터가 없으면 홈으로 이동
System.out.println("여기 들어옴!!");
if (loginUser == null) {
return "home";
}
// 세션이 유지되면 로그인 홈으로 이동
model.addAttribute("user", loginUser);
return "loginHome";
}
**LoginService.java**
@Service
@RequiredArgsConstructor
public class LoginService {
private final UserRepositoryImpl userRepositoryImpl;
private final UserRepository userRepository;
private final HttpSession session;
public User login(String loginId, String password) {
return userRepositoryImpl.findByLoginId(loginId).filter(u -> u.getPassword().equals(password))
.orElse(null);
//if null then login fail
}
public User getLoginUser() {
return (User)session.getAttribute(LOGIN_USER);
}
}
실제 로그인을 진행하는 AuthController
**AuthController.java**
@Controller
@RequiredArgsConstructor
// @RequestMapping("/api/auth")
@Slf4j
public class AuthController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute LoginForm loginForm) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@ModelAttribute @Validated LoginForm loginForm,
BindingResult bindingResult, @RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
System.out.println("로그인버튼 누름! 유저아이디 : " + loginForm.getLoginId() + " 비번 : " +loginForm.getPassword());
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
User loginUser = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
if (loginUser == null) {
bindingResult.reject("login fail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
System.out.println("로그인 성공!!");
//로그인 성공
HttpSession session = request.getSession();// 세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성하여 반환
session.setAttribute(SessionConstants.LOGIN_USER, loginUser);// 세션에 로그인 회원 정보 보관
// System.out.println("리다이렉트 유알엘 : " + redirectURL);
if (loginUser.getLoginId().equals("admin")) {
return "/admin/adminHome";
}
return "redirect:" + redirectURL;
}
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate(); //세션 정보 삭제
}
return "redirect:/";
}
}
로그인을 했는지 안했는지 판단하는 Interceptor
**LoginCheckInterceptor.java**
@Component
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {
private final LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
Exception {
//실행될 컨트롤러의 메소드(핸들러)
HandlerMethod handlerMethod = (HandlerMethod) handler;
//해당 핸들러에 이전에 만든 LoginCheck 어노테이션이 존재하는지 확인
LoginCheck loginCheck = handlerMethod.getMethodAnnotation(LoginCheck.class);
//1. loginCheck 가 null이라면, 로그인 없이 접근 간으하기 때문에 true를 리턴하여 다음 작없 실행
if (loginCheck == null) {
return true;
}
//2. loginCheck가 null이 아니라면, session에서 로그인 정보를 꺼내서 null 여부를 판단.
//null 이라면 로그인 이후 이용이 가능하다는 Exception 날린다.
if (loginService.getLoginUser() == null) {
throw ExceptionFactory.invalidAccess();
}
return true;
}
}
그리고 현재 로그인한 유저의 정보를 꺼내오기위한 Custom Annotation @CurrentUser를 쓰려고 ArgumentResolver
**LoginUserArgumentResolver.java**
/*
1.supportsParameter() : 현재 parameter를 resolver가 지원할지 true/false로 반환한다. 즉, 해당 메서드가 참이라면 resolveArgument()를 반환한다.
2.해당 코드에서는 hasParameterAnnotation 메소드를 사용하여 해당 메소드에 CurrentUser라는 어노테이션이 존재하는지 확인한다.
3.resolveArgument() : 실제 바인딩할 객체를 반환한다.
4.해당 코드에서는 현재 로그인된 사용자 객체를 반환한다.
*/
@Component
@RequiredArgsConstructor
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final LoginService loginService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return loginService.getLoginUser();
}
}
그리고 이를 WebConfigurer에 정의해준다.
**WebConfig.java**
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LoginCheckInterceptor loginCheckInterceptor;
private final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver);
}
}
이렇게 잘 했다고 생각했지만, 실행을 하니 오류가 떴다.
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: class org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to class org.springframework.web.method.HandlerMethod (org.springframework.web.servlet.resource.ResourceHttpRequestHandler and org.springframework.web.method.HandlerMethod are in unnamed module of loader 'app')] with root cause
java.lang.ClassCastException: class org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to class org.springframework.web.method.HandlerMethod (org.springframework.web.servlet.resource.ResourceHttpRequestHandler and org.springframework.web.method.HandlerMethod are in unnamed module of loader 'app')
at com.estgames.common.config.LoginCheckInterceptor.preHandle(LoginCheckInterceptor.java:27) ~[classes/:na]
at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:148) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.27.jar:5.3.27]
무슨 오류일까
저 에러 메시지를 따라가보면,
@Component
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {
private final LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
Exception {
//실행될 컨트롤러의 메소드(핸들러)
HandlerMethod handlerMethod = (HandlerMethod) handler;
//해당 핸들러에 이전에 만든 LoginCheck 어노테이션이 존재하는지 확인
LoginCheck loginCheck = handlerMethod.getMethodAnnotation(LoginCheck.class);
//1. loginCheck 가 null이라면, 로그인 없이 접근 간으하기 때문에 true를 리턴하여 다음 작없 실행
if (loginCheck == null) {
return true;
}
//2. loginCheck가 null이 아니라면, session에서 로그인 정보를 꺼내서 null 여부를 판단.
//null 이라면 로그인 이후 이용이 가능하다는 Exception 날린다.
if (loginService.getLoginUser() == null) {
throw ExceptionFactory.invalidAccess();
}
return true;
}
}
내가 정의한 Interceptor에서 에러가 뜬다..
여기서도
//실행될 컨트롤러의 메소드(핸들러)
HandlerMethod handlerMethod = (HandlerMethod) handler;
바로 여기서 에러가 뜬다!!
그냥 실행을 하면 바로 떠버린다. 왜그럴까!?!?!?!
오류메시지를 해석해보면,
ResourceHttpRequestHandler를 HandlerMethod 로 타입변환을 할 수 없다는 에러다.
스프링 2.X 프레임워크에서 정적 리소스를 요청할 때에도 인터셉터가 호출되기 때문에,
인터셉터에 전달되는 핸들러가 모두 HandlerMethod가 아닐 수 있다는 것이다!
HandlerMethod 로 들어올 것이라고 예상했으나, 실제로 받았던 이 핸들러가 바로 '정적 리소스를 처리하는 핸들러' 이다!200 응답 코드에, Last-modified 헤더를 함께 보낸다.If-Modified-Since 헤더를 포함해서 보낸다.If-Modified-Since 헤더값과 리소스의 변경 시점이 같다면304 상태코드를 응답하면서 다시 리소스를 응답하지 않는다.If-Modified-Since 헤더값과 리소스의 변경 시점이 다르다면200 상태코드를 응답하게 되면서 브라우저는 다시 응답의 Last-Modified 헤더값을 다음 요청의 If-Modified-Since 헤더값에 포함하여 전송한다.**WebConfig.java**
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/"); -> 이 부분을 추가해줬다.
}
이렇게 addPathPatterns 를 이용해서, API의 루트 URL을 명시해주면, 정적 요청없이 잘 동작한다.
어렵다.
인터셉터를 적용하거나 하지 않을 부분은 addPathPatterns 와 excludePathPatterns 에 작성하면
된다. 기본적으로 모든 경로에 해당 인터셉터를 적용하되 ( / ), 홈( / ), 회원가입( /members/add ),
로그인( /login ), 리소스 조회( /css/ ), 오류( /error )와 같은 부분은 로그인 체크 인터셉터를
적용하지 않는다. 서블릿 필터와 비교해보면 매우 편리한 것을 알 수 있다.
정리
서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술이다.
서블릿 필터와 비교해서 스프링 인터셉터가 개발자 입장에서 훨씬 편리하다는 것을 코드로 이해했을
것이다. 특별한 문제가 없다면 인터셉터를 사용하는 것이 좋다
따라서 나는
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/logout", "/css/**", "/*.ico", "/error");
}
이렇게 정의해줬다.
내가 만든 **@CurrentUser**도 동작 잘한다!!!