Spring boot

Bank App 만들기 - intercepter 활용(인증검사 공통 처리)

ryeonng 2024. 8. 16. 12:41

 

intercepter

인터셉터는 Spring MVC의 핵심 기능 중 하나로, 웹 애플리케이션에서 공통적인 처리를 재사용할 수 있게 해주는 강력한 도구이다.

인터셉터(Interceptor)는 들어오는 요청과 나가는 응답을 가로채어 특정 로직을 수행할 수 있게 해주는 매커니즘을 제공한다. 이는 AOP(Aspect-Oriented Programming)의 일종으로 볼 수 있으며, 컨트롤러(Controller)로 요청이 도달하기 전, 후 또는 완료된 후에 추가적인 처리를 하기 위해 사용된다.

대표적인 활용 사례

  1. 인증 및 권한 부여 : 사용자의 인증 정보를 검사하여 요청이 유효한 사용자로부터 온 것인지 확인하고, 특정 자원에 대한 접근 권한을 확인한다.
  2. 로깅 및 감사 : 요청의 처리 과정에 대한 로깅을 수행하거나 감사 로그를 생성하여 시스템의 보안과 무결성을 유지하는 데 도움을 준다.
  3. 성능 모니터링 : 요청 처리 시간을 측정하고 성능 문제를 식별하기 위한 메트릭을 수집한다.
  4. 공통적인 응답 데이터 추가 : 모든 응답에 공통적으로 포함되어야 하는 헤더나 데이터를 추가한다.

 

인터셉터 구현 방법

먼저 딱 2가지만 기억하자.

  1. 동작 시키고자 하는 인터셉터 기능을 클래스로 만들어 준다. 단, 만들고 자 하는 해당 클래스에 HandlerInterceptor 인터페이스를 구현하거나 HandlerInterceptorAdapter 클래스를 상속받아야 한다.
  2. 내가 만든 인터셉터를 Spring Boot 애플리케이션에 등록을 해주어야 동작 한다. 등록시에는 WebMvcConfigurer 인터페이스를 구현하는 설정 클래스에서 addInterceptors 메서드를 오버라이드하여 인터셉터를 등록한다.

당연히 필요하다면 인터셉터를 구현한 사용자 정의 클래스를 여러개 정의해서 프로젝트에 활용 할 수 있다.

 

 

인터셉터 구현 클래스 만드는 방법과 인터셉트 등록 처리

package com.tenco.bank.handler;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component // IoC 대상 (싱글톤 패턴)
public class AuthInteceptor implements HandlerInterceptor {
	
	// preHandle 동작 흐름 (단, 스프링부트 설정파일, 설정 클래스에 등록이 되어야 동작함)
	// 컨트롤러가 들어오기 전에 이 메서드가 먼저 동작
	// return 값 -- true : 컨트롤러 안으로 들여 보낸다. --false : 컨트롤러 안으로 들어갈 수 없다.
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		return HandlerInterceptor.super.preHandle(request, response, handler);
	}
	
	
	// postHandle
	// 뷰가 렌더링되기 바로 전에 콜백 되는 메서드
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		// TODO Auto-generated method stub
		HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
	}
	
	// 요청 처리가 완료된 후. 즉, 뷰가 완전히 렌더링 된 후에 호출 된다.
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		// TODO Auto-generated method stub
		HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
	}
}
  • 컨트롤러 호출 전 : preHandle
  • 컨트롤러 호출 후 : postHandle
  • 요청 완료 이후 : afterCompletion, 뷰가 렌더링 된 이후에 호출된다.

 

config/WebMvcConfig.java 파일 생성 - 인터셉터 등록 하기

package com.tenco.bank.config;

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

import com.tenco.bank.handler.AuthInterceptor;

import lombok.RequiredArgsConstructor;

// WebMvcConfigurer : 약속 - 설정파일
@Configuration // 하나의 클래스를 IOC 하고 싶을때 사용
@RequiredArgsConstructor
public class WebWvcConfig implements WebMvcConfigurer{

	@Autowired // DI
	private final AuthInterceptor authInterceptor;
	
	// @RequiredArgsConstructor : 생성자 대신 사용 가능
	
	// 우리가 만들어 놓은 AuthInterceptor를 등록해야 한다.
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(authInterceptor)
				.addPathPatterns("/account//**")
				.addPathPatterns("/auth/**");
		
	}
}

 

AccountController 인증 검사 제거 및 테스트 생성자 변경
package com.tenco.bank.controller;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;

