*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성
- Windows11(윈도우 11) 환경
- 자바 JDK 17 버전 설치 https://yungenie.tistory.com/11
[Java] 차근차근 Java 설치하기 (JDK17, Window 11)
자바 개발 도구 설치 방법에 대해서 알아보겠습니다. Java17은 LTS(Long Term Support : 장기 지원) 릴리즈로 1년 후까지 기술 지원 및 버그를 개선한 서비스를 제공받을 수 있습니다. 업데이트 버전을 꾸
yungenie.tistory.com
- 스프링 부트 4.31.0 사용 - STS(Spring Tool Suite) 설치(Spring Tools for Eclipse - https://spring.io/tools)
=> https://priming.tistory.com/147 참고
[Windows] Spring Tool Suite 4(STS 4) 다운로드 및 설치
STS란?Spring Tool Suite(STS)는 스프링 프로젝트를 생성하고, 개발할 수 있게 해주는 도구입니다. STS 설치 과정에 대해 설명드리겠습니다. 설치 파일 다운로드STS 공식 사이트에서 설치 파일을 다운로
priming.tistory.com

- MySQL Community Server 8.0.42 설치 https://dev.mysql.com/downloads/mysql/
MySQL :: Download MySQL Community Server
Select Version: 9.3.0 Innovation 8.4.5 LTS 8.0.42 Select Operating System: Select Operating System… Microsoft Windows Ubuntu Linux Debian Linux SUSE Linux Enterprise Server Red Hat Enterprise Linux / Oracle Linux Fedora Linux - Generic Oracle Solaris mac
dev.mysql.com

- Gradle
**STS에서 Gradle 프로젝트 생성한 과정



*** 함께 보면 좋은 글
스프링 부트 핵심 가이드(장정우 지음) - 스프링 부트 개요
1. 스프링 프레임워크자바(Java) 기반 애플리케이션 프레임워크로, 엔터프라이즈급(기업 환경 대상 개발) 애플리케이션을 위한 다양한 기능 제공-> 오픈소스 경량급 애플리케이션 프레임워크로
keep-programming-study.tistory.com
스프링 부트 핵심 가이드(장정우 지음) - 개발에 앞서 알면 좋은 기초 지식
1. 서버 간 통신마이크로서비스 아키텍처에서 한 서버가 다른 서버에 통신을 요청하는 것을 의미-> 한 대는 서버/다른 한 대는 클라이언트가 됨 가장 많이 사용되는 방식은 HTTP/HTTPS 방식(TCP/IP, SOA
keep-programming-study.tistory.com
스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security) 개요
*책 내용과 다르게, 다음과 같은 환경에서 프로젝트 생성 Windows11(윈도우 11) 환경자바 JDK 17 버전 설치 https://yungenie.tistory.com/11 [Java] 차근차근 Java 설치하기 (JDK17, Window 11)자바 개발 도구 설치 방법
keep-programming-study.tistory.com
1. JWT(Json Web Token) 개요
- 당사자 간 정보를 JSON 형태로 안전하게 전송하기 위한 토큰
- URL로 이용 가능한 문자열로만 구성되어 있어, HTTP 구성요소 어디든 위치 가능
- 디지털 서명이 적용되어 있어 신뢰성 확보
- 주로 서버와의 통신에서 권한 인가를 위해 사용
1) JWT의 구조
- 점(.)으로 구분된 세 부분으로 구성
- 형식: 헤더(Header).내용(Payload).서명(Signature)
(1) 헤더(Header)
- 검증과 관련 내용을 담고 있음
- alg, typ 속성
- alg: 해싱 알고리즘(SHA256 또는 RSA 사용)을 지정하여 토큰 검증시 사용되는 서명 부분에서 사용
-> 어떤 알고리즘을 사용해서 서명을 만들고 검증할지 지정하는 값 - typ: 토큰 타입 지정
- alg: 해싱 알고리즘(SHA256 또는 RSA 사용)을 지정하여 토큰 검증시 사용되는 서명 부분에서 사용
| 해싱 알고리즘 | 방식 | 키 구조 | JWT 사용 예 |
| SHA-256 (HMAC) | 해시 기반 | 대칭키 (서명·검증에 같은 키 사용) | HS256 |
| RSA | 공개키 암호화 | 비대칭키 (서명은 비밀키, 검증은 공개키) | RS256 |
{
"alg": "HS256",
"typ": "JWT"
}
- 완성된 헤더는 Base64Url 형식으로 인코딩되어 사용
-> JWT 헤더를 JSON으로 만든 뒤, Base64Url 방식으로 변환해서 토큰에 포함
(2) 내용(Payload)
- 토큰에 담는 정보를 포함
- 클레임(Claim) 속성을 세 가지로 분류
- 등록된 클레임(Registered Claims)
- iss: JWT 발급자(Issuer) 주체로, 값은 문자열이나 URI를 포함하는 대소문자 구분 문자열
- sub: JWT의 제목(Subject)
- aud: JWT의 수신인(Audience)로, 각 주체(iss)가 이 값으로 자신을 식별하지 않으면 JWT가 거부됨
- exp: JWT의 만료시간(Expiration)으로, 시간은 NumericDate 형식[1970년 1월 1일 00:00:00 UTC(유닉스 에포크, Unix Epoch)부터 경과한 초를 정수로 나타낸 값] 으로 지정
- nbf: 'Not Before'를 의미
- iat: JWT가 발급된 시간(Issued at)
- jti: JWT의 식별자(JWT ID)로, 중복 처리 방지를 위해 사용
- 공개 클레임(Public Claims)
- 키 값을 마음대로 정의할 수 있는데, 충돌이 발생하지 않도록 주의 필요
- 비공개 클레임(Private Claims)
- 통신 간 상호 합의된, 등록된/공개된 클레임이 아닌 클레임
- 등록된 클레임(Registered Claims)
{
"sub": "wikibooks payload",
"exp": "1602076408",
"userId": "wikibooks",
"username": "flature"
}
- 완성된 헤더는 Base64Url 형식으로 인코딩되어 사용
-> JWT 헤더를 JSON으로 만든 뒤, Base64Url 방식으로 변환해서 토큰에 포함
(3) 서명(Signature)
- 인코딩된 헤더, 인코딩된 내용, 비밀 키, 헤더의 알고리즘 속성값을 가져와 생성
- 서명은 토큰 값들을 포함하여 암호화하므로, 메시지가 도중에 변경되지 않았는지 확인할 때 사용
- 예시(HMAC SHA256 알고리즘 사용)
HMACSHA256(
base64UrlEncode(Header) + "." +
base64UrlEncode(Payload),
secret
)
2) JWT 디버거 사용하기
- JWT 공식 사이트(https://www.jwt.io/#debugger-io)에서 더욱 쉽게 JWT 생성 가능
-> 웹 브라우저에서 접속하여 다음과 같은 화면을 볼 수 있음 - JWT에 대한 상세 내용은 https://www.jwt.io/introduction#what-is-json-web-token에서 확인 가능
JSON Web Token Introduction - jwt.io
Learn about JSON Web Tokens, what are they, how they work, when and why you should use them.
www.jwt.io

2. 스프링 시큐리티와 JWT 적용
1) 기존 프로젝트(study)에 의존성 추가
- build.gradle의 dependencies에 다음과 같이 추가한 후, 저장 후 우클릭하여 Gradle -> Refresh Gradle Project 클릭
dependencies {
... 생략 ...
// Spring Boot 기본 스타터
implementation 'org.springframework.boot:spring-boot-starter'
// MariaDB JDBC Driver
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// JJWT (Java JWT)
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5') // JSON 처리용
}
2) UserDetails와 UserDetailsService 구현
- UserDetails는 스프링 시큐리티에서 제공하는 개념
- UserDetails에서의 username은 각 사용자를 구분할 수 있는 ID를 의미
- UserDetails의 구현체로 User 엔티티를 생성하게 하여 User 객체를 리턴하게끔 구현
-> Spring Security가 요구하는 사용자 정보 구조와 DB 엔티티를 일치시켜서 변환 과정을 줄이고,
권한/계정 상태를 통합 관리
(1) User 엔티티 생성
package com.example.demo.jpa.data.entity;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(nullable = false, unique = true)
private String uid;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
/** 사용자 권한 목록 반환 (roles → SimpleGrantedAuthority 변환) */
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
/** 로그인 아이디로 사용할 uid 반환 */
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public String getUsername() {
return this.uid;
}
/** 비밀번호 반환 (JSON 응답에는 노출되지 않음) */
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public String getPassword() {
return this.password;
}
/** 계정 만료 여부 (true → 만료 없음) */
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonExpired() {
return true;
}
/** 계정 잠금 여부 (true → 잠금 없음) */
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonLocked() {
return true;
}
/** 자격 증명 만료 여부 (true → 만료 없음) */
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/** 계정 활성화 여부 (true → 항상 활성화) */
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isEnabled() {
return true;
}
}
(2) UserRepository 구현
- JpaRepository 상속
package com.example.demo.jpa.data.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.jpa.data.entity.User;
/**
* User 엔티티용 JPA Repository
*
* - JpaRepository<User, Long> 상속: 기본적인 CRUD 메서드 제공
* - 제네릭 타입:
* User → 엔티티 클래스
* Long → 기본 키 타입
*
* 커스텀 메서드:
* - getByUid(String uid): uid 컬럼을 기준으로 User 엔티티 조회
* (Spring Data JPA의 메서드 이름 규칙에 따라 자동으로 쿼리 생성)
*/
public interface UserRepository extends JpaRepository<User, Long> {
User getByUid(String uid);
}
(3) UserDetailsService, UserDetailsServiceImpl 구현
package com.example.demo.service;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* 사용자 인증을 위한 서비스 인터페이스
*
* - Spring Security에서 사용자 정보를 불러오기 위해 사용
* - UserDetailsService 인터페이스는 반드시 구현체가 필요하며,
* loadUserByUsername() 메서드를 통해 DB 등에서 사용자 정보를 조회
*
* 주요 메서드:
* - loadUserByUsername(String username)
* → username(로그인 아이디)으로 UserDetails 객체를 반환
* → 사용자가 존재하지 않을 경우 UsernameNotFoundException 발생
*/
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
package com.example.demo.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import com.example.demo.jpa.data.repository.UserRepository;
import com.example.demo.service.UserDetailsService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
/** 로깅을 위한 Logger 인스턴스 */
private final Logger LOGGER = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
/** 사용자 정보를 조회하기 위한 JPA Repository */
private final UserRepository userRepository;
/**
* username(uid)으로 사용자 정보를 조회하여 UserDetails 반환
* - 로그 기록: 메서드 수행 시 username 출력
* - userRepository.getByUid(username) 호출로 DB에서 사용자 조회
* - 조회된 User 엔티티는 UserDetails를 구현하므로 그대로 반환 가능
*/
@Override
public UserDetails loadUserByUsername(String username) {
LOGGER.info("[loadUserByUsername] 수행, username: {}", username);
return userRepository.getByUid(username);
}
}
3) JwtTokenProvider 구현
(1) UsernamePasswordAuthenticationToken의 상속 구조

