아래의 링크가 정말로 잘 정리되어 있음 참고.
https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC
🌐 JWT 토큰 인증 이란? (쿠키 vs 세션 vs 토큰)
Cookie / Session / Token 인증 방식 종류 보통 서버가 클라이언트 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. JWT를 배우기 앞서 우선 쿠키와 세션의 통신 방식을 복습해
inpa.tistory.com
아래와 같은 요청사항이 들어옴.
- API 호출 시 Header에 토큰을 포함했으면 좋겠습니다.
- 해당 토큰이 어떤 토큰인지 구분할 수 있었으면 좋겠습니다.
- 토큰 관리(저장, 만료처리 등)는 하지 말아주세요.
위의 요청사항을 보고 JWT로 토큰 선택함
- 초기에는 단순히 UUID를 사용하려고 했으나, UUID만으로는 토큰 발급 주체나 생성 시점 등의 정보를 알 수 없음
- 반면 JWT는 다음과 같은 장점이 있음
- 구분 가능: sub, iat, exp 등의 Claim으로 토큰만 보고도 어떤 주체가 발급했는지 구분 가능
- Stateless: 별도의 저장소 없이 헤더에 담긴 토큰만으로 검증 가능
- 확장성: 추후 API 확장 시에도 현재 구조 그대로 재사용 가능
즉, 토큰 관리가 필요 없는 이번 요구사항에 적합
✅ JWT?
JWT(JSON Web Token)**는 JSON 포맷으로 사용자 인증 정보 등을 안전하게 전송하기 위한 토큰 기반 인증 방식 Stateless 인증: 서버가 세션을 유지할 필요 없이 클라이언트가 토큰을 헤더에 담아 보내면 됨
✅ JWT 구조
Header.Payload.Signature (3부분을 Base64URL로 인코딩 후 , 으로 구분)
📌 Header
{
"alg": "HS256",
"typ": "JWT"
}
- alg: 서명 알고리즘 (HMAC SHA256, RSA 등)
- typ: 타입(JWT)
📌 Payload (Claims)
{
"sub": "my-app",
"iat": 1721632583,
"exp": 1721634383
}
- sub: 주제(일반적으로 사용자명, 앱 이름)
- iat: 토큰 발급 시간
- exp: 만료 시간
📌 Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
- 위변조 방지
📌 장단점
- 장점
- Stateless: 서버에 세션 저장 불필요 → 수평 확장 유리
- 빠른 인증 처리
- 단점
- 만료 전 강제 만료 어려움(Refresh Token이나 블랙리스트 필요)
- Payload가 노출 가능(Base64 인코딩 → 암호화 아님, 민감정보 저장 금지)
📌 만료시간
- 만료시간은 보통 아래와 같이 설정한다고함
- Access Token → 보통 15분~30분 (짧게, 보안 강화)
- Refresh Token → 보통 7일~14일 (길게, 재발급용)
- 단순 API 인증이라면 30분~1시간 정도면 적절
✅ Config (Filter 등록)
package com.example.project.config;
import com.example.project.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class FilterConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public FilterRegistrationBean<JwtAuthenticationFilter> jwtFilter() {
FilterRegistrationBean<JwtAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(jwtAuthenticationFilter);
registrationBean.addUrlPatterns("/api/*"); //인증 필요한 경로만 필터 적용
return registrationBean;
}
}
✅ Filter (토큰 검증)
package com.example.project.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter implements Filter {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String header = httpRequest.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
log.warn("Authorization header is missing");
setUnauthorizedResponse(httpResponse, "Token is missing in the header.");
return;
}
String token = header.substring(7);
if (!jwtTokenProvider.validateToken(token)) {
log.warn("Invalid or expired token");
setUnauthorizedResponse(httpResponse, "Invalid or expired token.");
return;
}
//토큰에서 사용자 정보 추출 후 Controller에 전달
String appName = jwtTokenProvider.getSubject(token);
httpRequest.setAttribute("appName", appName);
chain.doFilter(request, response);
}
private void setUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"error\":\"" + message + "\"}");
}
}
✅ Provider (토큰 생성 & 검증)
package com.example.project.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;
@Slf4j
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long validityInMs;
private SecretKey secretKey;
@PostConstruct
protected void init() {
this.secretKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secret));
log.info("JWT Provider initialized. Expiration(ms): {}", validityInMs);
}
public String generateToken(String subject) {
Date now = new Date();
Date expiry = new Date(now.getTime() + validityInMs);
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("JWT validation failed: {}", e.getMessage());
return false;
}
}
public String getSubject(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}