[v4] JWT 필터링

HootJem's avatar
Sep 23, 2024
[v4] JWT 필터링
 
JWT로 로그인 처리한 서버에서 댓글을 작성하려고 한다. Reply가 댓글 로직이라고 할 때 이 곳에서 토큰 검증이 필요하다. (세션에는 저장된 정보가 없기 때문에 토큰 유효성검사만 이루어짐)
 
토큰을 검사하는 방법은 여러가지가 있다.
  • 컨트롤러 메서드 마다 검사 로직 추가하기
  • 인터셉터 활용하기
  • AOP 활용하기
  • 필터에서 처리
 
오늘은 필터에서 특정 url 이 붙은 경우 검사를 하도록 코드를 구성해 볼 것이다.
 
필터가 할 일은 다음과 같다.
  1. JWT 검증
  1. 세션에 저장
  1. 실패시 컨트롤러 진입 하지 않고 리턴.

1. 필터 구성전 살펴보기

ioc 등록 하고 메서드에 @bean 어노테이션 하면 메모리에 등록됨.
리턴값 등록위해 쓰기도함
@Configuration public class FilterConfig { public FilterConfig() { System.out.println("FilterConfig"); } @Bean public User go(){ System.out.println("user Go"); return User.builder().id(1).build(); } // public FilterRegistration jwtAuthorizationFilter() { return null; } }
코드 실행시
  • @Bean 이 없는 경우
    • FilterConfig 만 나옴
  • @Bean 이 있는 경우
    • FilterConfig 와 user Go 둘 다 나옴.

1.2 필터 코드 구성

애플리케이션이 실행될 때 언제 메서드가 메모리에 뜨는지 확인하였다.
이번에는 필터의 동작 예시를 살펴본다.
필터를 만들 때 리퀘스트, 리스폰스 둘 다 해놓고 만들어야 한다.
  • 동작 예시 코드
    • public class JwtAuthorizarionFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; System.out.println("JwtAuthorizarion 필터 동작"); PrintWriter out = response.getWriter(); //쓰기버퍼 System.out.println("<h1>good</h1>"); out.flush(); } }
 

2. 필터 구성하기

  • config 코드
@Configuration // ioc 등록 하고 메서드에 @bean 어노테이션 하면 메모리에 등록됨. 리턴값 등록위해 쓰기도함. public class FilterConfig { @Bean public FilterRegistrationBean<JwtAuthorizationFilter> jwtAuthenticationFilter() { FilterRegistrationBean<JwtAuthorizationFilter> bean = new FilterRegistrationBean<>(new JwtAuthorizationFilter()); bean.addUrlPatterns("/api/*"); // 여기에 오름내림 차순 어떤걸로 시작할 지 골라야함. 마지막 번호 몰라서 처음부터 함. bean.setOrder(0); return bean; } }
 
  • Filter 코드
public class JwtAuthorizationFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain Chain) throws IOException, ServletException { HttpServletRequest rq = (HttpServletRequest) servletRequest; HttpServletResponse resp = (HttpServletResponse) response; String accessToken = rq.getHeader("Authorization"); if (accessToken == null || accessToken.isBlank()) { // resp 해줘야 함 resp.setHeader("Content-Type", "application/json; charset=utf-8"); PrintWriter out = resp.getWriter(); //통신은 객체를 던지면 안된다. fail은 자바객체이다! 필터에서는 인식을 못 한다. Resp fail = Resp.fail(401, "Token이 없습니다."); String responseBody = new ObjectMapper().writeValueAsString(fail); out.print(responseBody); out.flush(); return; } //try catch 를 쓴 이유는 항상 동일의 형태로 담아주기 위해서.. try { User sessionUser = JwtUtil.verify(accessToken); // 서명이 위조, 만료 HttpSession session = rq.getSession(); session.setAttribute("sessionUser", sessionUser); Chain.doFilter(rq, resp); // 다음 필터로 가!! 없으면 DS로 감. } catch (Exception e) { resp.setHeader("Content-Type", "application/json; charset=utf-8"); PrintWriter out = resp.getWriter(); //통신은 객체를 던지면 안된다. fail은 자바객체이다! 필터에서는 인식을 못 한다. Resp fail = Resp.fail(401, e.getMessage()); String responseBody = new ObjectMapper().writeValueAsString(fail); out.print(responseBody); out.flush(); } } }
 

3. Postman 활용하여 확인