(2) JwtTokenProvider 구현
- JWT 토큰 생성에 필요한 정보를 UserDetails에서 가져와서, JwtTokenProvider에서 JWT 토큰을 생성
- 보안을 위해서는 secretKey 값을 application.properties 파일에서 값을 정의하나,
값을 가져올 수 없을 경우 기본값(아래 코드에서는 secretKey)을 가져옴
package com.example.demo.config.security;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import javax.crypto.spec.SecretKeySpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.example.demo.service.UserDetailsService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
/**
* JWT 토큰을 생성하고 관리하는 Provider 클래스
*
* 주요 역할:
* - secretKey 초기화 (Base64 인코딩)
* - 사용자 정보(uid, roles)를 Claims에 담아 JWT 토큰 생성
* - 토큰에서 subject(uid) 추출 (getUsername)
* - 토큰 기반으로 UserDetails 조회 후 Authentication 객체 생성 (getAuthentication)
* - HTTP 요청 헤더에서 토큰 추출 (resolveToken)
* - 토큰 유효성 검증 (validateToken: 서명 검증 및 만료 시간 확인)
*
* 즉, JWT 인증 흐름의 핵심 기능을 모두 제공하는 클래스이며,
* Spring Security의 필터나 인증 로직에서 직접 사용되어
* 사용자 인증 및 권한 부여 과정을 지원한다.
*
* 사용 라이브러리:
* - io.jsonwebtoken.Jwts: JWT 빌더 및 파서
* - io.jsonwebtoken.Claims: JWT payload에 담을 데이터
* - SignatureAlgorithm.HS256: HMAC-SHA256 알고리즘으로 서명
*/
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
/** 로깅을 위한 Logger 인스턴스 */
private final Logger LOGGER = LoggerFactory.getLogger(JwtTokenProvider.class);
/** 사용자 정보를 조회하기 위한 UserDetailsService (인증 과정에서 활용 가능) */
private final UserDetailsService userDetailsService;
/** application.yml 또는 properties에서 주입받는 secretKey (기본값: "secretKey") */
@Value("${springboot.jwt.secret}")
private String secretKey = "secretKey";
/** 토큰 유효 시간 (1시간 = 1000ms * 60 * 60) */
private final long tokenValidMillisecond = 1000L * 60 * 60;
/** 변환된 Key 객체를 저장할 필드 */
private Key key;
/**
* secretKey 초기화 메서드
* - @PostConstruct: Bean 생성 후 자동 실행
* - secretKey를 Base64 인코딩하여 보안 강화
* - secretKey → Key 객체 변환을 한 번만 수행(보안 강화를 위해 String 대신 Key 객체 사용)
*/
@PostConstruct
protected void init() {
LOGGER.info("[init] JwtTokenProvider 내 secretKey 초기화 시작");
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8),
SignatureAlgorithm.HS256.getJcaName());
LOGGER.info("[init] JwtTokenProvider 내 secretKey 초기화 완료");
}
/**
* JWT 토큰 생성 메서드
*
* @param userUid 사용자 고유 ID (subject)
* @param roles 사용자 권한 목록
* @return 생성된 JWT 토큰 문자열
*
* 동작 흐름:
* 1. Claims 객체 생성 → subject(userUid)와 roles 추가
* 2. 현재 시간(now) 기준으로 발급일(issuedAt)과 만료일(expiration) 설정
* 3. secretKey를 Key 객체로 변환 (SecretKeySpec)
* 4. Jwts.builder()로 토큰 생성 및 HS256 알고리즘으로 서명
* 5. compact() 호출로 최종 JWT 문자열 반환
*/
public String createToken(String userUid, List<String> roles) {
LOGGER.info("[createToken] 토큰 생성 시작");
// JWT payload에 담을 Claims 생성
Claims claims = Jwts.claims().setSubject(userUid);
claims.put("roles", roles);
Date now = new Date();
// JWT 토큰 빌드
String token = Jwts.builder()
.setClaims(claims) // 사용자 정보 포함
.setIssuedAt(now) // 발급 시간
.setExpiration(new Date(now.getTime() + tokenValidMillisecond)) // 만료 시간
.signWith(key, SignatureAlgorithm.HS256) // 서명 (Key 객체 기반)
.compact(); // 최종 문자열 반환
LOGGER.info("[createToken] 토큰 생성 완료");
return token;
}
/**
* JWT 토큰에서 사용자 이름(subject)을 추출하는 메서드
*
* 동작 흐름:
* 1. secretKey를 Key 객체로 변환 (SecretKeySpec 사용)
* - 문자열 기반 키 대신 Key 객체를 사용해야 보안 강화 및 최신 JJWT API 대응 가능
* 2. Jwts.parserBuilder()로 파서 생성
* - setSigningKey(key): 토큰 서명 검증을 위한 키 설정
* - build(): 파서 빌드
* 3. parseClaimsJws(token): 토큰을 파싱하여 Claims(페이로드) 추출
* 4. getBody().getSubject(): Claims에서 subject 값(사용자 UID) 반환
* 5. 로그 기록: 토큰 기반 회원 구별 정보 추출 시작/완료 로그 출력
*
* @param token JWT 토큰 문자열
* @return 토큰에 저장된 subject (userUid)
*/
public String getUsername(String token) {
LOGGER.info("[getUsername] 토큰 기반 회원 구별 정보 추출");
// JWT 파싱 및 subject 추출
String info = Jwts.parserBuilder()
.setSigningKey(key) // ✅ Key 객체 사용
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
LOGGER.info("[getUsername] 토큰 기반 회원 구별 정보 추출 완료, info: {}", info);
return info;
}
/**
* JWT 토큰을 기반으로 Authentication 객체를 생성하는 메서드
*
* 동작 흐름:
* 1. 로그 기록: 토큰 인증 정보 조회 시작
* 2. getUsername(token) 호출 → 토큰에서 subject(uid) 추출
* 3. userDetailsService.loadUserByUsername(uid) 호출 → DB에서 사용자 정보(UserDetails) 조회
* 4. 로그 기록: 조회 완료 및 사용자 이름 출력
* 5. UsernamePasswordAuthenticationToken 생성
* - principal: 조회된 UserDetails 객체
* - credentials: 빈 문자열("") → 비밀번호는 필요하지 않음
* - authorities: UserDetails에 포함된 권한 목록
* 6. 최종적으로 Authentication 객체 반환 → SecurityContext에 저장되어 인증 완료 상태로 사용됨
*
* @param token JWT 토큰 문자열
* @return Spring Security Authentication 객체
*/
public Authentication getAuthentication(String token) {
LOGGER.info("토큰 인증 정보 조회 시작");
// 토큰에서 사용자 uid(subject) 추출 후 DB에서 UserDetails 조회
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUsername(token));
LOGGER.info("토큰 인증 정보 조회 완료, UserDetails UserName: {}", userDetails.getUsername());
// UserDetails 기반으로 Authentication 객체 생성
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
/**
* HTTP 요청 헤더에서 JWT 토큰을 추출하는 메서드
*
* 동작 흐름:
* 1. 로그 기록: 토큰 추출 시작
* 2. HttpServletRequest의 "X-AUTH-TOKEN" 헤더 값 반환
* - 클라이언트가 요청 시 Authorization 헤더 대신 커스텀 헤더("X-AUTH-TOKEN")에 토큰을 담아 전달
* 3. 반환된 문자열이 JWT 토큰으로 사용됨
*
* @param request HttpServletRequest 객체
* @return 요청 헤더에 담긴 JWT 토큰 문자열
*/
public String resolveToken(HttpServletRequest request) {
LOGGER.info("[resolveToken] HTTP 헤더에서 Token 값 추출");
return request.getHeader("X-AUTH-TOKEN");
}
/**
* JWT 토큰의 유효성을 검증하는 메서드
*
* 동작 흐름:
* 1. 로그 기록: 토큰 유효성 체크 시작
* 2. secretKey → Key 객체 변환 (SecretKeySpec 사용)
* - 문자열 기반 키 대신 Key 객체를 사용하여 보안 강화
* 3. Jwts.parserBuilder()로 파서 생성 후 build()
* - setSigningKey(key): 토큰 서명 검증용 키 설정
* - parseClaimsJws(token): 토큰 파싱 및 서명 검증
* 4. Claims에서 만료 시간(expiration) 추출
* - 현재 시간과 비교하여 만료 여부 확인
* - 만료되지 않았다면 true 반환
* 5. 예외 발생 시 (서명 불일치, 토큰 변조, 만료 등) false 반환
*
* @param token JWT 토큰 문자열
* @return 토큰이 유효하면 true, 그렇지 않으면 false
*/
public boolean validateToken(String token) {
LOGGER.info("[validateToken] 토큰 유효 체크 시작");
try {
// 토큰 파싱 및 서명 검증
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
// 만료 시간 확인
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
LOGGER.info("[validateToken] 토큰 유효 체크 예외 발생");
return false;
}
}
}
4) JwtAuthenticationFilter 구현
- JWT 토큰으로 인증한 후 SecurityContextHolder에 추가하는 필터를 설정하는 클래스
- GenericFilterBean은 기존 필터에서 가져올 수 없는 스프링 설정 정보를 가져올 수 있게 확장된 추상 클래스
-> 서블릿은 사용자의 요청을 받으면 서블릿 생성 후 메모리에 저장해둔 다음 동일한 클라이언트의 요청을 받으면 재활용하는 구조이므로, 이를 상속받으면 RequestDispatcher에 의해 다른 서블릿으로 디스패치되면서 필터가 두 번 실행- 이를 해결하기 위해 등장한 것이 OncePerRequestFilter인데,
이는 GenericFilterBean을 상속받지만 매 요청받아 한 번만 실행됨
- 이를 해결하기 위해 등장한 것이 OncePerRequestFilter인데,
(1) OncePerRequestFilter 상속 ver - 이 버전을 사용할 것
package com.example.demo.config.security;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* JWT 인증을 처리하는 커스텀 필터 클래스
*
* OncePerRequestFilter를 상속하여 모든 요청마다 한 번만 실행됨.
*
* 주요 역할:
* 1. HTTP 요청에서 JWT 토큰 추출 (resolveToken)
* 2. 토큰 유효성 검증 (validateToken)
* 3. 토큰이 유효하다면 Authentication 객체 생성 후 SecurityContext에 저장
* - SecurityContextHolder는 현재 요청의 인증 상태를 관리하는 Spring Security의 핵심 컨텍스트
* 4. 필터 체인을 계속 진행하여 이후 요청 처리 로직으로 전달
*
* 즉, 이 필터는 클라이언트 요청이 들어올 때마다 JWT 기반 인증을 수행하고,
* 인증된 사용자 정보를 Spring Security 컨텍스트에 주입하는 역할을 담당
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
// JWT 검증을 건너뛸 경로들
private static final List<String> EXCLUDE_URLS = List.of(
"/swagger-ui",
"/swagger-ui/",
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs",
"/v3/api-docs/",
"/v3/api-docs/**",
"/v3/api-docs.yaml",
"/api/public",
"/sign-api",
"/exception"
);
/**
* 실제 필터 로직 구현 메서드
*
* 동작 흐름:
* 1. 요청(servletRequest)에서 JWT 토큰 추출
* 2. 토큰 값 로그 출력
* 3. 토큰 유효성 검증 시작
* 4. 토큰이 존재하고 유효하다면:
* - JwtTokenProvider.getAuthentication(token) 호출 → Authentication 객체 생성
* - SecurityContextHolder.getContext().setAuthentication(authentication) → 인증 정보 저장
* - 로그 출력: 유효성 체크 완료
* 5. 필터 체인 계속 진행 (filterChain.doFilter)
*
* @param servletRequest 클라이언트 요청 객체
* @param servletResponse 클라이언트 응답 객체
* @param filterChain 필터 체인 객체
*/
@Override
protected void doFilterInternal(HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
FilterChain filterChain) throws ServletException, IOException {
// Swagger, 공개 API는 JWT 검증 건너뛰기
String path = servletRequest.getRequestURI();
if (EXCLUDE_URLS.stream().anyMatch(path::startsWith)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 요청에서 JWT 토큰 추출
String token = jwtTokenProvider.resolveToken((HttpServletRequest) servletRequest);
LOGGER.info("[doFilterInternal] token 값 추출 완료. token: {}", token);
// 토큰 유효성 검증
LOGGER.info("[doFilterInternal] token 값 유효성 체크 시작");
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효하면 Authentication 객체 생성 후 SecurityContext에 저장
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
LOGGER.info("[doFilterInternal] token 값 유효성 체크 완료");
}
// 필터 체인 계속 진행
filterChain.doFilter(servletRequest, servletResponse);
}
}
(2) GenericFilterBean 상속 ver
package com.example.demo.config.security;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
/**
* JWT 인증을 처리하는 커스텀 필터 클래스 (GenericFilterBean 기반)
*
* 주요 역할:
* 1. HTTP 요청에서 JWT 토큰 추출 (resolveToken)
* 2. 토큰 유효성 검증 (validateToken)
* 3. 토큰이 유효하다면 Authentication 객체 생성 후 SecurityContext에 저장
* - SecurityContextHolder는 현재 요청의 인증 상태를 관리하는 Spring Security의 핵심 컨텍스트
* 4. 필터 체인을 계속 진행하여 이후 요청 처리 로직으로 전달
*
* 특징:
* - GenericFilterBean을 상속하여 Spring Security 필터 체인에 등록 가능
* - OncePerRequestFilter와 달리, 요청마다 한 번만 실행된다는 보장은 없지만
* 보통 SecurityConfig에서 적절히 등록하면 동일하게 동작
*
* 즉, 이 필터는 클라이언트 요청이 들어올 때마다 JWT 기반 인증을 수행하고,
* 인증된 사용자 정보를 Spring Security 컨텍스트에 주입하는 역할을 담당한다.
*/
public class JwtAuthenticationFilter2 extends GenericFilterBean {
private final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter2.class);
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter2(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
/**
* 실제 필터 로직 구현 메서드
*
* 동작 흐름:
* 1. 요청(servletRequest)에서 JWT 토큰 추출
* 2. 토큰 값 로그 출력
* 3. 토큰 유효성 검증 시작
* 4. 토큰이 존재하고 유효하다면:
* - JwtTokenProvider.getAuthentication(token) 호출 → Authentication 객체 생성
* - SecurityContextHolder.getContext().setAuthentication(authentication) → 인증 정보 저장
* - 로그 출력: 유효성 체크 완료
* 5. 필터 체인 계속 진행 (filterChain.doFilter)
*
* @param servletRequest 클라이언트 요청 객체
* @param servletResponse 클라이언트 응답 객체
* @param filterChain 필터 체인 객체
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
// 요청에서 JWT 토큰 추출
String token = jwtTokenProvider.resolveToken((HttpServletRequest) servletRequest);
LOGGER.info("[doFilter] token 값 추출 완료. token: {}", token);
// 토큰 유효성 검증
LOGGER.info("[doFilter] token 값 유효성 체크 시작");
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효하면 Authentication 객체 생성 후 SecurityContext에 저장
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
LOGGER.info("[doFilter] token 값 유효성 체크 완료");
}
// 필터 체인 계속 진행
filterChain.doFilter(servletRequest, servletResponse);
}
}
5) SecurityConfig 구현
- WebSecurityConfigureAdapter를 상속받는 Configuration 구현하는 방식은 최신 버전에서 완전히 제거됨
- Spring Security 팀은 구성 클래스 상속 방식 대신, 명시적인 Bean 등록 방식을 권장
-> SecurityFilterChain Bean을 직접 등록
package com.example.demo.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// JWT 필터를 SecurityConfig에 연결하기 위해 꼭 필요한 의존성 주입 코드
private final JwtTokenProvider jwtTokenProvider;
@Autowired
public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
/**
* Spring Security의 핵심 보안 설정을 정의하는 Bean
*
* SecurityFilterChain을 Bean으로 등록해야 최신 버전(Spring Security 5.7+)에서
* 보안 설정이 적용됨. (WebSecurityConfigurerAdapter는 제거됨)
*
* @param http HttpSecurity 객체 (Spring Security가 제공하는 보안 설정 DSL)
* @return SecurityFilterChain (최종 보안 필터 체인)
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF 비활성화 (REST API 서버에서는 보통 CSRF 토큰을 사용하지 않음)
.csrf(csrf -> csrf.disable())
// 요청별 권한 설정
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/public/**",
"/swagger-ui/**", // Swagger UI 리소스 전체 허용
"/swagger-ui.html", // Swagger UI 진입 페이지
"/v3/api-docs/**", // OpenAPI 문서 엔드포인트 허용
// 뒤에 실습할 api 엔드포인트들 허용
"/sign-api/**",
"**exception**"
).permitAll() // 공개 API는 모두 허용
.requestMatchers(HttpMethod.GET, "/product", "/product/**").permitAll() // GET 메서드 외에 나머지는 모두 인증 필요
// 관리자 전용 페이지라면 .authenticated() 대신 .hasRole("ADMIN")
.anyRequest().authenticated() // 그 외 요청은 인증 필요
)
// JWT 인증 필터를 UsernamePasswordAuthenticationFilter 앞에 추가
// → 기본 로그인 인증 필터보다 먼저 JWT 검증을 수행하도록 설정
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class)
// 기본 form 로그인 설정 (커스터마이징 가능)
.formLogin(Customizer.withDefaults());
// 최종적으로 SecurityFilterChain 반환
return http.build();
}
}
***다음 편에서 계속됩니다***
'스프링(Spring), 스프링부트(SpringBoot) > 스프링부트(SpringBoot) 기초' 카테고리의 다른 글
| 스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 3편 [책의 마지막 부분] (1) | 2026.04.25 |
|---|---|
| 스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security)와 JWT 2편 (1) | 2026.04.16 |
| 스프링부트 핵심 가이드(장정우 지음) - 스프링 시큐리티(Spring Security) 개요 (0) | 2026.03.26 |
| 스프링부트 핵심 가이드(장정우 지음) - 서버 간 통신 2: WebClient (0) | 2026.03.18 |
| 스프링부트 핵심 가이드(장정우 지음) - 서버 간 통신 1: RestTemplate (1) | 2026.03.03 |