Spring boot

2024.10.11 Blog 프로젝트 만들기(JPA) 인터셉터 만들어 보기

정훈5 2024. 10. 11. 10:15

 

학습 목표

1. 인터셉터의 개념을 활용해서 기능을 개발 할 수 있다.

 

인터셉터(Interceptor)

스프링 MVC에서 제공하는 기능으로, 클라이언트의 요청을 처리하는 과정에서 특정 작업을 수행할 수 있도록 도와줍니다. 인터셉터는 컨트롤러의 메서드(URI)에 접근하는 과정에서 요청을 가로채어 전처리(pre-processing) 및
후처리(post-processing)를 할 수 있습니다.

필터(Filter)와의 차이점

  • 필터는 서블릿 레벨에서 동작하며, 모든 요청에 대해 작동합니다.
  • 인터셉터는 스프링 MVC 레벨에서 동작하며, 특정 핸들러(컨트롤러)로의 요청에만 작동합니다.

인터셉터를 통해 로그인 여부 확인, 권한 검사, 로깅, 요청 시간 측정 등 다양한 작업을 효율적으로 처리할 수 있습니다.

 

로그인 인터셉터 만들기

로그인 인터셉터는 사용자의 로그인 상태를 확인하고, 로그인하지 않은 사용자가 보호된 리소스에 접근하려 할 때 적절한 조치를 취하는 역할을 합니다.

LoginInterceptor.java

더보기
package com.tenco.blog_v1.common.config;

import com.tenco.blog_v1.common.errors.Exception401;
import com.tenco.blog_v1.user.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

// IOC 안한 상태 이다. (스프링이 실행될 때 메모리에 안띄움)
public class LoginInterceptor  implements HandlerInterceptor {


    /**
     * 컨트롤러 메서드 호출 전에 실행 되는 메서드 이다.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler chosen handler to execute, for type and/or instance evaluation
     * @return
     * @throws Exception
     */
    // 컨트롤러 타기전에 미리 동작한다.
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("LoginInterceptor preHandle 실행");
        // 로그인 여부 판단을 한다.
        HttpSession session = request.getSession(false); // 기존 만들어진 세션이 없다면 null을 반환 한다.
        if(session == null) {
            throw new Exception401("로그인이 필요 합니다");
        }

        // 키 - 값 --> 세션 메모리지에 저장 방식은 map 구조로 저장한다. (sessionUser) 문자열 사용 중
        User sessionUser =  (User) session.getAttribute("sessionUser");
        if(sessionUser == null) {
            throw  new Exception401("로그인이 필요 합니다");
        }