  • 확인
notion image
url 이 api 가 들어가 있다면 controller 에 가기전, 먼저 Token 부터 확인한다.

  1. 로그인 해야 토큰이 발급된다.
 
notion image
notion image
 

4. 댓글 작성

지금 토큰은 D/S 앞에서 값을 검증하고 있다. 댓글을 달기 위해서는 유저 토큰 확인 → 컨트롤러에서(해당 유저 정보 체크) → 등등…
과정이 필요한데, 이를 위해 또 다시 토큰을 검증하기는 일이 많아진다. 따라서 토큰을 확인할 때 세션에 잠시 저장을 시켜버린다.
 
  • JwtUtil
public static User verify(String jwt){ jwt = jwt.replace("Bearer ", ""); DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512("metacoding")).build().verify(jwt); int id = decodedJWT.getClaim("id").asInt(); String username = decodedJWT.getClaim("username").asString(); return User.builder() .id(id) .username(username) .build(); }
 
  1. 댓글 쓰기 요청
    1. 상단의 리스폰스 헤더에서 확인한 Authorization 값을 요청 헤더에 넣어준다.
notion image
DTO 형식에 맞게
notion image
요청을 보내면 Body 에서 리턴된 저장된 값을 확인할 수 있고, 만들어진 쿠키를 확인하면 JwtUtil 에서 저장한 값이 들어가 있음을 알 수 있다.
notion image
이렇게 insert 나 update 된 데이터를 리턴하는게 restAPI의 규칙이다.
저장된 값은 H2 콘솔가면 확인가능
 
실패 확인
notion image
 
notion image
이것때문에 예외가 잡아진다. 만약 해당 코드가 없으면 서버 계속 터지는거.
 

  • 서버가 공격받는다면
존재하지 않는 게시글 번호로 10000번 댓글 작성요청을 보낸다.
 
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.URL; public class Attack { public static void main(String[] args) { String targetUrl = "http://127.0.0.1:8080/api/reply"; // 요청할 URL String jsonInputString = """ {"boardId":100, "comment":"댓글이다"} """; for (int i = 0; i < 10000; i++) { new Thread(() -> { try { URL url = new URL(targetUrl); // HttpURLConnection 객체 생성 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 요청 방식 설정 (POST) conn.setRequestMethod("POST"); // 요청에 JSON 형식으로 데이터를 보낸다고 명시 conn.setRequestProperty("Content-Type", "application/json; utf-8"); conn.setRequestProperty("Authorization", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiLrsJTrs7QiLCJpZCI6MSwiZXhwIjoxNzI3NjcyMDQ1LCJ1c2VybmFtZSI6InNzYXIifQ.XJTahPFEbvy8a_RoftAKctxNcknVMec6regFKhqgFKIbfSeUPxCY7ygPNzdEoOvffOcd5eZX0wrTijwIcT2sqA"); conn.setDoOutput(true); PrintWriter out = new PrintWriter(conn.getOutputStream()); out.println(jsonInputString); out.flush(); BufferedReader br = new BufferedReader( new InputStreamReader(conn.getInputStream()) ); System.out.println(br.readLine()); } catch (Exception e) { e.printStackTrace(); } }).start(); } } }
notion image
local로 요청했기 때문에 별 일 없었지만 만약 서비스 환경이었다면 이 요청을 처리하는 동안 다른 클라이언트의 요청처리가 불가능 했을 것이다.
 
이런 공격을 막기 위해 같은 ip 에서 반복적으로 요청하는 것을 확인하여 처리할 수 있다.
코드를 짤 수 있고, 전문으로 처리해 주는 사이트에서 제공을 받을 수 있다.
(Cloudflare 도 있음 - 리버스 프록시 처럼 사용됨)
notion image
 
 

마무리하며
rest api 서버 로 만들려면 단순히 model 하여 페이지 리턴하는 것 삭제,
값이 들어있다면 body 에 insert update 한 값을 담아서 프론트로 넘겨야한다
 
Share article

[HootJem] 개발 기록 블로그