웹 애플리케이션에서 클라이언트의 요청이 스프링 컨테이너(DispatcherServlet)에 도달하기 전/후에 위치하여 요청과 응답을 가로채고 검사, 변경하는 공통 처리 컴포넌트입니다. 인증, 로깅, 인코딩 변환 등 전역적인 부가 작업을 수행하는 자바 Servlet 표준 기술이다
기존 알고 있는 동작 순서
post man(user) -> 요청 -> Controller -> 응답 -> postman(user)
post man(user) -> 요청 -> filter -> Controller -> filter -> 응답 -> postman(user)
필터를 통해 다양한 작업을 처리할 수 있다
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를 사용하면 순서를 지정할 수 있다


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

이 순서로 실행이 된다
JWT는 JSON Web Token의 약자로 사용자의 정보를 담아 이를 암호화한 JSON 객체이다. 기존의 Cookie와 Session는 각각 보안 취약점과 stateless 위반의 문제가 있었지만 JWT는 이를 해역하기 위해 등장했다.
JWT는 암호화 알고리즘을 통해 디지털로 서명되기 때문에 인증되었고 뢰할 수 있는 토큰이다. JWT는 공개키 암호 방식(PKC) 즉 비대칭키 방식이다.
어떤 암호화 알고리즘을 사용할 건인지(HS256 (HMAC with SHA-256), RS256 (RSASSA-PKCS1-v1_5 with SHA-256), PS256 (RSASSA-PSS with SHA-256))
어떤 정보를 넣을 것인지
어떤 암호키로 암호화 할 것 인지 보통은 Secrect Key를 넣음
만료 시간
만료시간이 필요한 이유
- JWT는 최초 로그인이 성공하면 이후 검증을 안하므로 보안에 약함
- 안정 장치로 만료시간을 지정
토큰을 발행하는 경우
Client 입장
- 인증/인가를 위해 로그인
Server 입장
- Client가 보낸 정보를 기반으로 로그인 시도
- 로그인에 성공하면 JWT 토큰을 발행
토큰을 검사하는 경우
Client 입장
Postman을 통해서 요청을 보냄
요청 시 jwt 토큰을 보내고 싶으면 헤더에 포함해서 보냄
[Authorization → Bearer Token → JWT 토큰 넣어주기]

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

Server 입장
1. Client에서 보낸 요청 중 header에 `JWT` 토큰이 들어있는지 검사
2. `JWT` 토큰이 있으면 해당 토큰이 유효한 토큰인지 검사
3. 유효한 토큰이라면 `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를 사용해 토큰 검증과 유효성 검사를 구현했다. 하지만 Filter를 활용하면 이러한 검사를 매 요청마다 반복해서 처리하지 않아도 된다.