Spring Boot 프로젝트에서 JWT를 사용하여 인증과 권한 부여를 구현하는 방법에 대해 설명합니다. 이 글에서는 JWT의 생성, 검증, 쿠키를 통한 JWT 저장 및 처리 방법을 설명하며, io.jsonwebtoken:jjwt 라이브러리를 활용합니다. Gradle을 사용하는 환경을 기준으로 설명합니다.
1. Gradle 의존성 추가
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.9.1'
}
2. JWT 유틸리티 클래스 작성
이제 JWT를 생성하고 검증하는 유틸리티 클래스를 작성해보겠습니다. JwtUtil 클래스는 JWT 생성, 검증, 쿠키 저장, 쿠키에서 토큰 추출, 토큰 무효화 기능을 제공합니다.
// JwtUtil.java
package taxi.share.back.util;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
@Slf4j
@Component
public class JwtUtil {
private final RedisTemplate<String, Object> redisTemplate;
private static final String SECRET_KEY = "taxi_share";
private static final long EXPIRATION_TIME = 600; // 600s
private static final ZoneId KST_ZONE_ID = ZoneId.of("Asia/Seoul");
@Autowired
public JwtUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// JWT 토큰 생성
public String generateToken(String userId) {
ZonedDateTime now = ZonedDateTime.now(KST_ZONE_ID);
ZonedDateTime expirationTime = now.plusMinutes(10);
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(Date.from(now.toInstant()))
.setExpiration(Date.from(expirationTime.toInstant())) // 토큰 만료시간을 설정
.signWith(SignatureAlgorithm.HS512, SECRET_KEY.getBytes())
.compact();
}
// JWT 토큰을 쿠키에 저장
public void addTokenToCookie(String token, HttpServletResponse response) {
Cookie cookie = new Cookie("jwt-token", token);
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setPath("/");
cookie.setMaxAge((int) (EXPIRATION_TIME)); // 쿠키의 만료시간을 설정
response.addCookie(cookie);
}
// JWT 토큰 유효성 검증
public boolean validateToken(String token, HttpServletResponse response) {
try {
Jwts.parser()
.setSigningKey(SECRET_KEY.getBytes())
.parseClaimsJws(token)
.getBody();
return true;
} catch (ExpiredJwtException e) {
String userId = e.getClaims().getSubject();
log.info("Http Cookie Session 만료 >>>> {}", userId);
invalidateCookie(response, "jwt-token");
return false;
}
}
// 쿠키에서 JWT 토큰 추출
public String extractTokenFromCookie(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
// JWT 쿠키 무효화
public void invalidateCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
3. 주요 메서드 분석
3-(1) generateToken: JWT 토큰 생성
public String generateToken(String userId) {
ZonedDateTime now = ZonedDateTime.now(KST_ZONE_ID);
ZonedDateTime expirationTime = now.plusMinutes(10);
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(Date.from(now.toInstant()))
.setExpiration(Date.from(expirationTime.toInstant()))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY.getBytes())
.compact();
}
generateToken 메서드는 주어진 userId를 기반으로 JWT 토큰을 생성합니다. 이 토큰에는 다음과 같은 정보가 포함됩니다:
- 발급 시간 (setIssuedAt): 토큰이 발급된 시간을 나타냅니다.
- 만료 시간 (setExpiration): 토큰의 유효 기간을 설정합니다. 이 예제에서는 현재 시간으로부터 10분 후로 설정됩니다.
- 서명 (signWith): HS512 알고리즘을 사용하여 서명됩니다. 이는 토큰의 무결성을 보장하기 위해 사용됩니다.
3-(2) addTokenToCookie: JWT 토큰을 쿠키에 저장
public void addTokenToCookie(String token, HttpServletResponse response) {
Cookie cookie = new Cookie("jwt-token", token);
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setPath("/");
cookie.setMaxAge((int) (EXPIRATION_TIME));
response.addCookie(cookie);
}
addTokenToCookie 메서드는 생성된 JWT 토큰을 쿠키에 저장합니다. 쿠키는 다음과 같이 설정됩니다:
- HttpOnly: 쿠키는 클라이언트 측에서 JavaScript로 접근할 수 없습니다. 이는 보안을 강화하는 데 도움이 됩니다.
- Secure: false로 설정되어 있지만, 실제 배포 환경에서는 반드시 true로 설정하여 HTTPS를 통해서만 쿠키를 전송하도록 해야 합니다.
- Path: 쿠키가 유효한 경로를 설정합니다.
- MaxAge: 쿠키의 만료 시간을 설정합니다. 여기서는 10분으로 설정되어 있습니다.
3-(3) validateToken: JWT 토큰 유효성 검증
public boolean validateToken(String token, HttpServletResponse response) {
try {
Jwts.parser()
.setSigningKey(SECRET_KEY.getBytes())
.parseClaimsJws(token)
.getBody();
return true;
} catch (ExpiredJwtException e) {
String userId = e.getClaims().getSubject();
log.info("Http Cookie Session 만료 >>>> {}", userId);
invalidateCookie(response, "jwt-token");
return false;
}
}
validateToken 메서드는 전달된 JWT 토큰의 유효성을 검증합니다. 토큰이 만료되었거나 잘못된 경우 ExpiredJwtException이 발생합니다. 이 경우 토큰은 무효로 처리되며, 쿠키가 무효화됩니다.
3-(4) extractTokenFromCookie: 쿠키에서 JWT 토큰 추출
public String extractTokenFromCookie(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
extractTokenFromCookie 메서드는 HTTP 요청에서 지정된 이름의 쿠키를 찾아 그 값을 반환합니다. 이는 클라이언트가 보낸 요청에서 JWT 토큰을 추출하는 데 사용됩니다.
3-(5) invalidateCookie: JWT 쿠키 무효화
public void invalidateCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
4. JWT 토큰의 만료 시간과 쿠키의 만료 시간 비교
코드에서 generateToken 메서드는 JWT 토큰의 만료 시간을 현재 시점으로부터 10분 후로 설정하고, addTokenToCookie 메서드는 쿠키의 만료 시간을 동일하게 10분으로 설정하고 있습니다. 그러나 실무에서는 이 두 시간을 다르게 설정할 수도 있습니다.
(1) 설정이 다를 경우의 동작
- JWT 토큰이 먼저 만료된 경우: 쿠키는 여전히 브라우저에 남아 있지만, 토큰은 이미 만료되어 인증에 사용할 수 없습니다. 이 경우, 서버는 만료된 토큰을 검증할 수 없으며, 사용자는 다시 인증을 받아야 합니다.
- 쿠키가 먼저 만료된 경우: 쿠키가 만료되어 브라우저에서 삭제되면, JWT 토큰에 접근할 수 없게 됩니다. 사용자는 다시 로그인하거나 새로운 JWT 토큰을 받아야 합니다.
(2) 실무에서의 고려사항
JWT 토큰의 만료 시간과 쿠키의 만료 시간은 보안과 사용자 경험 사이의 균형을 맞추기 위해 신중하게 설정되어야 합니다. 예를 들어, 보안을 강화하기 위해 JWT 토큰의 만료 시간을 짧게 설정하고, 쿠키의 만료 시간을 길게 설정하여 사용자가 자주 로그인하지 않도록 할 수 있습니다.
'Java' 카테고리의 다른 글
[Gradle] 의존성 설정 알아보기 (1) | 2024.09.27 |
---|---|
[JUnit]Spring Boot에서 JUnit으로 테스트하는 방법 (1) | 2024.09.09 |
[Spring] 왜 자주 사용하는 객체들을 자동으로 빈으로 등록하지 않을까? (0) | 2024.08.30 |
[Redis] 직렬화 (0) | 2024.08.26 |
[Spring] 생성자 주입이 필요한 이유 (0) | 2024.08.23 |