Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions src/main/java/org/terning/terningserver/auth/dto/Token.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.terning.terningserver.auth.dto;

public record Token(String accessToken, String refreshToken) {
}
85 changes: 85 additions & 0 deletions src/main/java/org/terning/terningserver/auth/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.terning.terningserver.auth.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.terning.terningserver.auth.dto.Token;
import org.terning.terningserver.common.config.ValueConfig;
import org.terning.terningserver.auth.jwt.exception.JwtErrorCode;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtProvider {

private static final String USER_ID_CLAIM = "userId";
private static final String TOKEN_PREFIX = "Bearer ";

private final ValueConfig valueConfig;
private SecretKey secretKey;

@PostConstruct
protected void init() {
secretKey = Keys.hmacShaKeyFor(valueConfig.getSecretKey().getBytes());
Copy link

Copilot AI Jul 13, 2025

Choose a reason for hiding this comment

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

Specify a charset when converting the secret key string to bytes, e.g., getSecretKey().getBytes(StandardCharsets.UTF_8), to avoid platform-default encoding differences.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

피드백 감사합니다!

getSecretKey().getBytes()처럼 문자열을 바이트로 변환할 때 Charset을 명시하지 않으면, OS에 따라 기본 인코딩이 달라져 예상치 못한 인증 오류를 발생시킬 수 있는 중요한 부분을 잘 짚어주신 것 같아요!

마침 ValueConfig의 init() 메소드에서는 이미 StandardCharsets.UTF_8을 사용해 Base64 인코딩을 하고 있었는데, JwtProvider에서는 이 부분을 놓치고 있었네요.

두 클래스 간의 역할을 명확히 하고 일관성을 유지하기 위해, ValueConfig에서는 Base64 인코딩 로직을 제거하고, JwtProvider의 init() 메소드에서 제안해주신 대로 StandardCharsets.UTF_8을 명시하여 SecretKey를 생성하도록 수정하겠습니다. 이렇게 하면 SecretKey 생성 책임이 JwtProvider로 일원화되어 코드가 더 명확해질 것 같네요!

꼼꼼한 리뷰 감사합니다!

}

public Token generateTokens(Long userId) {
String accessToken = generateToken(userId, valueConfig.getAccessTokenExpired());
String refreshToken = generateToken(userId, valueConfig.getRefreshTokenExpired());
return new Token(accessToken, refreshToken);
}

public Token generateAccessToken(Long userId) {
String accessToken = generateToken(userId, valueConfig.getAccessTokenExpired());
return new Token(accessToken, null);
}

public Long getUserIdFrom(String authorizationHeader) {
String token = resolveToken(authorizationHeader);

Claims claims = parseClaims(token);

Object userIdClaim = claims.get(USER_ID_CLAIM);
if (userIdClaim instanceof Number) {
return ((Number) userIdClaim).longValue();
}
throw new JwtException(JwtErrorCode.INVALID_USER_ID_TYPE.getMessage());
}

public String resolveToken(String rawToken) {
if (rawToken != null && rawToken.startsWith(TOKEN_PREFIX)) {
return rawToken.substring(TOKEN_PREFIX.length());
}
throw new JwtException(JwtErrorCode.TOKEN_NOT_FOUND.getMessage());
}

private String generateToken(Long userId, long expiration) {
Claims claims = Jwts.claims();
claims.put(USER_ID_CLAIM, userId);

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(secretKey)
.compact();
}

private Claims parseClaims(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new JwtException(JwtErrorCode.EXPIRED_JWT_TOKEN.getMessage());
} catch (UnsupportedJwtException | MalformedJwtException | SecurityException | IllegalArgumentException e) {
throw new JwtException(JwtErrorCode.INVALID_JWT_TOKEN.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.terning.terningserver.common.security.jwt.exception;
package org.terning.terningserver.auth.jwt.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -11,6 +11,8 @@ public enum JwtErrorCode {
INVALID_USER_ID(HttpStatus.BAD_REQUEST, "유효하지 않은 userId 값입니다."),
INVALID_USER_ID_TYPE(HttpStatus.BAD_REQUEST, "유효하지 않은 userId 타입입니다."),
INVALID_USER_DETAILS_TYPE(HttpStatus.INTERNAL_SERVER_ERROR, "유효하지 않은 UserDetail 타입입니다."),
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "Authorization 헤더에 토큰이 없습니다."),
EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT 토큰입니다."),
;

public static final String PREFIX = "[JWT ERROR]";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.terning.terningserver.common.security.jwt.exception;
package org.terning.terningserver.auth.jwt.exception;

import lombok.Getter;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import org.terning.terningserver.auth.common.exception.AuthException;
import org.terning.terningserver.common.exception.dto.ErrorResponse;
import org.terning.terningserver.common.exception.enums.ErrorMessage;
import org.terning.terningserver.common.security.jwt.exception.JwtErrorCode;
import org.terning.terningserver.common.security.jwt.exception.JwtException;
import org.terning.terningserver.auth.jwt.exception.JwtErrorCode;
import org.terning.terningserver.auth.jwt.exception.JwtException;

@RestControllerAdvice
@Slf4j
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.terning.terningserver.common.security.jwt.exception.JwtErrorCode;
import org.terning.terningserver.auth.jwt.exception.JwtErrorCode;

import java.util.Map;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.terning.terningserver.common.security.jwt.exception.JwtErrorCode;
import org.terning.terningserver.common.security.jwt.exception.JwtException;
import org.terning.terningserver.auth.jwt.exception.JwtErrorCode;
import org.terning.terningserver.auth.jwt.exception.JwtException;
import org.terning.terningserver.common.security.jwt.auth.JwtClaimsParser;

@Component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.terning.terningserver.common.config.ValueConfig;
import org.terning.terningserver.common.security.jwt.exception.JwtErrorCode;
import org.terning.terningserver.common.security.jwt.exception.JwtException;
import org.terning.terningserver.auth.jwt.exception.JwtErrorCode;
import org.terning.terningserver.auth.jwt.exception.JwtException;
import org.terning.terningserver.common.security.jwt.provider.JwtKeyProvider;

@Service
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.terning.terningserver.common.security.jwt.auth;

import org.terning.terningserver.common.security.jwt.exception.JwtErrorCode;
import org.terning.terningserver.common.security.jwt.exception.JwtException;
import org.terning.terningserver.auth.jwt.exception.JwtErrorCode;
import org.terning.terningserver.auth.jwt.exception.JwtException;

public class UserIdConverter {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.terning.terningserver.common.security.jwt.exception.JwtErrorCode;
import org.terning.terningserver.common.security.jwt.exception.JwtException;
import org.terning.terningserver.auth.jwt.exception.JwtErrorCode;
import org.terning.terningserver.auth.jwt.exception.JwtException;

import java.io.IOException;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.terning.terningserver.external.pushNotification.user.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
Expand All @@ -24,7 +25,7 @@ public class UserSyncEvent {

private Long userId;

@Enumerated
@Enumerated(EnumType.STRING)
private UserSyncEventType eventType;

private String newValue;
Expand Down
Loading