Developer/Backend
Java 19 + Spring Boot 3.1.2 JWT 인증 시스템 구축 가이드
웰크
2025. 4. 25. 20:44
이 글은 Java 19, Spring Boot 3.1.2, Spring Security 6, Gradle 빌드 시스템 환경에서 JWT(JSON Web Token)를 기반으로 인증 시스템을 구축하는 과정을 정리한 중급 개발자용 실전 가이드입니다.
✅ JWT의 장점과 단점
🔷 장점
- 서버가 상태를 저장하지 않아 확장성과 분산 아키텍처에 유리함
- 사용자 정보와 권한을 클라이언트가 포함하여 요청할 수 있음
- 인증 서버와 리소스 서버의 분리 용이
🔶 단점
- 토큰 무효화 어려움 (서버 세션 없음)
- 페이로드 인코딩만 되어 있어 민감 정보 포함 불가
- 탈취 시 위험 → HTTPS 및 토큰 저장 방식에 주의 필요
⚙️ JWT 사용 시 고려사항
- HMAC 또는 RSA 서명 알고리즘 사용 (HS256, RS256 등)
- Access Token은 짧게, Refresh Token은 길게 설정
- HTTPS 전송 필수 / 클라이언트는 HTTP-Only 쿠키 사용 권장
- 로그아웃 또는 재발급 시 토큰 무효화 전략 필요 (블랙리스트 등)
🛠 Gradle 환경 설정 및 의존성 추가
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")
}
🔐 JWT 인증 구성하기 (JwtUtil + 필터 + SecurityConfig)
✅ JwtUtil.java
@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;
}
}
}
✅ JwtAuthenticationFilter.java
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 관련 자주 묻는 질문 Top 5
1. JWT는 세션 기반 인증과 어떻게 다른가요?
세션 기반 인증은 서버가 사용자 세션 정보를 저장하고 관리하는 방식입니다. 반면, JWT는 클라이언트가 토큰을 보유하고 서버는 이를 검증만 하는 무상태(stateless) 인증 방식입니다.
- 세션: 상태 유지 필요, 서버 메모리 또는 DB 사용
- JWT: 확장성 우수, 마이크로서비스/모바일에 적합
2. JWT를 어디에 저장하는 것이 가장 안전한가요?
- LocalStorage: XSS에 취약
- SessionStorage: 탭 닫히면 삭제되지만 여전히 XSS 취약
- HTTP-Only 쿠키: JS 접근 불가, 가장 안전
✅ 권장: Secure, SameSite 설정이 된 HTTP-Only 쿠키 사용
3. JWT 유효 기간은 어떻게 설정해야 하나요?
- Access Token: 15분~1시간 정도로 짧게 설정
- Refresh Token: 1주~1개월 정도로 설정
💡 Refresh Token을 사용해 Access Token 재발급 로직 구현 필수
4. 보안을 위해 어떤 점을 고려해야 하나요?
- 민감 정보는 페이로드에 포함하지 않기
- 강력한 서명 알고리즘 사용 (예: RS256)
- HTTPS 필수
- jti 클레임을 활용해 토큰 재사용 방지 및 블랙리스트 관리
5. Refresh Token은 어떻게 구현하나요?
- 로그인 시 Access + Refresh Token 동시 발급
- Refresh Token은 DB나 Redis 등에 저장
- Access Token 만료 시 Refresh Token으로 재발급
- 로그아웃 시 Refresh Token 삭제 및 무효화 처리
💡 실전 주제 심화 예제
✅ Redis 기반 Refresh 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);
}
}
✅ OAuth2 통합 방식
@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);
}
⚠️ JWT 잘못 사용하는 예시 (주의)
❌ 예: 비밀 키를 하드코딩하거나 공개 저장소에 커밋
private String secretKey = "123456"; // ❌ 보안 위협, 환경변수 또는 설정파일 사용해야 함
❌ 예: 민감 정보 페이로드에 저장
{
"sub": "user123",
"password": "plaintext-pass" // ❌ 절대 저장 금지
}
✅ 해결: application.yml
또는 환경변수로 비밀 키 관리
jwt:
secret: ${JWT_SECRET}
📌 요약 정리
- JWT는 무상태 인증 구조로, 확장성과 분산 환경에 적합합니다.
- Spring Security와 연동 시 필터 기반 인증 및 사용자 주입이 가능합니다.
- Redis를 활용한 Refresh Token 저장, OAuth2 통합, 보안 실수 방지 등 다양한 실전 예제를 함께 다룹니다. 강화하려는 중급 개발자를 위한 실전 가이드입니다. 티스토리 등 블로그 플랫폼에 쉽게 게시할 수 있도록 마크다운으로 구성되어 있습니다.
반응형