import com.tenco.bank.dto.DepositDTO;
import com.tenco.bank.dto.SaveDTO;
import com.tenco.bank.dto.TransferDTO;
import com.tenco.bank.dto.WithdrawalDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.UnAuthorizedException;
import com.tenco.bank.repository.model.Account;
import com.tenco.bank.repository.model.HistoryAccount;
import com.tenco.bank.repository.model.User;
import com.tenco.bank.service.AccountService;
import com.tenco.bank.utils.Define;

import jakarta.servlet.http.HttpSession;

@Controller // IoC대상 (싱글톤으로 관리)
@RequestMapping("/account") // 대문열기
public class AccountController {

	// 계좌 생성 화면 요청 - DI 처리
	private final HttpSession session;
	private final AccountService accountService; // 멤버변수 선언 시 final 사용하면 성능적으로 더 낫다.

	// @Autowired
	public AccountController(HttpSession session, AccountService accountService) {
		this.session = session;
		this.accountService = accountService;
	}

	
	
	/**
	 * 계좌 생성 페이지 요청 주소설계 - http://localhost:8080/account/save
	 * 
	 * @return save.jsp
	 */
	@GetMapping("/save")
	public String savePage() {

		
		return "account/save";
	}

	/**
	 * 계좌 생성 기능 요청 주소설계 - http://localhost:8080/account/save
	 * 
	 * @return : 추후 계좌 목록 페이지로 이동 처리
	 */
	@PostMapping("/save")
	public String saveProc(SaveDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {

		// 3. 유효성 검사
		if (dto.getNumber() == null || dto.getNumber().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}

		if (dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}

		if (dto.getBalance() == null || dto.getBalance() <= 0) {
			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
		}

		// 4. 서비스 호출
		accountService.createAccount(dto, principal.getId());

		return "redirect:/account/list";
	}

	/**
	 * 계좌 목록 페이지 요청 주소설계 - http://localhost:8080/account/list , ..../
	 * 
	 * @return
	 */
	@GetMapping({ "/list", "/" }) // url 매핑을 두 개 설정 가능하다.
	public String listPage(Model model, @SessionAttribute(Define.PRINCIPAL) User principal) { // TODO - 검색 기능 + 페이징 처리 추가 가능
		// 2. 유효성 검사 - 추출할 데이터 없으므로 아직 필요 x

		// 3. 서비스 호출
		List<Account> accountList = accountService.readAccountListByUserId(principal.getId());
		if (accountList.isEmpty()) { // 리스트는 있으나, 값이 비어있는 경우
			model.addAttribute("accountList", null); // 값은 null
		} else {
			model.addAttribute("accountList", accountList);
		}
		// JSP에 데이터를 넣어주는 기술 - set/getAttribute
		return "account/list";
	}

	/**
	 * 출금 페이지 요청
	 * 
	 * @return withdrawal.jsp
	 */
	@GetMapping("/withdrawal")
	public String withdrawalPage() {
		
		return "account/withdrawal";
	}

	/**
	 * 출금 기능 요청
	 * 
	 * @param dto
	 * @return
	 */
	@PostMapping("/withdrawal")
	public String withdrawalProc(WithdrawalDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {

		// 유효성 검사 (직접 자바 코드를 통해 개발했었지만) -> 스프링부트에서 제공하는 유효성 검사 라이브러리 @Valid 존재
		if (dto.getAmount() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
		}

		if (dto.getAmount().longValue() <= 0) {
			throw new DataDeliveryException(Define.W_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
		}

		if (dto.getWAccountNumber() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}

		if (dto.getWAccountPassword() == null || dto.getWAccountPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}

		accountService.updateAccountWithdraw(dto, principal.getId());

		return "redirect:/account/list";
	}

	/**
	 * 입금 페이지 요청
	 * 
	 * @return deposit.jsp
	 */
	@GetMapping("/deposit")
	public String depositPage() {
		
		return "account/deposit";
	}

	/**
	 * 입금 기능 요청
	 * 
	 * @param dto
	 * @return
	 */
	@PostMapping("/deposit")
	public String depositProc(DepositDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {
		// 유효성 검사
		if (dto.getAmount() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
		}

		if (dto.getAmount().longValue() <= 0) {
			throw new DataDeliveryException(Define.W_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
		}

		if (dto.getDAccountNumber() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}

		accountService.updateAccountDeposit(dto, principal.getId());

		return "redirect:/account/list";
	}

	/**
	 * 이체 페이지 요청
	 * 
	 * @return transfer.jsp
	 */
	@GetMapping("/transfer")
	public String transferPage() {
		
		return "account/transfer";
	}

	/**
	 * 이체 기능 처리 요청
	 * 
	 * @param dto
	 * @return
	 */
	@PostMapping("/transfer")
	public String tranferProc(TransferDTO dto, @SessionAttribute(Define.PRINCIPAL) User principal) {

		// 유효성 검사
		if (dto.getAmount() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
		}

		if (dto.getAmount().longValue() <= 0) {
			throw new DataDeliveryException(Define.W_BALANCE_VALUE, HttpStatus.BAD_REQUEST);
		}
		
		if (dto.getWAccountNumber() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}

		if (dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}
		
		if (dto.getDAccountNumber() == null) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}
		
		accountService.updateAccountTransfer(dto, principal.getId());

		return "redirect:/account/list";
	}

	/**
	 * 계좌 상세 보기 페이지
	 * 주소설계 - http://localhost:8080/account/detail/1?type=all, deposit, withdraw
	 * @RequestParam - 쿼리 스트링으로 들어오는 주소를 받아오는 방법
	 * @return
	 */
	@GetMapping("/detail/{accountId}")
	public String detail(@PathVariable(name = "accountId") Integer accountId, 
			@RequestParam(required = false, name = "type") String type,
			@RequestParam(name = "page", defaultValue = "1") int page,
			@RequestParam(name = "size", defaultValue = "2") int size,
			Model model) {
		
		
		
		// 유효성 검사
		List<String> validTypes = Arrays.asList("all", "deposit", "withdrawal");
		
		if(!validTypes.contains(type)) {
			throw new DataDeliveryException("유효하지 않은 접근 입니다.", HttpStatus.BAD_REQUEST);
		}
		
		// 페이지 개수를 계산하기 위해 총 페이지 수를 계산해주어야 한다.
		int totalRecords = accountService.countHistoryByAccountIdAndType(type, accountId);
		int totalPages = (int)Math.ceil((double)totalRecords / size);
		
		Account account = accountService.readAccountById(accountId);
		List<HistoryAccount> historyList = accountService.readHistoryByAccountId(type, accountId, page, size);
		
		model.addAttribute("account", account);
		model.addAttribute("historyList", historyList);
		
		model.addAttribute("currentPage", page);
		model.addAttribute("totalPages", totalPages);
		model.addAttribute("type", type);
		model.addAttribute("size", size);
		
		
		return "account/detail";
	}
	
	
	
}

 

UserController 생성자 수정 및 코드 추가
package com.tenco.bank.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.tenco.bank.dto.SignInDTO;
import com.tenco.bank.dto.SignUpDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.repository.model.User;
import com.tenco.bank.service.UserService;
import com.tenco.bank.utils.Define;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;

@Controller  
@RequestMapping("/user")  
@RequiredArgsConstructor
public class UserController {
	
	@Autowired
	private final UserService userService;
	private final HttpSession session;
	
	/**
	 * 회원 가입 페이지 요청 
	 * 주소 설계 : http://localhost:8080/user/sign-up
	 * @return signUp.jsp 
	 */
	@GetMapping("/sign-up")
	public String signUpPage() {
		return "user/signUp";
	}
	
	/**
	 * 회원 가입 로직 처리 요청
	 * 주소 설계 : http://localhost:8080/user/sign-up
	 * @param dto
	 * @return
	 */
	@PostMapping("/sign-up")
	public String signUpProc(SignUpDTO dto) {
		if(dto.getUsername() == null || dto.getUsername().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_USERNAME, HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getFullname() == null || dto.getFullname().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_FULLNAME, HttpStatus.BAD_REQUEST);
		}
		userService.createUser(dto);
		return "redirect:/user/sign-in";
	}
	
	/**
	 * 로그인 화면 요청 
	 * @return
	 */
	@GetMapping("/sign-in")
	public String singInPage() { 
		return "user/signIn";
	}
	
	
	/**
	 * 로그인 요청 처리 
	 * 주소설계 : http://localhost:8080/user/sign-in
	 * @return
	 */
	@PostMapping("/sign-in")
	public String signProc(SignInDTO dto) {
		if(dto.getUsername() == null || dto.getUsername().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_USERNAME, HttpStatus.BAD_REQUEST);
		}
		if(dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}
		User principal = userService.readUser(dto);
		
		session.setAttribute(Define.PRINCIPAL, principal);
		
		return "redirect:/account/list"; 
	}
	
	/**
	 * 로그아웃 처리 
	 * @return
	 */
	@GetMapping("/logout")
	public String logout() {
		session.invalidate(); 
		return "redirect:/user/sign-in";
	}
}