✅ JWT란?
JWT (JSON Web Token)는 사용자 인증 및 권한 부여를 위해 사용되는 토큰 기반 인증 방식입니다.
로그인 성공 시, 서버는 클라이언트에게 암호화된 토큰을 발급하고, 클라이언트는 이후 요청마다 이 토큰을 전송하여 인증을 진행합니다.
✅ JWT 구조
JWT는 총 3개의 파트로 구성되어 있습니다.
xxxxx.yyyyy.zzzzz
Header.Payload.Signature
1. Header
{
"alg": "HS256",
"typ": "JWT"
}
- 토큰의 타입과 해싱 알고리즘 정보
2. Payload
{
"sub": "userId",
"name": "ㅇㅇㅇ",
"role": "USER",
"exp": 1712345678
}
- 사용자 정보(Claims)를 담는 부분
exp
: 토큰 만료 시간 (Unix timestamp)
3. Signature
- 위의 Header와 Payload를 기반으로 생성한 서명
- 예:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
✅ JWT 인증 흐름
- 사용자가 ID/PW로 로그인 요청
- 서버는 유효한 사용자임을 확인 후 Access Token과 Refresh Token을 발급
- 클라이언트는 이후 요청마다 Access Token을 HTTP 헤더에 담아 요청
Authorization: Bearer <ACCESS_TOKEN>
- 서버는 토큰을 검증하고, 사용자를 인증된 상태로 처리
- Access Token이 만료되면 Refresh Token으로 새로운 Access Token을 발급받음
✅ Access Token vs Refresh Token
항목 | Access Token | Refresh Token |
---|---|---|
용도 | API 요청 인증 | Access Token 갱신용 |
수명 | 짧음 (5~30분) | 김 (1일~2주 이상) |
저장 위치 | 메모리 or localStorage | HttpOnly Cookie 권장 |
보안 위험 | 탈취 시 API 호출 가능 | 탈취 시 새 토큰 발급 가능 |
✅Spring Boot에서 JWT 구성 요소
JwtUtil
: JWT 생성 및 검증 유틸 클래스JwtAuthenticationFilter
: 요청마다 토큰 검증 필터SecurityConfig
: Spring Security 설정 클래스UserDetailsService
: 사용자 정보 로딩AuthenticationManager
: 로그인 처리
✅JWT의 장점
- 서버가 세션 정보를 저장할 필요 없음 (Stateless)
- 마이크로서비스, SPA, 모바일 앱에 적합
- 확장성과 성능 측면에서 유리
✅JWT 사용 시 주의사항
- HTTPS 필수 : 토큰 탈취 방지
- 토큰 만료 및 갱신 정책 명확히 설정
- 가능한 Refresh Token은 HttpOnly Cookie로 관리
- 사용자 권한 정보는 꼭 필요한 최소한만 담을 것
✅ 참고 라이브러리
jjwt
: io.jsonwebtoken 라이브러리 (Java)spring-security
: JWT 필터와 인증 구성에 사용
// Gradle
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
✅ Spring Boot에서 JWT 인증 구현 - 핵심 구성요소
1. JwtUtil.java
package com.example.jwt.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtil {
private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 혹은 Base64 인코딩된 SecretKey 사용
private final long ACCESS_TOKEN_EXP = 1000 * 60 * 30; // 30분
// ✅ Access Token 생성
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXP))
.signWith(key)
.compact();
}
// ✅ 토큰에서 사용자 이름 추출
public String extractUsername(String token) {
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// ✅ 토큰 유효성 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
2. JwtAuthenticationFilter.java
package com.example.jwt.security;
import com.example.jwt.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, CustomUserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String jwt = authHeader.substring(7);
String username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
}
filterChain.doFilter(request, response);
}
}
3. SecurityConfig.java (Spring Security 설정)
package com.example.jwt.config;
import com.example.jwt.security.JwtAuthenticationFilter;
import com.example.jwt.util.JwtUtil;
import com.example.jwt.security.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(JwtUtil jwtUtil, CustomUserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService),
UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4. CustomUserDetailsService.java
package com.example.jwt.security;
import com.example.jwt.model.User;
import com.example.jwt.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository repo) {
this.userRepository = repo;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
return new CustomUserDetails(user);
}
}
✅ 마무리 요약
JwtUtil
: 토큰 생성, 파싱, 검증JwtAuthenticationFilter
: 요청마다 토큰을 확인하여 인증 처리SecurityConfig
: Spring Security에서 JWT 필터 적용 및 세션 비활성화CustomUserDetailsService
: DB에서 사용자 정보 불러오는 서비스
'백엔드 부트캠프 > TIL' 카테고리의 다른 글
[내일배움캠프Spring-37일차] 클린아키텍처 (1) | 2025.04.09 |
---|---|
[내일배움캠프Spring-36일차] Race Condition (경쟁 조건) 정리 (0) | 2025.04.08 |
[내일배움캠프Spring-34일차] JPA 의 Paging / Pageable / DTO 매핑 페이징 (0) | 2025.04.04 |
[내일배움캠프Spring-33일차] CH 3 일정 관리 앱 Develop 完 (0) | 2025.04.03 |
[내일배움캠프Spring-32일차] CH 3 일정 관리 앱 Develop Lv7~Lv8, Refactoring (1) | 2025.04.02 |