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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sejongisc.backend.common.auth.jwt.JwtParser;
import org.sejongisc.backend.common.auth.jwt.JwtUtils;
import org.sejongisc.backend.common.config.security.SecurityConstants;
import org.sejongisc.backend.common.exception.ErrorCode;
import org.sejongisc.backend.common.exception.ErrorResponse;
Expand All @@ -31,7 +31,7 @@
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtParser jwtParser;
private final JwtUtils jwtUtils;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final ObjectMapper objectMapper;

Expand All @@ -55,8 +55,8 @@ protected void doFilterInternal(@NotNull HttpServletRequest request,
token = resolveTokenFromCookie(request);
}

if (token != null && jwtParser.validationToken(token) ) {
UsernamePasswordAuthenticationToken authentication = jwtParser.getAuthentication(token);
if (token != null && jwtUtils.validationToken(token) ) {
UsernamePasswordAuthenticationToken authentication = jwtUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("SecurityContext에 인증 저장됨: {}", authentication.getName());
} else {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,112 @@

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Date;
import java.util.UUID;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtParser {
public class JwtUtils {

private static final String CLAIM_USER_ID = "uid";
private static final String CLAIM_ROLE = "role";

private final TokenEncryptor tokenEncryptor;
private final CustomUserDetailsService customUserDetailsService;

@Value("${jwt.secret}")
private String rawSecretKey;

@Value("${jwt.expireDate.accessToken}")
private long accessTokenValidityInMillis;

@Value("${jwt.expireDate.refreshToken}")
private long refreshTokenValidityInMillis;

private SecretKey secretKey;

@PostConstruct
public void init() {
//byte[] keyBytes = Base64.getDecoder().decode(rawSecretKey); // rawSecretKey의 base64 규격 이슈로 아래 코드로 대체
this.secretKey = Keys.hmacShaKeyFor(rawSecretKey.getBytes(StandardCharsets.UTF_8));
}

// 토큰 유효성 검사
public boolean validationToken(String token) {
// Access Token 생성
public String createToken(UUID userId, Role role, String email) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidityInMillis);

return Jwts.builder()
.setSubject(email)
.claim(CLAIM_USER_ID, userId.toString())
.claim(CLAIM_ROLE, role.name())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

// Refresh Token 생성 (AES-GCM 암호화 포함)
public String createRefreshToken(UUID userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + refreshTokenValidityInMillis);

String rawRefreshToken = Jwts.builder()
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();

return tokenEncryptor.encrypt(rawRefreshToken);
}

// 토큰 만료일 조회
public Date getExpiration(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
}

// 토큰에서 userId 추출 (uid 클레임 우선, 없으면 subject 사용 - RefreshToken 호환)
public UUID getUserIdFromToken(String token) {
Claims claims = parseClaims(token);
String userIdStr = claims.get(CLAIM_USER_ID, String.class);

if (userIdStr == null || userIdStr.isBlank()) {
userIdStr = claims.getSubject();
}

if (userIdStr == null || userIdStr.isBlank()) {
throw new JwtException("JWT에 userId(uid/subject)가 없습니다.");
}

try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
log.info("Token validation success");
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Token validation failed: {}", e.getMessage());
return false;
return UUID.fromString(userIdStr);
} catch (IllegalArgumentException | NullPointerException e) {
throw new JwtException("잘못된 userId 형식의 JWT입니다.");
}
}

// Authentication 생성
// 토큰에서 Role 추출
public String getRoleFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.get(CLAIM_ROLE, String.class);
}

// Authentication 객체 생성
public UsernamePasswordAuthenticationToken getAuthentication(String token) {
Claims claims = parseClaims(token);
// TODO : 유지보수성을 위해 클레임 키 상수화 고려
String userId = claims.get("uid", String.class);
String roleStr = claims.get("role", String.class);
String userId = claims.get(CLAIM_USER_ID, String.class);
String roleStr = claims.get(CLAIM_ROLE, String.class);

if (roleStr == null) {
throw new JwtException("JWT에 role 클레임이 없습니다.");
}
Expand All @@ -62,18 +132,28 @@ public UsernamePasswordAuthenticationToken getAuthentication(String token) {
}

if (userId == null) {
throw new JwtException("JWT에 userId(uid)가 없습니다.");
throw new JwtException("JWT에 userId(uid)가 없습니다.");
}

// DB에서 다시 유저를 불러오기 (CustomUserDetailsService 사용)
UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId); // TODO : 성능 고려해서 DB 조회 제거 고민

// TODO: 성능 고려해서 DB 조회 제거 고민
UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId);
log.debug("인증 객체 생성 완료");
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}

