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 @@ -89,4 +89,14 @@ public ResponseTemplate<?> singOut(@RequestHeader("Authorization") String authHe

return new ResponseTemplate<>(HttpStatus.OK, "로그아웃 성공", token);
}

@Operation(summary = "토큰 재발급", description = "Refresh Token 기반의 Access Token 재발급 API 입니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공"),
})
@PostMapping("/reissue")
public ResponseTemplate<TokenInfo> reissue(@CookieValue("Refresh") String refreshToken, HttpServletResponse response) {
TokenInfo token = authService.reissue(refreshToken, response);
return new ResponseTemplate<>(HttpStatus.OK, "토큰 재발급 성공", token);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.postdm.backend.domain.auth.application;

import com.postdm.backend.domain.auth.domain.RefreshToken;
import com.postdm.backend.domain.auth.dto.SignInRequestDto;
import com.postdm.backend.domain.auth.dto.SignUpRequestDto;
import com.postdm.backend.domain.auth.repository.RefreshTokenRepository;
import com.postdm.backend.domain.email.domain.entity.CertificationEntity;
import com.postdm.backend.domain.email.domain.repository.CertificationRepository;
import com.postdm.backend.domain.member.domain.entity.Member;
Expand All @@ -14,9 +16,13 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
public class AuthService { // 로그인 및 회원가입 서비스

Expand All @@ -26,6 +32,7 @@ public class AuthService { // 로그인 및 회원가입 서비스
private final JwtProvider jwtProvider;
private final int refreshedMS;
private final TokenBlacklistService tokenBlacklistService;
private final RefreshTokenRepository refreshTokenRepository;

// 생성자 주입 방식
public AuthService(
Expand All @@ -34,13 +41,15 @@ public AuthService(
BCryptPasswordEncoder bCryptPasswordEncoder,
JwtProvider jwtProvider,
@Value("${jwt.refreshedMs}") int refreshedMS,
TokenBlacklistService tokenBlacklistService) {
TokenBlacklistService tokenBlacklistService,
RefreshTokenRepository refreshTokenRepository) {
this.memberRepository = memberRepository;
this.certificationRepository = certificationRepository;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.jwtProvider = jwtProvider;
this.refreshedMS = refreshedMS;
this.tokenBlacklistService = tokenBlacklistService;
this.refreshTokenRepository = refreshTokenRepository;
}


Expand Down Expand Up @@ -126,8 +135,16 @@ public TokenInfo signIn(SignInRequestDto signInRequestDto, HttpServletResponse r
TokenInfo token = jwtProvider.generateToken(username, role); // 로그인이 완료되면 토큰 생성
String refreshToken = jwtProvider.generateRefreshToken(username, role);

response.addCookie(createCookie(refreshToken)); // 쿠키에 refresh 토큰 담음
LocalDateTime expiration = jwtProvider.getExpiration(refreshToken);

// 로그인 성공 후, Refresh Token을 DB에 저장
refreshTokenRepository.save(new RefreshToken(
username,
refreshToken,
expiration
));

response.addCookie(createCookie(refreshToken)); // 쿠키에 refresh 토큰 담음

return token; // 응답 body에는 access 토큰 반환
}
Expand All @@ -142,11 +159,62 @@ private Cookie createCookie(String value) { // 쿠키 생성 메소드
}

public void signOut(String accessToken) {
long expireMillis = jwtProvider.getExpiration(accessToken);
boolean isNewlySaved = tokenBlacklistService.saveIfNotExists(accessToken, expireMillis);
LocalDateTime expiration = jwtProvider.getExpiration(accessToken);
boolean isNewlySaved = tokenBlacklistService.saveIfNotExists(accessToken, expiration);

if (!isNewlySaved) {
throw new CustomException(ErrorCode.ALREADY_SIGN_OUT);
}

Authentication authentication = jwtProvider.getAuthentication(accessToken);
Member member = (Member) authentication.getPrincipal();
String username = member.getUsername();

// Refresh Token 삭제
refreshTokenRepository.findById(username)
.ifPresent(refreshTokenRepository::delete);
}

public TokenInfo reissue(String oldRefreshToken, HttpServletResponse response) {
if (!jwtProvider.validateToken(oldRefreshToken)) {
throw new CustomException(ErrorCode.INVALID_REFRESH_TOKEN);
}

Authentication authentication = jwtProvider.getAuthentication(oldRefreshToken);
Member member = (Member) authentication.getPrincipal();
String username = member.getUsername();

RefreshToken saved = refreshTokenRepository.findById(username)
.orElseThrow(() -> new CustomException(ErrorCode.REFRESH_TOKEN_NOT_FOUND));

if (!saved.getRefreshToken().equals(oldRefreshToken)) {
throw new CustomException(ErrorCode.INVALID_REFRESH_TOKEN);
}

// Access Token 재발급
String role = extractRole(authentication);
String newAccessToken = jwtProvider.generateAccessToken(username, role);

// Refresh Token 회전(Rotation)
String newRefreshToken = jwtProvider.generateRefreshToken(username, role);
LocalDateTime expiration = jwtProvider.getExpiration(newRefreshToken);
saved.update(newRefreshToken, expiration);
refreshTokenRepository.save(saved);

response.addCookie(createCookie(newRefreshToken));

return TokenInfo.builder()
.grantType("Bearer")
.accessToken(newAccessToken)
.role(role)
.build();
}

private String extractRole(Authentication authentication) {
return authentication.getAuthorities().stream()
.findFirst()
.map(GrantedAuthority::getAuthority)
.orElseThrow(() -> new CustomException(ErrorCode.ROLE_NOT_FOUND))
.replace("ROLE_", "");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

@Service
@RequiredArgsConstructor
Expand All @@ -17,7 +18,9 @@ public class TokenBlacklistService {
private final TokenBlacklistRepository repository;

public void save(String token, long expireMillis) {
LocalDateTime expiration = LocalDateTime.now().plus(Duration.ofMillis(expireMillis));
LocalDateTime expiration = Instant.ofEpochMilli(expireMillis)
.atZone(ZoneId.of("Asia/Seoul"))
.toLocalDateTime();
repository.save(new TokenBlacklist(token, expiration));
}

Expand All @@ -26,13 +29,12 @@ public boolean isBlacklisted(String token) {
}

// 중복 저장 방지
public boolean saveIfNotExists(String token, long expireMillis) {
public boolean saveIfNotExists(String token, LocalDateTime expireMillis) {
if (repository.existsByToken(token)) {
return false; // 이미 블랙리스트에 있음
return false;
}

LocalDateTime expiration = LocalDateTime.now().plus(Duration.ofMillis(expireMillis));
repository.save(new TokenBlacklist(token, expiration));
repository.save(new TokenBlacklist(token, expireMillis));
return true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.postdm.backend.domain.auth.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class RefreshToken {

@Id
private String username;

@Lob
private String refreshToken;

private LocalDateTime expiration;

public void update(String newToken, LocalDateTime newExpiration) {
this.refreshToken = newToken;
this.expiration = newExpiration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.postdm.backend.domain.auth.repository;

import com.postdm.backend.domain.auth.domain.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.postdm.backend.global.common.response.ErrorCode;
import com.postdm.backend.global.common.response.ErrorResponse;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
Expand Down Expand Up @@ -46,4 +48,10 @@ protected ResponseEntity<ErrorResponse> validationExceptionHandler(Exception e)
.body(new ErrorResponse(ErrorCode.VALIDATION_FAIL));
}

}
@ExceptionHandler(JwtException.class)
public ResponseEntity<ErrorResponse> handleJwtException(JwtException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse(ErrorCode.INVALID_REFRESH_TOKEN));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ public enum ErrorCode {


METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "잘못된 HTTP 메서드를 호출했습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러 입니다.")
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러 입니다."),

INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 Refresh Token입니다."),
REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "저장된 Refresh Token이 없습니다."),
ROLE_NOT_FOUND(HttpStatus.UNAUTHORIZED, "권한 정보를 찾을 수 없습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Date;
import java.util.List;
Expand Down Expand Up @@ -120,14 +122,16 @@ public Authentication getAuthentication(String token) { // 토큰에서 사용
}

// JWT 토큰 만료 시간 반환
public long getExpiration(String token) {
public LocalDateTime getExpiration(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();

Date expiration = claims.getExpiration();
return expiration.getTime() - System.currentTimeMillis();
return expiration.toInstant()
.atZone(ZoneId.of("Asia/Seoul"))
.toLocalDateTime();
}
}
13 changes: 0 additions & 13 deletions backend/src/main/resources/application-test.yml

This file was deleted.

Loading