이 글은 Java 19, Spring Boot 3.1.2, Spring Security 6, Gradle 빌드 시스템 환경에서 JWT(JSON Web Token)를 기반으로 인증 시스템을 구축하는 과정을 정리한 중급 개발자용 실전 가이드입니다.
build.gradle.kts:
dependencies {
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
}
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
private final long ACCESS_EXPIRATION = 1000 * 60 * 15;
private final long REFRESH_EXPIRATION = 1000 * 60 * 60 * 24 * 7;
public String generateAccessToken(String username) {
return generateToken(username, ACCESS_EXPIRATION);
}
public String generateRefreshToken(String username) {
return generateToken(username, REFRESH_EXPIRATION);
}
private String generateToken(String username, long expiration) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser().setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody().getSubject();
}
public boolean isTokenValid(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
String username = jwtUtil.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.isTokenValid(token)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
}
filterChain.doFilter(request, response);
}
}
@GetMapping("/api/user/me")
public ResponseEntity<?> getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok(userDetails.getUsername());
}
@GetMapping("/api/user/context")
public ResponseEntity<?> getContextUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok(authentication.getName());
}
세션 기반 인증은 서버가 사용자 세션 정보를 저장하고 관리하는 방식입니다. 반면, JWT는 클라이언트가 토큰을 보유하고 서버는 이를 검증만 하는 무상태(stateless) 인증 방식입니다.
✅ 권장: Secure, SameSite 설정이 된 HTTP-Only 쿠키 사용
💡 Refresh Token을 사용해 Access Token 재발급 로직 구현 필수
@Service
public class RefreshTokenService {
private final RedisTemplate<String, String> redisTemplate;
public void storeToken(String username, String refreshToken) {
redisTemplate.opsForValue().set(username, refreshToken, 7, TimeUnit.DAYS);
}
public boolean isTokenValid(String username, String token) {
String stored = redisTemplate.opsForValue().get(username);
return token.equals(stored);
}
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2User user = (OAuth2User) authentication.getPrincipal();
String token = jwtUtil.generateAccessToken(user.getAttribute("email"));
response.sendRedirect("https://your-frontend.com/oauth?token=" + token);
}
private String secretKey = "123456"; // ❌ 보안 위협, 환경변수 또는 설정파일 사용해야 함
{
"sub": "user123",
"password": "plaintext-pass" // ❌ 절대 저장 금지
}
application.yml
또는 환경변수로 비밀 키 관리jwt:
secret: ${JWT_SECRET}
JSON을 대체하는 데이터 포맷: LinkedIn, Uber, Slack, Auth0 사례 비교 분석 (1) | 2025.04.26 |
---|---|
이해하기 쉽게 정리하는 클린 아키텍처 (0) | 2022.08.02 |
코드 구성하기 (0) | 2022.06.13 |
의존성 역전하기 (0) | 2022.06.13 |
계층형 아키텍처의 문제는 무엇일까? (0) | 2022.06.13 |