// 토큰 유효성 검증
public boolean validationToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
log.info("Token validation success");
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Token validation failed: {}", e.getMessage());
return false;
}
}

// Claims 파싱
// Claims 파싱 (만료된 토큰도 클레임 추출 가능)
private Claims parseClaims(String token) {
try {
return Jwts.parserBuilder()
Expand All @@ -85,25 +165,4 @@ private Claims parseClaims(String token) {
return e.getClaims();
}
}

public UUID getUserIdFromToken(String token) {
Claims claims = parseClaims(token);
String userIdStr = claims.get("uid", String.class);

// uid 클레임이 없을 경우 subject로 대체 (RefreshToken 호환)
if (userIdStr == null || userIdStr.isBlank()) {
userIdStr = claims.getSubject();
}

// 여전히 없거나 비어 있으면 명시적 예외 처리
if (userIdStr == null || userIdStr.isBlank()) {
throw new JwtException("JWT에 userId(uid/subject)가 없습니다.");
}

try {
return UUID.fromString(userIdStr);
} catch (IllegalArgumentException | NullPointerException e) {
throw new JwtException("잘못된 userId 형식의 JWT입니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.sejongisc.backend.common.auth.jwt;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
import org.sejongisc.backend.activity.event.ActivityEvent;
import org.sejongisc.backend.common.auth.entity.RefreshToken;
import org.sejongisc.backend.common.auth.repository.RefreshTokenRepository;
import org.sejongisc.backend.common.auth.jwt.JwtParser;
import org.sejongisc.backend.common.auth.jwt.JwtProvider;
import org.sejongisc.backend.common.auth.jwt.JwtUtils;
import org.sejongisc.backend.common.exception.CustomException;
import org.sejongisc.backend.common.exception.ErrorCode;
import org.sejongisc.backend.user.entity.Role;
Expand All @@ -29,8 +28,9 @@ public class AuthService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
private final JwtUtils jwtUtils;
private final RefreshTokenRepository refreshTokenRepository;

private final JwtParser jwtParser;
private final ApplicationEventPublisher eventPublisher;

Expand All @@ -49,14 +49,14 @@ public AuthResponse login(AuthRequest request) {
!passwordEncoder.matches(request.getPassword().trim(), user.getPasswordHash())) {
throw new CustomException(ErrorCode.UNAUTHORIZED);
}

String accessToken = jwtUtils.createToken(user.getUserId(), user.getRole(), user.getEmail());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 PENDING_MEMBER 검증 이전으로 올린 이유가 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 그 부분은 제가 반영되기 전 코드에서 작업한 것 같아서 수정해야 할 것 같습니당

String refreshToken = jwtUtils.createRefreshToken(user.getUserId());

if (user.getRole().equals(Role.PENDING_MEMBER)) {
throw new CustomException(ErrorCode.NEED_PENDING_APPROVAL);
}

String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail());
String refreshToken = jwtProvider.createRefreshToken(user.getUserId());

// 기존 토큰 삭제 후 새로 저장
refreshTokenRepository.findByUserId(user.getUserId())
.ifPresent(refreshTokenRepository::delete);
Expand Down Expand Up @@ -89,7 +89,7 @@ public AuthResponse login(AuthRequest request) {

@Transactional
public void logout(String accessToken) {
UUID userId = jwtParser.getUserIdFromToken(accessToken);
UUID userId = jwtUtils.getUserIdFromToken(accessToken);
refreshTokenRepository.deleteByUserId(userId);
log.info("로그아웃 완료: userId={}", userId);
}
Expand Down
Loading