본문 바로가기
프로젝트/파이널프로젝트-대학 행정 그룹웨어

Interceptor로 관리자 권한 체크하기

by moca7 2024. 12. 1.

 

 

 

ㅁ 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/ 하위의 모든 경로를 매핑합니다.