1. 세팅 코드
1) handler 폴더
① Exception400.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\handler\ex\Exception400.java
package com.metacoding.springv2._core.handler.ex;
public class Exception400 extends RuntimeException {
public Exception400(String message) {
super(message);
}
}② Exception401.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\handler\ex\Exception401.java
package com.metacoding.springv2._core.handler.ex;
public class Exception401 extends RuntimeException {
public Exception401(String message) {
super(message);
}
}③ Exception403.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\handler\ex\Exception403.java
package com.metacoding.springv2._core.handler.ex;
public class Exception403 extends RuntimeException {
public Exception403(String message) {
super(message);
}
}④ Exception404.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\handler\ex\Exception404.java
package com.metacoding.springv2._core.handler.ex;
public class Exception404 extends RuntimeException {
public Exception404(String message) {
super(message);
}
}⑤ Exception500.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\handler\ex\Exception500.java
package com.metacoding.springv2._core.handler.ex;
public class Exception500 extends RuntimeException {
public Exception500(String message) {
super(message);
}
}⑥ GlobalExceptionHandler.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\handler\GlobalExceptionHandler.java
package com.metacoding.springv2._core.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import com.metacoding.springv2._core.handler.ex.*;
import com.metacoding.springv2._core.util.Resp;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception400.class)
public ResponseEntity<?> exApi400(Exception400 e) {
log.warn("[WARN] 사용자 입력 유효성 실패: " + e.getMessage());
return Resp.fail(HttpStatus.BAD_REQUEST, e.getMessage());
}
@ExceptionHandler(Exception401.class)
public ResponseEntity<?> exApi401(Exception401 e) {
log.warn("[WARN] 사용자 인증 실패: " + e.getMessage());
return Resp.fail(HttpStatus.UNAUTHORIZED, e.getMessage());
}
@ExceptionHandler(Exception403.class)
public ResponseEntity<?> exApi403(Exception403 e) {
log.warn("[WARN] 사용자 권한 실패: " + e.getMessage());
return Resp.fail(HttpStatus.FORBIDDEN, e.getMessage());
}
@ExceptionHandler(Exception404.class)
public ResponseEntity<?> exApi404(Exception404 e) {
log.warn("[WARN] 사용자 자원 찾기 실패: " + e.getMessage());
return Resp.fail(HttpStatus.NOT_FOUND, e.getMessage());
}
@ExceptionHandler(Exception500.class)
public ResponseEntity<?> exApi500(Exception500 e) {
log.warn("[ERROR] 예상 가능한 서버 오류: " + e.getMessage());
return Resp.fail(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
// 해당 오류가 발생하면 직접 처리 혹은 Exception500으로 관리하는 것이 좋다.
@ExceptionHandler(Exception.class)
public ResponseEntity<?> exUnKnown(Exception e) {
log.error("[SYSTEM] 예상 불가능한 서버 오류: " + e.getMessage());
e.printStackTrace();
return Resp.fail(HttpStatus.INTERNAL_SERVER_ERROR, "관리자에게 문의하세요");
}
}⑦ GlobalValidationHandler.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\handler\GlobalValidationHandler.java
package com.metacoding.springv2._core.handler;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.validation.*;
import com.metacoding.springv2._core.handler.ex.Exception400;
import java.util.List;
@Aspect // 관점 관리
@Component
public class GlobalValidationHandler {
@Before("@annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping)")
public void badRequestAdvice(JoinPoint jp) { // jp는 실행될 실제 메서드의 모든 것을 투영하고 있다.
Object[] args = jp.getArgs(); // 메서드의 매개변수들
for (Object arg : args) { // 매개변수 개수만큼 반복 (어노테이션은 제외)
if (arg instanceof Errors) {
Errors errors = (Errors) arg;
// 에러가 존재한다면!!
if (errors.hasErrors()) {
List<FieldError> fErrors = errors.getFieldErrors();
for (FieldError fieldError : fErrors) {
throw new Exception400(fieldError.getField() + ":" + fieldError.getDefaultMessage());
}
}
}
}
}
}2) util 폴더
① JwtProvider.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\util\JwtProvider.java
package com.metacoding.springv2._core.util;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import com.metacoding.springv2.user.User;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Component
public class JwtProvider {
// Bearer JWT -> JWT만 추출하기
public String resolveToken(HttpServletRequest request) {
return null;
}
// 토큰을 검증하고 Authentication 반환
public Authentication getAuthentication(String token) {
return null;
}
// 토큰이 유효한지 단순 체크
public boolean validateToken(String token) {
return false;
}
}② JwtUtil.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\util\JwtUtil.java
package com.metacoding.springv2._core.util;
import java.util.Date;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.metacoding.springv2.user.User;
public class JwtUtil {
public static final String HEADER = "Authorization"; // HTTP 헤더 이름
public static final String TOKEN_PREFIX = "Bearer "; // 토큰 접두사
public static final String SECRET = "메타코딩시크릿키"; // 토큰 서명에 사용될 비밀 키 (강력하게 변경 필요!)
public static final Long EXPIRATION_TIME = 1000L * 60 * 60 * 24 * 7; // 토큰 유효기간 7일
// JWT 토큰 생성
public static String create(User user) {
return null;
}
// JWT 토큰 검증 및 디코딩
public static User verify(String jwt) {
return null;
}
}③ Resp.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\util\Resp.java
package com.metacoding.springv2._core.util;
import lombok.Data;
import org.springframework.http.*;
@Data
public class Resp<T> {
private Integer status;
private String msg;
private T body;
public Resp(Integer status, String msg, T body) {
this.status = status;
this.msg = msg;
this.body = body;
}
public static <B> ResponseEntity<Resp<B>> ok(B body) {
Resp<B> resp = new Resp<>(200, "성공", body);
return new ResponseEntity<>(resp, HttpStatus.OK); // body, header를 응답할 수 있는 클래스
}
public static ResponseEntity<?> fail(HttpStatus status, String msg) {
Resp<?> resp = new Resp<>(status.value(), msg, null);
return new ResponseEntity<>(resp, status);
}
}④ RespFilter.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\util\RespFilter.java
package com.metacoding.springv2._core.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
@Slf4j
public class RespFilter {
private static ObjectMapper om = new ObjectMapper();
public static void fail(HttpServletResponse response, int status, String msg) throws IOException {
response.setStatus(status);
response.setContentType("application/json;charset=utf-8");
Resp<?> resp = new Resp<>(status, msg, null);
String responseBody = null;
try {
responseBody = om.writeValueAsString(resp);
} catch (JsonProcessingException e) {
log.error("JSON 변환 실패", e);
responseBody = """
{"status":500, "msg":"서버 내부 오류", "body":null}
""";
;
}
PrintWriter out = response.getWriter();
out.println(responseBody);
out.flush();
}
}3) auth 폴더
① AuthController.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\auth\AuthController.java
package com.metacoding.springv2.auth;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
@GetMapping("/health")
public String healthCheck() {
return "health ok";
}
}② AuthRequest.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\auth\AuthRequest.java
package com.metacoding.springv2.auth;
import com.metacoding.springv2.user.User;
import jakarta.validation.constraints.*;
public class AuthRequest {
public record JoinDTO(
@Size(min = 4, max = 20, message = "유저네임은 4자 이상 20자 이하로 입력해주세요")
@NotEmpty(message = "유저네임을 입력해주세요")
String username,
@NotBlank(message = "비밀번호를 입력해주세요")
@Size(min = 4, max = 12, message = "비밀번호는 4~12자여야 합니다")
String password,
@Email(message = "이메일 형식이 올바르지 않습니다")
String email) {
public User toEntity(String encPassword) {
return User.builder()
.username(username)
.password(encPassword)
.email(email)
.roles("USER")
.build();
}
}
public record LoginDTO(
@NotEmpty(message = "유저네임을 입력해주세요") String username,
@NotBlank(message = "비밀번호를 입력해주세요") String password) {
}
}③ AuthResponse.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\auth\AuthResponse.java
package com.metacoding.springv2.auth;
import com.metacoding.springv2.user.User;
public class AuthResponse {
public record DTO(Integer id, String username, String email, String roles) {
public DTO(User user) {
this(user.getId(), user.getUsername(), user.getEmail(), user.getRoles());
}
}
}4) board 폴더
① Board.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\board\Board.java
package com.metacoding.springv2.board;
import java.sql.Timestamp;
import java.util.*;
import org.hibernate.annotations.CreationTimestamp;
import com.metacoding.springv2.reply.Reply;
import com.metacoding.springv2.user.User;
import jakarta.persistence.*;
import lombok.*;
@NoArgsConstructor
@Getter
@Entity
@Table(name = "board_tb")
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 30, nullable = false)
private String title;
@Column(length = 300, nullable = false)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@CreationTimestamp
private Timestamp createdAt;
@OneToMany(mappedBy = "board", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private List<Reply> replies = new ArrayList<>();
public void update(String title, String content) {
this.title = title;
this.content = content;
}
@Builder
public Board(Integer id, String title, String content, User user, Timestamp createdAt) {
this.id = id;
this.title = title;
this.content = content;
this.user = user;
this.createdAt = createdAt;
}
}5) reply 폴더
① Reply.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\reply\Reply.java
package com.metacoding.springv2.reply;
import java.sql.Timestamp;
import org.hibernate.annotations.CreationTimestamp;
import com.metacoding.springv2.board.Board;
import com.metacoding.springv2.user.User;
import jakarta.persistence.*;
import lombok.*;
@NoArgsConstructor
@Getter
@Entity
@Table(name = "reply_tb")
public class Reply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 100, nullable = false)
private String comment;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
private Board board;
@CreationTimestamp
private Timestamp createdAt;
@Builder
public Reply(Integer id, String comment, User user, Board board, Timestamp createdAt) {
this.id = id;
this.comment = comment;
this.user = user;
this.board = board;
this.createdAt = createdAt;
}
}6) user 폴더
① User.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\user\User.java
package com.metacoding.springv2.user;
import java.sql.Timestamp;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Getter
@Entity
@Table(name = "user_tb")
public class User {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Integer id;
@Column(unique = true, length = 20, nullable = false)
private String username;
@Column(length = 60, nullable = false)
private String password;
@Column(length = 30, nullable = false)
private String email;
private String roles; // 디폴트값은 USER
@CreationTimestamp
private Timestamp createdAt;
@Builder
public User(Integer id, String username, String password, String email, String roles, Timestamp createdAt) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.roles = roles;
this.createdAt = createdAt;
}
public void update(String email, String password) {
this.email = email;
this.password = password;
}
}7) db 폴더
① data.sql
C:\workspace\spring_lab\spring-rest-start\src\main\resources\db\data.sql
insert into user_tb (username, password, email, roles,created_at) values ('ssar', '$2y$04$0yDwb5VSijD7z8Wj3lFlwu50bcZRkUwqZQWekol9g.h1eCEto02VK', 'ssar@metacoding.com', 'USER',now());
insert into user_tb (username, password, email, roles,created_at) values ('cos', '$2y$04$0yDwb5VSijD7z8Wj3lFlwu50bcZRkUwqZQWekol9g.h1eCEto02VK', 'cos@metacoding.com', 'USER,ADMIN',now());
insert into board_tb (title, content, user_id,created_at) values ('title1', 'content1', 1,now());
insert into board_tb (title, content, user_id,created_at) values ('title2', 'content2', 1,now());
insert into board_tb (title, content, user_id,created_at) values ('title3', 'content3', 1,now());
insert into board_tb (title, content, user_id,created_at) values ('title4', 'content4', 2,now());
insert into board_tb (title, content, user_id,created_at) values ('title5', 'content5', 2,now());
insert into reply_tb (comment, board_id, user_id,created_at) values ('comment1', 4, 2,now());
insert into reply_tb (comment, board_id, user_id,created_at) values ('comment2', 4, 2,now());
insert into reply_tb (comment, board_id, user_id,created_at) values ('comment3', 4, 2,now());
insert into reply_tb (comment, board_id, user_id,created_at) values ('comment4', 5, 1,now());
insert into reply_tb (comment, board_id, user_id,created_at) values ('comment5', 5, 1,now());8) properties 파일
① application-dev.properties
C:\workspace\spring_lab\spring-rest-start\src\main\resources\application-dev.properties
# ===== Server =====
server.port=8080
spring.servlet.encoding.charset=UTF-8
spring.servlet.encoding.enabled=true
spring.servlet.encoding.force=true
# ===== H2 Datasource =====
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
# ===== JPA / Hibernate =====
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.open-in-view=false
# ===== SQL Init (data.sql) =====
spring.sql.init.data-locations=classpath:db/data.sql
spring.jpa.defer-datasource-initialization=true
# ===== Logging Level =====
logging.level.com.metacoding.springv2=DEBUG
# ===== CORS Allow =====
app.cors.allowed-origins=*
spring.output.ansi.enabled=always② application-prod.properties
C:\workspace\spring_lab\spring-rest-start\src\main\resources\application-prod.properties
# ===== Server =====
server.port=8080
spring.servlet.encoding.charset=UTF-8
spring.servlet.encoding.enabled=true
spring.servlet.encoding.force=true
# ===== MySQL Datasource =====
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://${RDS_HOSTNAME}:${RDS_PORT}/${RDS_DB_NAME}
spring.datasource.username=${RDS_USERNAME}
spring.datasource.password=${RDS_PASSWORD}
# ===== H2 console (사용 안 함) =====
spring.h2.console.enabled=false
# ===== JPA / Hibernate =====
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
# ===== Logging Level =====
logging.level.com.metacoding.springv2=INFO
# ===== CORS Allow =====
app.cors.allowed-origins=http://${FRONTEND_HOSTNAME}:${FRONTEND_PORT}③ application.properties
C:\workspace\spring_lab\spring-rest-start\src\main\resources\application.properties
spring.profiles.active=dev
2. 실습 코드
1) config 폴더
① SecurityConfig.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\config\SecurityConfig.java
package com.metacoding.springv2._core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.metacoding.springv2._core.filter.JwtAuthorizationFilter;
@Configuration
public class SecurityConfig {
// 시큐리티 필터 등록
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 인증/권한 주소 커스터마이징
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
// 폼 로그인 비활성화 ( POST : x-www-form urlencoded : username, password )
http.formLogin(f -> f.disable());
// 베이직 인증 비활성화 (request 할 때마다 username, password를 요구)
http.httpBasic(b -> b.disable());
// 인증 필터를 변경 (before/after 상관 없음!)
http.addFilterBefore(new JwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}2) filter 폴더
① JwtAuthorizationFilter.java
C:\workspace\spring_lab\spring-rest-start\src\main\java\com\metacoding\springv2\_core\filter\JwtAuthorizationFilter.java
package com.metacoding.springv2._core.filter;
import java.io.IOException;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// 인가 필터
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
System.out.println("~~~~~~~~~~~~~~~~~~JwtAuthorizationFilter~~~~~~~~~~~~~");
// localhost:8080/good?username=ssar&password=1234
String username = request.getParameter("username");
String password = request.getParameter("password");
if (username.equals("ssar") && password.equals("1234")) {
filterChain.doFilter(request, response);
} else {
response.getWriter().println("get out");
}
}
}3. 코드 이해하기
1️⃣ SecurityConfig 역할
SecurityConfig는 말 그대로 보안 정책 + 필터 체인 설정 담당1) 핵심 코드
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);- 해당 설정의 의미
경로 | 의미 |
/api/** | 인증된 사용자만 접근 가능 |
나머지 요청 | 모두 허용 |
⬇️
/api/users → 인증 필요 /home → 그냥 접근 가능
2) formLogin / httpBasic 비활성화
http.formLogin(f -> f.disable()); http.httpBasic(b -> b.disable());
➡️ 기본 로그인 방식 제거
- formLogin → username/password 폼 로그인 제거
- httpBasic → 요청마다 인증창 뜨는 방식 제거
➡️ 우리가 직접 필터로 인증 처리하겠다는 의미
3) 커스텀 필터 등록 (가장 중요)
http.addFilterBefore(
new JwtAuthorizationFilter(),
UsernamePasswordAuthenticationFilter.class
);- 의미
Spring Security 기본 인증 필터보다 먼저 JwtAuthorizationFilter 실행
⬇️
요청 → JwtAuthorizationFilter → 기본 시큐리티 필터들2️⃣ JwtAuthorizationFilter 역할
인가(Authorization) 필터처럼 동작하도록 만든 커스텀 필터
public class JwtAuthorizationFilter extends OncePerRequestFilter1) OncePerRequestFilter 의미
요청당 딱 한 번만 실행되는 필터 (중복 실행 방지)
2) 실제 수행 로직
String username = request.getParameter("username");
String password = request.getParameter("password");➡️ 요청 파라미터 추출
localhost:8080/api/test?username=ssar&password=12343) 인증 체크
if (username.equals("ssar") && password.equals("1234"))- 조건 만족하면
filterChain.doFilter(request, response);
➡️ 다음 필터로 넘김 (통과)
- 조건 실패하면
response.getWriter().println("get out");➡️ 요청 종료 (체인 중단)
3️⃣ 전체 동작 순서 (가장 중요한 부분)
브라우저 요청 발생
GET /api/test?username=ssar&password=12341) 요청 진입
클라이언트 요청
↓
Spring Security Filter Chain 시작2) JwtAuthorizationFilter 실행
왜 먼저 실행?
👉
addFilterBefore() 때문JwtAuthorizationFilter
콘솔:
~~~~~~~~~~~~~~~~~~JwtAuthorizationFilter~~~~~~~~~~~~~3) username / password 검사
✅ 성공 케이스
username=ssar
password=1234→ doFilter 호출
→ 다음 필터로 이동
❌ 실패 케이스
조건 불만족
→ "get out" 출력
→ 필터 체인 종료
→ 컨트롤러까지 못 감
4) Security 인증 체크
이제 Spring Security 정책 적용:
.requestMatchers("/api/**").authenticated()문제 발생 가능 포인트 👇
⚠️ 중요한 개념적 문제점 (실습 코드의 핵심 이해 포인트)
1) 상황
- 현재 필터
✔ 통과 / 차단만 함
❌ SecurityContext에 인증 객체 저장 안 함
⬇️
- Spring Security 입장
"이 사용자가 인증된 사용자라는 정보가 없음"그래서 실제로는 아래와 같은 일 벌어짐
2) 문제
ssar / 1234 입력해도 필터는 통과시키지만 Spring Security는 인증 안 된 사용자로 판단
→
/api/** 접근 거부 가능3) 문제가 생기는 이유
Spring Security가 인증 판단하는 기준
SecurityContextHolder.getContext().getAuthentication()
여기에 인증 객체가 있어야 함.
현재 코드에는 이 작업이 없음.
4️⃣ 실습 코드의 진짜 목적
- 필터 체인 구조 이해
- 필터 순서 이해
- doFilter() 역할 이해
- 체인 중단 메커니즘 이해
5️⃣ 동작 흐름 한 줄 요약
요청 발생
↓
JwtAuthorizationFilter 실행
↓
username/password 검사
↓
성공 → 다음 필터
실패 → 응답 종료
↓
Spring Security 정책 검사
↓
컨트롤러 진입✅ 핵심 포인트 정리
1) SecurityConfig
- 보안 정책 정의
- 필터 체인 구성
- 커스텀 필터 위치 지정
2) JwtAuthorizationFilter
- 요청 가로채기
- 인증 조건 검사
- 통과 / 차단 결정
3) filterChain.doFilter()
의미 “내 역할 끝났으니 다음 필터로 넘긴다”
Share article