[TIL]26_05_04 심화 스프링 CH1 1-1 ~ 1-3

전은기·2026년 5월 4일

Filter란?

웹 애플리케이션에서 클라이언트의 요청이 스프링 컨테이너(DispatcherServlet)에 도달하기 전/후에 위치하여 요청과 응답을 가로채고 검사, 변경하는 공통 처리 컴포넌트입니다. 인증, 로깅, 인코딩 변환 등 전역적인 부가 작업을 수행하는 자바 Servlet 표준 기술이다

기존 알고 있는 동작 순서
post man(user) -> 요청 -> Controller -> 응답 -> postman(user)

post man(user) -> 요청 -> filter -> Controller -> filter -> 응답 -> postman(user)

필터를 통해 다양한 작업을 처리할 수 있다

Filter 구조

OncePerRequestFilter
- 요청 당 한 번만 실행되도록 보장
- 인증/인가, 로깅, 트랜잭션 관리 같은 곳에서 자주 사용

CharacterEncodingFilter
- 모든 요청/응답에 지정한 인코딩을 적용

HiddenHttpMethodFilter
- HTML form은 GET, POST만 지원 -> _method파라미터로 PUT, DELETE 등 매핑 가능하게 해줌

FormContentFilter
-application/x-www-form-urlencoded데이터를 PUT, PATCH, DELETE 요청에서도 읽을 수 있게 해줌

RequestContextFilter
- 현재 요청을 RequestContextHolder에 바인딩
- @RequestScopeBean 사용 시 필요

필터 구현

filterChain.doFilter(request, response);를 기준으로 필터를 통과해서 들어갈 때와 필터를 통과해 나갈 때를 구분 짓는다

Filter는 단일으로 존재하지 않고 묶음 단위로 생각하면 편하다

필터 순서 지정은 @Order를 사용하면 순서를 지정할 수 있다


그럼 이미지 상으로 프로그램이 실행이 되면

이 순서로 실행이 된다

Filter 적용 시점

  1. 로깅(요청/응답)
    • 모든 컨트롤러 호출 전후에서 공통적으로 사용 가능
    • 장애 발생 시 어느 API에서 문제 발생했는지 빠르게 추적가능
    • 성능 분석에 활용 가능
  2. 공통 인증/권한 체크(JWT)
    • 컨트롤러까지 들어오기 전 인증 실패 요청을 걸러낼 수 있음
    • 인증/인가 로직을 중앙 집중화 -> 코드 중복 제거
    • 보안상 안전(미인증 요청이 내부 로직까지 내려가지 않음)

JWT 란

JWT는 JSON Web Token의 약자로 사용자의 정보를 담아 이를 암호화한 JSON 객체이다. 기존의 Cookie와 Session는 각각 보안 취약점과 stateless 위반의 문제가 있었지만 JWT는 이를 해역하기 위해 등장했다.
JWT는 암호화 알고리즘을 통해 디지털로 서명되기 때문에 인증되었고 뢰할 수 있는 토큰이다. JWT는 공개키 암호 방식(PKC) 즉 비대칭키 방식이다.

JWT 사용할 때 필요한 것

  • 어떤 암호화 알고리즘을 사용할 건인지(HS256 (HMAC with SHA-256), RS256 (RSASSA-PKCS1-v1_5 with SHA-256), PS256 (RSASSA-PSS with SHA-256))

  • 어떤 정보를 넣을 것인지

  • 어떤 암호키로 암호화 할 것 인지 보통은 Secrect Key를 넣음

  • 만료 시간

만료시간이 필요한 이유
- JWT는 최초 로그인이 성공하면 이후 검증을 안하므로 보안에 약함
- 안정 장치로 만료시간을 지정

JWT 흐름

토큰을 발행하는 경우

Client 입장
- 인증/인가를 위해 로그인

Server 입장
- Client가 보낸 정보를 기반으로 로그인 시도
- 로그인에 성공하면 JWT 토큰을 발행

토큰을 검사하는 경우

Client 입장

Postman을 통해서 요청을 보냄
요청 시 jwt 토큰을 보내고 싶으면 헤더에 포함해서 보냄

