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

url 이 
api 가 들어가 있다면 controller 에 가기전, 먼저 Token 부터 확인한다.- 로그인 해야 토큰이 발급된다.


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();
    }- 댓글 쓰기 요청
상단의 리스폰스 헤더에서 확인한 Authorization 값을 요청 헤더에 넣어준다.

DTO 형식에 맞게 

요청을 보내면 Body 에서 리턴된 저장된 값을 확인할 수 있고,
만들어진 쿠키를 확인하면 
JwtUtil 에서 저장한 값이 들어가 있음을 알 수 있다.
이렇게 insert 나 update 된 데이터를 리턴하는게 restAPI의 규칙이다.
저장된 값은 H2 콘솔가면 확인가능
실패 확인


이것때문에 예외가 잡아진다. 
만약 해당 코드가 없으면 서버 계속 터지는거.
- 서버가 공격받는다면
존재하지 않는 게시글 번호로 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();
        }
    }
}
local로 요청했기 때문에 별 일 없었지만 만약 서비스 환경이었다면 이 요청을 처리하는 동안 다른 클라이언트의 요청처리가 불가능 했을 것이다.
이런 공격을 막기 위해 같은 ip 에서 반복적으로 요청하는 것을 확인하여 처리할 수 있다.
코드를 짤 수 있고, 전문으로 처리해 주는 사이트에서 제공을 받을 수 있다.
(Cloudflare 도 있음 - 리버스 프록시 처럼 사용됨)

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