ㅁ Interceptor (정확히는 HandlerInterceptor)
- servlet에서 특정 controller가 실행되기 전, 실행된 후에 낚아채서 실행할 내용을 정의할 수 있다.
- 특정 요청을 할 수 있는 회원이 맞는지(로그인 여부 판단),
특정 요청을 할 수 있는 권한이 맞는지(회원의 권한 체크)
- preHandle(전처리 담당 메소드) : DispatcherServlet이 특정 Controller를 호출하기 전에 낚아채는 영역
- postHandle(후처리 담당 메소드) : Controller에서 요청 처리 후 DispatcherServlet으로 뷰 정보가 돌아가는 순간 낚아채는 영역
ㅁ 현재 문제점
- 관리자 계정이 아닌 일반 계정으로 로그인해본다.
- 주소창에 http://localhost:9999/equipmentAndFacility/list을 입력하면 비품/시설 목록 관리 페이지가 뜬다.
이 페이지는 로그인한 유저가 관리자 계정일 때만 보여져야 한다.
- 주소창에 http://localhost:9999/reservation/approveReservation를 입력하면 비품/시설 예약관리 페이지가 뜬다
이 페이지도 로그인한 유저가 관리자 계정일 때만 보여져야 한다.
- 스프링의 인터셉터를 사용해서 url에 해당하는 Controller가 실행되기 이전에 로그인한 유저가 관리자인지 권한체크를 하도록 한다.
ㅁ 스프링의 경우
(1) com.br.gdcampus.interceptor 패키지 생성
(2) com.br.gdcampus.interceptor에 AdminCheckInterceptor (일반) 클래스 생성
package com.br.gdcampus.interceptor;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.support.RequestContextUtils;
import com.br.gdcampus.dto.UserDto;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AdminCheckInterceptor implements HandlerInterceptor{
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpSession session = request.getSession();
if (session.getAttribute("loginUser") != null) {
UserDto loginUser = (UserDto) session.getAttribute("loginUser");
String userNo = loginUser.getUserNo();
if (userNo != null && !userNo.isEmpty() && userNo.charAt(0) == 'A') { // userNo의 첫 문자가 'A'인지 확인
log.debug("[AdminCheckInterceptor] 관리자 계정 확인: 로그인한 유저는 관리자입니다. (userNo: {})", userNo);
return true; // 정상적으로 컨트롤러 실행
} else {
// alert 메세지와 함께 메인페이지로 redirect.
// RedirectAttributes의 대안
FlashMap flashMap = new FlashMap(); // Map같은거라 키-밸류 세트로 무언가를 담을 수 있다.
flashMap.put("alertMsg", "관리자 계정만 접근 가능합니다.");
// RequestContextUtils.getFlashMapManager(request) 여기까지가 FlashMapManager 객체를 반환한다.
RequestContextUtils.getFlashMapManager(request).saveOutputFlashMap(flashMap, request, response);
response.sendRedirect(request.getContextPath());
return false; // 컨트롤러 실행 차단
}
} else {
// alert 메세지와 함께 메인페이지로 redirect.
FlashMap flashMap = new FlashMap();
flashMap.put("alertMsg", "로그인 후 이용가능한 서비스입니다.");
RequestContextUtils.getFlashMapManager(request).saveOutputFlashMap(flashMap, request, response);
response.sendRedirect(request.getContextPath());
return false; // 컨트롤러 실행 차단
}
}
}
i) HandlerInterceptor를 implements한다.
ii) ctrl을 누르고 클릭해서 HandlerInterceptor를 가서 메소드 preHandle을 가져온다.
- 오버라이딩이기 때문에 매개변수의 타입이나 개수는 건들면 안된다.
- preHandle 메소드에 빨간 줄이 뜬다. default에서 public으로 수정하라고 뜬다.
인터페이스를 구현할 때 default 키워드는 사용할 수 없다.
- 오버라이딩이어도 접근제한자는 수정가능하다. 부모보다 더 크게는 가능하다. default에서 public으로 수정한다.
iii) HttpSession session = request.getSession();을 작성한다.
- preHandle 메소드의 매개변수를 보면 요청과 관련된 객체인 request와 응답과 관련된 객체인 response가 있다.
request가 있으면 session을 꺼낼 수 있다.
iv) session으로부터 loginUser라는 key로 담은 Object 타입의 value 값을 꺼낸다.
- UserDto 객체로 형변환하고 getUserNo() 메소드로 현재 로그인한 유저의 사번을 알아내서 관리자 계정인지 확인한다.
- 로그인되어있지 않거나, 로그인 했지만 관리자 계정이 아닌 경우에는 alert 메세지를 띄우고 메인페이지로 redirect시킨다.
- preHandle 메소드의 반환형이 boolean이라 true나 false를 반환해줘야 한다.
true를 리턴하면 원래 실행하려하던 컨트롤러가 쭉 이어서 실행된다.
false를 리턴하면 컨트롤러가 실행되지 않는다.
v) RedirectAttributes 대신 FlashMap을 사용한다.
- 컨트롤러에서 RedirectAttributes를 쓸 때는 메소드에 매개변수로 추가해서 이용했었다.
preHandle 메소드는 오버라이딩한 메소드이기 때문에 함부로 매개변수를 추가할 수 없다.
- flashMap.put()은 FlashMap 객체에 데이터를 넣는 것이고,
saveOutputFlashMap()은 그 FlashMap을 실제로 저장하는 것이다.
vi) response.sendRedirect(request.getContextPath()); 구문으로 메인페이지로 리다이렉트(url 재요청)한다.
(3) servlet-context.xml에 인터셉터 등록
<?xml version="1.0" encoding="UTF-8"?>
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<resources mapping="/upload/**" location="file:///upload/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.br.spring" />
<!-- webSocket 관련 등록 구문 -->
<beans:bean class="com.br.spring.handler.ChatEchoHandler" id="chatEchoHandler" />
<websocket:handlers>
<websocket:mapping handler="chatEchoHandler" path="/chat" />
<websocket:handshake-interceptors>
<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
</websocket:handshake-interceptors>
<websocket:sockjs />
</websocket:handlers>
<!-- interceptor 관련 등록 구문 -->
<interceptors>
<interceptor>
<mapping path="/equipmentAndFacility/list " />
<mapping path="/reservation/approveReservation" />
<beans:bean class="com.br.gdcampus.interceptor.AdminCheckInterceptor" id="AdminCheckInterceptor"/>
</interceptor>
<!--
<interceptor>
</interceptor>
-->
</interceptors>
<!-- Scheduler -->
<task:annotation-driven />
</beans:beans>
- 웹소켓 관련 등록 구문 아래에 인터셉터 관련 등록 구문을 추가한다.
- 인터셉터 클래스를 여러개 만들고 여러개 등록할 수도 있다.
interceptors 안에 interceptor 태그를 여러개 두면 된다.
ㅁ 스프링 부트의 경우
(1) com.br.gdcampus.interceptor 패키지 생성
(2) com.br.gdcampus.interceptor에 AdminCheckInterceptor (일반) 클래스 생성
package com.br.gdcampus.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.support.RequestContextUtils;
import com.br.gdcampus.dto.UserDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component // servlet-context.xml 대체
public class AdminCheckInterceptor implements HandlerInterceptor{
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpSession session = request.getSession();
if (session.getAttribute("loginUser") != null) {
UserDto loginUser = (UserDto) session.getAttribute("loginUser");
String userNo = loginUser.getUserNo();
if (userNo != null && !userNo.isEmpty() && userNo.charAt(0) == 'A') { // userNo의 첫 문자가 'A'인지 확인
log.debug("[AdminCheckInterceptor] 관리자 계정 확인: 로그인한 유저는 관리자입니다. (userNo: {})", userNo);
return true; // 정상적으로 컨트롤러 실행
} else {
// alert 메세지와 함께 메인페이지로 redirect.
// RedirectAttributes의 대안
FlashMap flashMap = new FlashMap(); // Map같은거라 키-밸류 세트로 무언가를 담을 수 있다.
flashMap.put("alertMsg", "관리자 계정만 접근 가능합니다.");
// RequestContextUtils.getFlashMapManager(request) 여기까지가 FlashMapManager 객체를 반환한다.
RequestContextUtils.getFlashMapManager(request).saveOutputFlashMap(flashMap, request, response);
response.sendRedirect(request.getContextPath());
return false; // 컨트롤러 실행 차단
}
} else {
// alert 메세지와 함께 메인페이지로 redirect.
FlashMap flashMap = new FlashMap();
flashMap.put("alertMsg", "로그인 후 이용가능한 서비스입니다.");
RequestContextUtils.getFlashMapManager(request).saveOutputFlashMap(flashMap, request, response);
response.sendRedirect(request.getContextPath());
return false; // 컨트롤러 실행 차단
}
}
}
- AdminCheckInterceptor 클래스 위에 @Component 어노테이션을 붙인다.
@Component이 스프링에서의 servlet-context.xml에 빈 등록하는 구문을 대체한다.
// 스프링 부트(톰캣 10버전)의 경우
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
// 스프링(톰캣 9버전)의 경우
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
- 현재 사용한 버전(3.2.11)의 스프링 부트는 내장 톰캣이 10버전이어서 HttpServletRequest, HttpServletResponse, HttpSession 클래스 import를 다시 해야 한다.
- 스프링 때는 톰캣 9버전이어서 javax 패키지에서 import 했다.
스프링 부트는 톰캣 10버전이어서 jakarta 패키지에서 import 해온다.
(3) com.br.gdcampus.config의 WebMvcConfig에 인터셉터 등록
package com.br.gdcampus.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.br.gdcampus.interceptor.AdminCheckInterceptor;
import com.br.gdcampus.interceptor.LoginCheckInterceptor;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final LoginCheckInterceptor loginCheckInterceptor;
private final AdminCheckInterceptor adminCheckInterceptor;
// (스프링) servlet-context.xml의 <resources> 태그 설정 대신 정적 자원 경로를 매핑하는 메소드
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("classpath:/static/");
// 첨부파일 조회할 때
// file:///upload/는 서버의 루트 디렉토리(C 드라이브 기준) 아래에 upload 폴더가 있다고 가정
// <resources mapping="/upload/**" location="file:///upload/" />
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:///upload/");
}
// 인터셉터 등록 메소드
public void addInterceptors(InterceptorRegistry registry) {
// 로그인 체크 인터셉터
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/main2.do")
.addPathPatterns("/user/profile/*");
// 관리자 체크 인터셉터(상우)
registry.addInterceptor(adminCheckInterceptor)
.addPathPatterns("/equipmentAndFacility/list")
.addPathPatterns("/reservation/approveReservation");
}
}
- 의존성 주입(DI)을 통해 interceptor 필드를 초기화한다.
- addInterceptors 메소드를 오버라이딩 후 아래 내용을 작성한다. (여러개의 Interceptor 등록 가능)
registry.addInterceptor(실행시킬Interceptor객체)
.addPathPatterns("url mapping값")
.addPathPatterns("url mapping값");
(4) contextPath 이슈가 있다.
- Interceptor 클래스에 contextPath로 리다렉트 구문 작성시 contextPath가 빈 문자열일 경우 제대로 redirect 되지 않는다.
- 스프링 때는 response.sendRedirect(request.getContextPath());로 리다이렉트했다.
- 스프링 부트는 context path가 빈 문자열이라서 제대로 리다이렉트되지 않는다.
response.sendRedirect(request.getContextPath().equals("") ? "/" : request.getContextPath());로 수정한다.
ㅁ 서버 start
- 로그인하지 않은 상태로 url을 써서 이동을 요청하면 alert가 뜨고 메인페이지(http://localhost:9999/)로 이동된다.
- 관리자 계정이 아닌 계정으로 로그인 후 url을 써서 이동을 요청하면 alert가 뜨고 메인페이지(http://localhost:9999/)로 이동된다.
- 관리자 계정으로 로그인후 url을 써서 이동을 요청하면 제대로 가지고, AdminCheckInterceptor에 작성한 log 출력문도 제대로 출력된다.
============================================================================
ㅁ 로그인 여부도 체크하기
- WebMvcConfig에 LoginCheckInterceptor를 등록하는 구문에 url을추가한다.
※ 애스터리스크(*)
- /user/profile/* : /user/profile/ 하위의 1단계 경로만 매핑됩니다.
- /user/profile/** : /user/profile/ 하위의 모든 경로를 매핑합니다.