Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions src/main/java/com/ajouchong/jwt/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
Expand All @@ -19,15 +20,20 @@
import java.nio.charset.StandardCharsets;
import java.util.Collections;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {



private final JwtTokenProvider jwtTokenProvider;
private final MemberRepository memberRepository;

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {

protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {

String token = extractToken(request);

if (token == null) {
Expand All @@ -36,15 +42,16 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht
}

try {
// 토큰 만료 여부 확인
if (jwtTokenProvider.isExpired(token)) {
log.warn("만료된 토큰으로 인증 시도: {}", token.substring(0, Math.min(token.length(), 20)) + "...");
sendErrorResponse(response, "Token has expired.");
return;
}

// 인증 정보 설정
setAuthentication(token);
log.debug("JWT 인증 성공: {}", request.getRequestURI());
} catch (Exception e) {
log.warn("JWT 인증 실패: {} - {}", request.getRequestURI(), e.getMessage());
sendErrorResponse(response, "Invalid token: " + e.getMessage());
return;
}
Expand All @@ -53,39 +60,42 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht
}

private String extractToken(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
String authorization = request.getHeader(JwtConstants.AUTHORIZATION_HEADER);
if (authorization != null && authorization.startsWith(JwtConstants.BEARER_PREFIX)) {
return authorization.substring(JwtConstants.BEARER_PREFIX.length());
}

return extractTokenFromCookies(request);
}

private String extractTokenFromCookies(HttpServletRequest request) {
if (request.getCookies() == null) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}

for (Cookie cookie : request.getCookies()) {
if ("accessToken".equals(cookie.getName())) {
for (Cookie cookie : cookies) {
if (JwtConstants.ACCESS_TOKEN_COOKIE_NAME.equals(cookie.getName())) {
try {
return java.net.URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8);
} catch (Exception e) {
System.err.println("Invalid Cookie Format: " + e.getMessage());
log.warn("쿠키 디코딩 실패: {}", e.getMessage());
return null;
}
}
}
return null;
}


private void setAuthentication(String token) {
String email = jwtTokenProvider.getEmailFromToken(token);
String role = jwtTokenProvider.getRoleFromToken(token);

Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("User not found: " + email));
.orElseThrow(() -> {
log.error("토큰의 이메일로 회원을 찾을 수 없음: {}", email);
return new IllegalArgumentException("User not found: " + email);
});

Authentication authentication = new UsernamePasswordAuthenticationToken(
member,
Expand All @@ -100,6 +110,6 @@ private void sendErrorResponse(HttpServletResponse response, String message) thr
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"" + message + "\"}");
response.getWriter().write(String.format(JwtConstants.ERROR_RESPONSE_TEMPLATE, message));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
public class JwtAuthenticationResponse {
private String tokenType = "Bearer";
private String accessToken;
private String refreshToken;
private Long expiresIn;

public JwtAuthenticationResponse(String accessToken) {
this.accessToken = accessToken;
}

public JwtAuthenticationResponse(String accessToken, String refreshToken, Long expiresIn) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expiresIn = expiresIn;
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/ajouchong/jwt/JwtConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.ajouchong.jwt;

public final class JwtConstants {

// 토큰 타입
public static final String ACCESS_TOKEN_SUBJECT = "accessToken";
public static final String REFRESH_TOKEN_SUBJECT = "refreshToken";

// 클레임 키
public static final String EMAIL_CLAIM = "email";
public static final String NAME_CLAIM = "name";
public static final String ROLE_CLAIM = "role";

// 쿠키 이름
public static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken";
public static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken";

// 토큰 유효기간 (밀리초)
public static final long ACCESS_TOKEN_VALIDITY = 1000L * 60 * 60 * 24; // 1일
public static final long REFRESH_TOKEN_VALIDITY = 1000L * 60 * 60 * 24 * 7; // 7일

// 쿠키 유효기간 (초)
public static final int ACCESS_TOKEN_COOKIE_MAX_AGE = 60 * 120; // 120분
public static final int REFRESH_TOKEN_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7일

// 보안 설정
public static final int MIN_SECRET_KEY_LENGTH = 32;

// HTTP 헤더
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";

// 응답 메시지
public static final String ERROR_RESPONSE_TEMPLATE = "{\"error\": \"Unauthorized\", \"message\": \"%s\"}";

private JwtConstants() {
// 유틸리티 클래스이므로 인스턴스화 방지
}
}
6 changes: 5 additions & 1 deletion src/main/java/com/ajouchong/jwt/JwtTokenDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JwtTokenDto {
private String grantType;
private String accessToken;
private String refreshToken;
private Long expiresIn;
}
121 changes: 73 additions & 48 deletions src/main/java/com/ajouchong/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,77 +21,85 @@
public class JwtTokenProvider {

private static SecretKey secretKey = null;
private static final long accessTokenValidity = 1000L * 60 * 60 * 24; // 1일

public JwtTokenProvider(@Value("${jwt.secret}") String secret) {
byte[] keyBytes = Base64.getDecoder().decode(secret);
if (keyBytes.length < 32) { // 최소 길이 검증
throw new IllegalArgumentException("JWT Secret key는 최소 32 bytes이어야 합니다.");
validateAndInitializeSecretKey(secret);
}

private void validateAndInitializeSecretKey(String secret) {
try {
byte[] keyBytes = Base64.getDecoder().decode(secret);
if (keyBytes.length < JwtConstants.MIN_SECRET_KEY_LENGTH) {
throw new IllegalArgumentException("JWT Secret key는 최소 " + JwtConstants.MIN_SECRET_KEY_LENGTH + " bytes이어야 합니다.");
}
secretKey = Keys.hmacShaKeyFor(keyBytes);
} catch (IllegalArgumentException e) {
log.error("JWT Secret key 초기화 실패: {}", e.getMessage());
throw e;
}
secretKey = Keys.hmacShaKeyFor(keyBytes);
}

public static String createAccessToken(Member member) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidity);
Date expiryDate = new Date(now.getTime() + JwtConstants.ACCESS_TOKEN_VALIDITY);

return Jwts.builder()
.setSubject("accessToken")
.setSubject(JwtConstants.ACCESS_TOKEN_SUBJECT)
.setClaims(createAccessTokenClaims(member))
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

private static Map<String, Object> createAccessTokenClaims (Member member) {
Map<String, Object> map = new HashMap<>();
map.put("email", member.getEmail());
map.put("name", member.getName());
map.put("role", member.getRole());
return map;
private static Map<String, Object> createAccessTokenClaims(Member member) {
Map<String, Object> claims = new HashMap<>();
claims.put(JwtConstants.EMAIL_CLAIM, member.getEmail());
claims.put(JwtConstants.NAME_CLAIM, member.getName());
claims.put(JwtConstants.ROLE_CLAIM, member.getRole());
return claims;
}

public String createRefreshToken(Member member) {
Date now = new Date();

long refreshTokenValidity = 1000L * 60 * 60 * 24 * 7;
Date expiryDate = new Date(now.getTime() + refreshTokenValidity);
Date expiryDate = new Date(now.getTime() + JwtConstants.REFRESH_TOKEN_VALIDITY);

return Jwts.builder()
.setSubject("refreshToken")
.setSubject(JwtConstants.REFRESH_TOKEN_SUBJECT)
.setClaims(createRefreshTokenClaims(member))
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

private static Map<String, Object> createRefreshTokenClaims (Member member) {
Map<String, Object> map = new HashMap<>();
map.put("email", member.getEmail());
return map;
private static Map<String, Object> createRefreshTokenClaims(Member member) {
Map<String, Object> claims = new HashMap<>();
claims.put(JwtConstants.EMAIL_CLAIM, member.getEmail());
return claims;
}

public boolean isExpired(String token) {
try {
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
} catch (ExpiredJwtException e) {
return true; // 만료된 경우 true 반환
log.debug("JWT 토큰이 만료되었습니다: {}", e.getMessage());
return true;
} catch (JwtException e) {
return true; // 유효하지 않은 토큰도 만료된 것으로 처리
log.debug("유효하지 않은 JWT 토큰입니다: {}", e.getMessage());
return true;
}
}

public String getEmailFromToken(String token) {
return getClaimsFromToken(token).get("email", String.class);
return getClaimsFromToken(token).get(JwtConstants.EMAIL_CLAIM, String.class);
}

public String getRoleFromToken(String token) {
return getClaimsFromToken(token).get("role", String.class);
return getClaimsFromToken(token).get(JwtConstants.ROLE_CLAIM, String.class);
}

public String getNameFromToken(String token) {
return getClaimsFromToken(token).get("name", String.class);
return getClaimsFromToken(token).get(JwtConstants.NAME_CLAIM, String.class);
}

private Claims getClaimsFromToken(String token) {
Expand All @@ -102,30 +110,31 @@ private Claims getClaimsFromToken(String token) {
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
log.warn("토큰이 만료되었습니다: {}", e.getMessage());
throw new InvalidJwtException("토큰이 만료되었습니다.");
} catch (JwtException e) {
log.warn("유효하지 않은 JWT 토큰입니다: {}", e.getMessage());
throw new InvalidJwtException("유효하지 않은 JWT 토큰입니다.");
}
}

public void setJwtCookie(HttpServletResponse response, String accessToken, String refreshToken) {
Cookie accessCookie = new Cookie("accessToken", accessToken);
accessCookie.setHttpOnly(true);
accessCookie.setSecure(true); // HTTPS 환경에서만 전송
accessCookie.setPath("/");
accessCookie.setMaxAge(60 * 120); // 120분 후 만료
response.addCookie(accessCookie);

setCookie(response, JwtConstants.ACCESS_TOKEN_COOKIE_NAME, accessToken, JwtConstants.ACCESS_TOKEN_COOKIE_MAX_AGE);

if (refreshToken != null) {
Cookie refreshCookie = new Cookie("refreshToken", refreshToken);
refreshCookie.setHttpOnly(true);
refreshCookie.setSecure(true);
refreshCookie.setPath("/");
refreshCookie.setMaxAge(60 * 60 * 24 * 7); // 7일 후 만료
response.addCookie(refreshCookie);
setCookie(response, JwtConstants.REFRESH_TOKEN_COOKIE_NAME, refreshToken, JwtConstants.REFRESH_TOKEN_COOKIE_MAX_AGE);
}
}

private void setCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}

public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
Expand All @@ -134,31 +143,47 @@ public boolean validateToken(String token) {
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
log.error("JWT 토큰이 만료되었습니다: {}", e.getMessage());
log.debug("JWT 토큰이 만료되었습니다: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.error("지원되지 않는 JWT 토큰입니다: {}", e.getMessage());
log.warn("지원되지 않는 JWT 토큰입니다: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.error("잘못된 JWT 서명입니다: {}", e.getMessage());
} catch (SignatureException e) {
log.error("JWT 서명 검증 실패: {}", e.getMessage());
log.warn("잘못된 JWT 서명입니다: {}", e.getMessage());
} catch (SecurityException e) {
log.warn("JWT 서명 검증 실패: {}", e.getMessage());
} catch (JwtException e) {
log.error("유효하지 않은 JWT 토큰입니다: {}", e.getMessage());
log.warn("유효하지 않은 JWT 토큰입니다: {}", e.getMessage());
}
return false;
}

public String getRefreshTokenFromCookie(HttpServletRequest request) {
if (request.getCookies() == null) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}
for (Cookie cookie : request.getCookies()) {
if ("refreshToken".equals(cookie.getName())) {

for (Cookie cookie : cookies) {
if (JwtConstants.REFRESH_TOKEN_COOKIE_NAME.equals(cookie.getName())) {
return cookie.getValue();
}
}
return null;
}

public void clearJwtCookies(HttpServletResponse response) {
clearCookie(response, JwtConstants.ACCESS_TOKEN_COOKIE_NAME);
clearCookie(response, JwtConstants.REFRESH_TOKEN_COOKIE_NAME);
}

private void clearCookie(HttpServletResponse response, String name) {
Cookie cookie = new Cookie(name, null);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}

public static class InvalidJwtException extends RuntimeException {
public InvalidJwtException(String message) {
super(message);
Expand Down
Loading
Loading