Skip to content

Commit

Permalink
Merge branch 'develop' into add/#9
Browse files Browse the repository at this point in the history
  • Loading branch information
daeun084 authored Oct 18, 2024
2 parents f7b5d38 + f4bca36 commit d424ed3
Show file tree
Hide file tree
Showing 16 changed files with 322 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import corecord.dev.common.response.ApiResponse;
import corecord.dev.common.status.ErrorStatus;
import corecord.dev.domain.token.exception.model.TokenException;
import corecord.dev.domain.user.exception.model.UserException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand All @@ -14,6 +15,13 @@
@RestControllerAdvice(annotations = {RestController.class})
public class GeneralExceptionAdvice extends ResponseEntityExceptionHandler {

// UserException 처리
@ExceptionHandler(UserException.class)
public ResponseEntity<ApiResponse<Void>> handleUserException(UserException e) {
log.warn(">>>>>>>>UserException: {}", e.getUserErrorStatus().getMessage());
return ApiResponse.error(e.getUserErrorStatus());
}

// TokenException 처리
@ExceptionHandler(TokenException.class)
public ResponseEntity<ApiResponse<Void>> handleTokenException(TokenException e) {
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/corecord/dev/common/status/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ public enum ErrorStatus implements BaseErrorStatus {
* 404 : 존재하지 않는 정보에 대한 요청.
*/

BAD_REQUEST(HttpStatus.BAD_REQUEST, "E400", "잘못된 요청입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "E401", "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "E403", "접근 권한이 없습니다."),
NOT_FOUND(HttpStatus.NOT_FOUND, "E404", "요청한 자원을 찾을 수 없습니다."),
BAD_REQUEST(HttpStatus.BAD_REQUEST, "E0400", "잘못된 요청입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "E0401", "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "E0403", "접근 권한이 없습니다."),
NOT_FOUND(HttpStatus.NOT_FOUND, "E0404", "요청한 자원을 찾을 수 없습니다."),

/**
* Error Code
* 500 : 서버 내부 오류
*/
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E500", "서버 내부 오류입니다.");
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E9999", "서버 내부 오류입니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
35 changes: 25 additions & 10 deletions src/main/java/corecord/dev/common/util/CookieUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,51 @@
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
@RequiredArgsConstructor
public class CookieUtil {
@Value("${jwt.refresh-token.expiration-time}")
private long refreshTokenExpirationTime;

public ResponseCookie createRefreshTokenCookie(String refreshToken) {
return ResponseCookie.from("refresh_token", refreshToken)
@Value("${jwt.register-token.expiration-time}")
private long registerTokenExpirationTime;

@Value("${jwt.access-token.expiration-time}")
private long accessTokenExpirationTime;

public ResponseCookie createTokenCookie(String tokenName, String token) {
long expirationTime = switch (tokenName) {
case "refreshToken" -> refreshTokenExpirationTime;
case "registerToken" -> registerTokenExpirationTime;
case "accessToken" -> accessTokenExpirationTime;
default -> throw new IllegalArgumentException("Unexpected value: " + tokenName);
};

return ResponseCookie.from(tokenName, token)
.httpOnly(true)
.sameSite("None") // 배포 시 수정
.secure(false) // 배포 시 수정
.secure(false) // 배포 시 true로 설정
.sameSite("None")
.path("/")
.maxAge(refreshTokenExpirationTime)
.maxAge(expirationTime / 1000) // maxAge는 초 단위
.build();
}

public Cookie getCookie(HttpServletRequest request) {
public String getCookieValue(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh_token")) {
return cookie;
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
}
return null;
}

public Cookie deleteRefreshTokenCookie() {
Cookie cookie = new Cookie("refresh_token", "");
public Cookie deleteCookie(String cookieName) {
Cookie cookie = new Cookie(cookieName, "");
cookie.setMaxAge(0);
cookie.setPath("/");
return cookie;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/corecord/dev/common/util/JwtFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
String token = resolveToken(request);

if (token != null && jwtUtil.isAccessTokenValid(token)) {
String userId = jwtUtil.getUserIdFromAcccessToken(token).toString();
String userId = jwtUtil.getUserIdFromAccessToken(token).toString();
Authentication authToken = new UsernamePasswordAuthenticationToken(
userId, // principal로 userId 사용
null, // credentials는 필요 없으므로 null
Expand Down
41 changes: 20 additions & 21 deletions src/main/java/corecord/dev/common/util/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import corecord.dev.domain.token.exception.enums.TokenErrorStatus;
import corecord.dev.domain.token.exception.model.TokenException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
Expand Down Expand Up @@ -61,50 +62,48 @@ public String generateRegisterToken(String providerId) {
.compact();
}

public boolean isRegisterTokenValid(String token) {
return isTokenValid(token, "providerId", TokenErrorStatus.INVALID_REGISTER_TOKEN);
}

public boolean isAccessTokenValid(String token) {
return isTokenValid(token, "userId");
return isTokenValid(token, "userId", TokenErrorStatus.INVALID_ACCESS_TOKEN);
}

public boolean isRegisterTokenValid(String token) {
return isTokenValid(token, "providerId");
public boolean isRefreshTokenValid(String token) {
return isTokenValid(token, "userId", TokenErrorStatus.INVALID_REFRESH_TOKEN);
}

private boolean isTokenValid(String token, String claimKey) {
private boolean isTokenValid(String token, String claimKey, TokenErrorStatus invalidTokenErrorStatus) {
try {
var claims = Jwts.parser()
.verifyWith(this.getSigningKey())
.build()
.parseSignedClaims(token);

// 토큰 만료 여부 확인
Date expirationDate = claims.getPayload().getExpiration();
if (expirationDate.before(new Date())) {
log.warn("토큰이 만료되었습니다.");
return false; // 만료된 토큰
log.warn("{}이 만료되었습니다.", invalidTokenErrorStatus.getMessage());
throw new TokenException(invalidTokenErrorStatus);
}

// 필수 클레임이 있는지 확인
String claimValue = claims.getPayload().get(claimKey, String.class);
if (claimValue == null || claimValue.isEmpty()) {
log.warn("토큰에 {} 클레임이 없습니다.", claimKey);
return false; // 필수 클레임이 없는 경우
throw new TokenException(invalidTokenErrorStatus);
}

return true; // 유효한 토큰
return true;

} catch (ExpiredJwtException e) {
log.warn("토큰이 만료되었습니다: {}", e.getMessage());
throw new TokenException(invalidTokenErrorStatus);
} catch (JwtException e) {
log.warn("유효하지 않은 토큰입니다. JWT 예외: {}", e.getMessage());
return false;
} catch (IllegalArgumentException e) {
log.warn("잘못된 토큰 형식입니다. 예외: {}", e.getMessage());
return false;
log.warn("유효하지 않은 토큰입니다: {}", e.getMessage());
throw new TokenException(invalidTokenErrorStatus);
}
}


public String getTokenFromHeader(String authorizationHeader) {
return authorizationHeader.substring(7);
}

public String getProviderIdFromToken(String token) {
try {
log.info("토큰 파싱");
Expand All @@ -121,7 +120,7 @@ public String getProviderIdFromToken(String token) {
}
}

public String getUserIdFromAcccessToken(String token) {
public String getUserIdFromAccessToken(String token) {
try {
return Jwts.parser()
.verifyWith(this.getSigningKey())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.Optional;

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuthLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Value("${jwt.redirect.access}")
private String ACCESS_TOKEN_REDIRECT_URI; // 기존 유저 로그인 시 리다이렉트 URI
private String ACCESS_TOKEN_REDIRECT_URI;

@Value("${jwt.redirect.register}")
private String REGISTER_TOKEN_REDIRECT_URI; // 신규 유저 로그인 시 리다이렉트 URI
private String REGISTER_TOKEN_REDIRECT_URI;

private final JwtUtil jwtUtil;
private final CookieUtil cookieUtil;
Expand All @@ -49,29 +48,75 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
log.info("name: {}", name);

Optional<User> optionalUser = userRepository.findByProviderId(providerId);
User user;

if (optionalUser.isPresent()) {
log.info("기존 유저입니다. 액세스 토큰과 리프레쉬 토큰을 발급합니다.");
user = optionalUser.get();
refreshTokenRepository.deleteByUserId(user.getUserId());
String refreshToken = jwtUtil.generateRefreshToken(user.getUserId());
RefreshToken newRefreshToken = RefreshToken.builder().userId(user.getUserId()).refreshToken(refreshToken).build();
refreshTokenRepository.save(newRefreshToken);

ResponseCookie cookie = cookieUtil.createRefreshTokenCookie(refreshToken);
response.addHeader("Set-Cookie", cookie.toString());

String accessToken = URLEncoder.encode(jwtUtil.generateAccessToken(user.getUserId()));
String redirectURI = String.format(ACCESS_TOKEN_REDIRECT_URI, accessToken);
getRedirectStrategy().sendRedirect(request, response, redirectURI);
handleExistingUser(request, response, optionalUser.get());
} else {
log.info("신규 유저입니다. 레지스터 토큰을 발급합니다.");
String registerToken = URLEncoder.encode(jwtUtil.generateRegisterToken(providerId));
String redirectURI = String.format(REGISTER_TOKEN_REDIRECT_URI, registerToken);
getRedirectStrategy().sendRedirect(request, response, redirectURI);
handleNewUser(request, response, providerId);
}
}

// 기존 유저 처리
private void handleExistingUser(HttpServletRequest request, HttpServletResponse response, User user) throws IOException {
log.info("기존 유저입니다. 액세스 토큰과 리프레쉬 토큰을 발급합니다.");

// 기존 리프레쉬 토큰 삭제
refreshTokenRepository.deleteByUserId(user.getUserId());

// 새로운 리프레쉬 토큰 생성 및 저장
String refreshToken = jwtUtil.generateRefreshToken(user.getUserId());
saveRefreshToken(user, refreshToken);

// 기존 쿠키 삭제
deleteExistingTokens(response);

// RefreshToken 쿠키 추가
ResponseCookie refreshTokenCookie = cookieUtil.createTokenCookie("refreshToken", refreshToken);
response.addHeader("Set-Cookie", refreshTokenCookie.toString());

// AccessToken 쿠키 추가
String accessToken = jwtUtil.generateAccessToken(user.getUserId());
ResponseCookie accessTokenCookie = cookieUtil.createTokenCookie("accessToken", accessToken);
response.addHeader("Set-Cookie", accessTokenCookie.toString());

String redirectURI = String.format(ACCESS_TOKEN_REDIRECT_URI, accessToken, refreshToken);

// 액세스 토큰 리다이렉트
getRedirectStrategy().sendRedirect(request, response, redirectURI);
}

// 신규 유저 처리
private void handleNewUser(HttpServletRequest request, HttpServletResponse response, String providerId) throws IOException {
log.info("신규 유저입니다. 레지스터 토큰을 발급합니다.");

// 이미 레지스터 토큰이 있다면 삭제
response.addCookie(cookieUtil.deleteCookie("registerToken"));

// 레지스터 토큰 발급
String registerToken = jwtUtil.generateRegisterToken(providerId);

// 레지스터 토큰 쿠키 설정
ResponseCookie registerTokenCookie = cookieUtil.createTokenCookie("registerToken", registerToken);
response.addHeader("Set-Cookie", registerTokenCookie.toString());

String redirectURI = String.format(REGISTER_TOKEN_REDIRECT_URI, registerToken);

// 레지스터 토큰 리다이렉트
getRedirectStrategy().sendRedirect(request, response, redirectURI);
}

// 리프레쉬 토큰 저장
private void saveRefreshToken(User user, String refreshToken) {
RefreshToken newRefreshToken = RefreshToken.builder()
.userId(user.getUserId())
.refreshToken(refreshToken)
.build();
refreshTokenRepository.save(newRefreshToken);
}

// 기존 토큰 삭제
private void deleteExistingTokens(HttpServletResponse response) {
response.addCookie(cookieUtil.deleteCookie("accessToken"));
response.addCookie(cookieUtil.deleteCookie("refreshToken"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package corecord.dev.domain.token.converter;

import corecord.dev.domain.token.dto.response.TokenResponse;

public class TokenConverter {
public static TokenResponse.AccessTokenResponse toAccessTokenResponse(String newAccessToken) {
return TokenResponse.AccessTokenResponse.builder().accessToken(newAccessToken).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
@Getter
@AllArgsConstructor
public enum TokenErrorStatus implements BaseErrorStatus {
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "E001", "유효하지 않은 액세스 토큰입니다."),
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "E002", "유효하지 않은 리프레쉬 토큰입니다."),
INVALID_REGISTER_TOKEN(HttpStatus.UNAUTHORIZED, "E003", "유효하지 않은 회원가입 토큰입니다."),
REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E004", "해당 유저 ID의 리프레쉬 토큰이 없습니다.");
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "E0400_ACCESS", "유효하지 않은 액세스 토큰입니다."),
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "E0400_REFRESH", "유효하지 않은 리프레쉬 토큰입니다."),
INVALID_REGISTER_TOKEN(HttpStatus.UNAUTHORIZED, "E0400_REGISTER", "유효하지 않은 회원가입 토큰입니다."),
REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E0404_REFRESH", "해당 유저 ID의 리프레쉬 토큰이 없습니다."),
REGISTER_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E0404_REGISTER", "회원가입 토큰이 없습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Loading

0 comments on commit d424ed3

Please sign in to comment.