        // return false <- 이면 컨트롤러에 들어가지 않는다.
        return true;
      //  return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    /**
     * 컨트롤러 실행 후, 뷰 렌더링되기(리졸버 타기) 전에 실행되는 메서드
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler the handler (or {@link HandlerMethod}) that started asynchronous
     * execution, for type and/or instance examination
     * @param modelAndView the {@code ModelAndView} that the handler returned
     * (can also be {@code null})
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    /**
     * 뷰가 렌더링 된 후 실행되는 메서드 
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler the handler (or {@link HandlerMethod}) that started asynchronous
     * execution, for type and/or instance examination
     * @param ex any exception thrown on handler execution, if any; this does not
     * include exceptions that have been handled through an exception resolver
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

 

로그인 인터셉터 등록 하기

작성한 인터셉터를 스프링 부트 애플리케이션에 등록하고, 적용할 URL 패턴을 설정합니다.

 

webConfig.java

더보기
package com.tenco.blog_v1.common.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

// @Component // IOC 하나만 메모리

@Configuration // 메서드에서 @bean을 해야한다면 써야 한다.
public class webConfig implements WebMvcConfigurer {

    @Autowired // DI 처리
    private LoginInterceptor loginInterceptor;
    
    /**
     * 인터셉터를 등록하고 적용할 URL 패턴을 설정하는 메서드이다.
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/protected/**") // 인터셉터를 적용시킬 경로 패턴 설정
                .excludePathPatterns("/public/**", "/login", "/logout"); // 인터셉터를 제외할 경로 패턴 설정
    }

}

하지만 LoginInterceptor 아진 빈 등록 처리가 안되어 있다.

 

InterceptorConfig.java

LoginInterceptor 빈으로 등록 처리

  • @Bean: 메서드가 반환하는 객체를 스프링의 빈으로 등록합니다.
  • LoginInterceptor: 앞서 작성한 로그인 인터셉터를 빈으로 등록하여 WebConfig에서 주입받을 수 있게 합니다.
더보기
package com.tenco.blog_v1.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class InterceptorConfig {

    @Bean // 스프링 컨테이너 등록 처리 : 로그인 인터셉터를 빈으로 등록
    // 빈으로 등록 처리
    public LoginInterceptor loginInterceptor() {
        return new LoginInterceptor();
    }
}

User.java 추가

@Column(nullable = false)

private String role;

더보기
package com.tenco.blog_v1.user;

import com.tenco.blog_v1.board.Board;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

import java.sql.Timestamp;
import java.util.List;

@NoArgsConstructor
@Data
@Entity
@Table(name = "user_tb")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(updatable = true) // 유니크 제약조건 설정
    private String username;
    private String password;
    private String email;

    // 일반 사용자라면 "USER"
    // 관리자 사용자라면 "ADMIN"
    @Column(nullable = false)
    private String role; // 등급? 추가

    @CreationTimestamp // 엔티티(Entity) 생성시 자동으로 현재 시간 입력 어노테이션
    private Timestamp createdAt;

    // 단방향, 양방향 매핑(mappedBy)
//    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) // 지연 로딩 설정
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) // 즉시 로딩 설정
    // 앞은 자기자신 기준
    private List<Board> boards;

    @Builder
    public User(Integer id, String username, String password, String email, Timestamp createdAt, List<Board> boards) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.createdAt = createdAt;
        this.boards = boards;
    }

}

 

data.sql 추가

user_tb에 role 컬럼 추가 데이터 추가

더보기
-- 사용자 데이터 삽입
INSERT INTO user_tb(username, password, email, role, created_at) VALUES('길동', '1234', 'a@nate.com', 'USER', NOW());
INSERT INTO user_tb(username, password, email, role, created_at) VALUES('둘리', '1234', 'b@nate.com', 'USER', NOW());
INSERT INTO user_tb(username, password, email, role, created_at) VALUES('마이콜', '1234', 'c@nate.com', 'ADMIN', NOW());

-- 게시글 데이터 삽입
INSERT INTO board_tb(title, content, user_id, created_at) VALUES('제목1', '내용1', 1, NOW());
INSERT INTO board_tb(title, content, user_id, created_at) VALUES('제목2', '내용2', 1, NOW());
INSERT INTO board_tb(title, content, user_id, created_at) VALUES('제목3', '내용3', 2, NOW());
INSERT INTO board_tb(title, content, user_id, created_at) VALUES('제목4', '내용4', 3, NOW());

 

 

인터셉터를 이용한 권한 관리

인터셉터를 활용하여 특정 권한을 가진 사용자만 접근할 수 있도록 설정할 수 있습니다.

예를 들어, 관리자 권한을 가진 사용자만 접근할 수 있는 경로를 설정할 수 있습니다.

 

 

AdminInterceptor.java (복붙)

더보기
package com.tenco.blog_jpa_step3.commom.config;

import com.tenco.blog_jpa_step3.commom.errors.Exception403;
import com.tenco.blog_jpa_step3.user.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class AdminInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("AdminInterceptor preHandle 실행");
        HttpSession session = request.getSession(false);

        if (session == null) {
            throw new Exception403("접근이 금지되었습니다.");
        }

        User sessionUser = (User) session.getAttribute("sessionUser");
        if (sessionUser == null || sessionUser.getUsername().equals("ADMIN")) {
        //if (sessionUser == null || ! sessionUser.isAdmin()) { // isAdmin() 메서드는 User 클래스에 구현되어 있다고 가정
            throw new Exception403("관리자 권한이 필요합니다.");
        }

        // 관리자 권한이 있는 경우 계속 진행
        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        System.out.println("AdminInterceptor postHandle 실행");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("AdminInterceptor afterCompletion 실행");
    }
}

 

InterceptorConfig 코드 추가 (복붙)

더보기
package com.tenco.blog_jpa_step3.commom.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class InterceptorConfig {

    /**
     * 로그인 인터셉터를 빈으로 등록
     */
    @Bean
    public LoginInterceptor loginInterceptor() {
        return new LoginInterceptor();
    }

    @Bean
    public AdminInterceptor adminInterceptor() {
        return new AdminInterceptor();
    }

}

 

WebConfig 코드 추가 (복붙)

더보기
package com.tenco.blog_jpa_step3.commom.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Autowired
    private AdminInterceptor adminInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 로그인 인터셉터 적용
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/protected/**")
                .excludePathPatterns("/public/**", "/login", "/logout");

        // 관리자 인터셉터 적용
        registry.addInterceptor(adminInterceptor)
                .addPathPatterns("/admin/**"); // /admin/** 경로에만 관리자 인터셉터 적용}
    }
}