[Authorization → Bearer Token → JWT 토큰 넣어주기]

header에 자동으로 아래처럼 설정됨

Server 입장

1. Client에서 보낸 요청 중 header에 `JWT` 토큰이 들어있는지 검사
2. `JWT` 토큰이 있으면 해당 토큰이 유효한 토큰인지 검사
3. 유효한 토큰이라면 `JWT` 토큰 안에 들어 있는 값을 복호화 함

JWT 실습

build.gradle 추가

	implementation "io.jsonwebtoken:jjwt-api:0.12.5"
    runtimeOnly  "io.jsonwebtoken:jjwt-impl:0.12.5"
    runtimeOnly  "io.jsonwebtoken:jjwt-jackson:0.12.5"

application.yml


spring:
  datasource:
    url: jdbc:mysql://localhost:3306/nbcam
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
    defer-datasource-initialization: true

jwt :
  secret:
    key : asdfghjklqwertyuiopzxcvbnm1234567890asdfghjklqwertyuiopzxcvbnm1234567890asdfghjklqwertyuiopzxcvbnm1234567890

JwtUtil


package com.sparta.cch.advance.common.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {

    public static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

    @Value("${jwt.secret.key}")           // application.yml 에 있는 key 가져옴
    private String secretKeyString;

    private SecretKey key;
    private JwtParser parser;

    /**
     * 빈 초기화 메서드
     * @PostConstruct 어플리케이션 실행 될 때 가장 먼저 실행 되게 하는 어노테이션
     */

    @PostConstruct
    public void init() {
        byte[] bytes = Decoders.BASE64.decode(secretKeyString);
        this.key = Keys.hmacShaKeyFor(bytes);
        this.parser = Jwts.parser()
                .verifyWith(this.key)
                .build();
    }


    // 토큰 생성
    public String generateToken(String username) {
        Date now = new Date();
        return BEARER_PREFIX + Jwts.builder()
                .claim("username", username)
                .issuedAt(now)
                .expiration(new Date(now.getTime() + TOKEN_TIME))
                .signWith(key, Jwts.SIG.HS256)
                .compact();
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        if (token == null || token.isBlank()) return false;
        try {
            parser.parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            // 개별 예외 분리 없음: 서명/형식/만료 등 모든 실패를 한 번에 처리
            log.info("Invalid JWT: {}", e.toString());
            return false;
        }
    }



    // 토큰 복호화
    private Claims extractAllClaims(String token) {
        return parser.parseSignedClaims(token).getPayload();
    }

    public String extractUsername(String token) {
        return extractAllClaims(token).get("username", String.class);
    }


}

UserController


package com.sparta.cch.advance.user.controller;

import com.sparta.cch.advance.common.utils.JwtUtil;
import com.sparta.cch.advance.user.model.request.LoginRequest;
import com.sparta.cch.advance.user.model.response.LoginResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
@Slf4j
public class UserController {

    private final JwtUtil jwtUtil;

    @GetMapping("/get")
    public String getUserInfo(HttpServletRequest request) {
        String username = (String) request.getAttribute("username");
        log.info(username);
        return username;
    }

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {

        String token = jwtUtil.generateToken(request.getUsername());

        return ResponseEntity.ok(new LoginResponse(token));
    }

    @GetMapping("/validate")
    public ResponseEntity<Boolean> checkValidata(HttpServletRequest request) {

        String authorizationHeader = request.getHeader("Authorization");

        String jwt = authorizationHeader.substring(7);

        Boolean validate = jwtUtil.validateToken(jwt);

        return ResponseEntity.ok(validate);
    }

    @GetMapping("/username")
    public ResponseEntity<String> getUsername(HttpServletRequest request) {
        String authorizationHeader = request.getHeader("Authorization");

        String jwt = authorizationHeader.substring(7);

        String username = jwtUtil.extractUsername(jwt);

        return ResponseEntity.ok(username);
    }


}

JWT와 필터

JWT를 사용해 토큰 검증과 유효성 검사를 구현했다. 하지만 Filter를 활용하면 이러한 검사를 매 요청마다 반복해서 처리하지 않아도 된다.

profile
개발자가 되고 싶습니다

0개의 댓글