Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] refresh token으로 로그인 상태 유지 #24

Merged
merged 30 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5a352fa
feat: 로그인 시 refresh token 발급 및 헤더에 응답
helenason Feb 25, 2025
4da0d05
feat: 매 요청에서 token 검증 및 재발급
helenason Feb 25, 2025
a3867ac
test: OAuthService 로그인 로직
helenason Feb 25, 2025
7892636
refac: refresh token 만료 환경 변수 추출
helenason Feb 25, 2025
0830840
test: UuidTokenProvider 클래스
helenason Feb 25, 2025
3b18bd7
fix: 인증 로직 통합 테스트하면서 잘못된 로직 수정
helenason Feb 25, 2025
360d0e9
refac: 인증 필터 이름 변경
helenason Feb 25, 2025
3bc77ac
test: mock user 추출
helenason Feb 25, 2025
1e837de
test: ServiceTest 추출
helenason Feb 25, 2025
9bcacd8
refac: 패키지 구조 기존 구조랑 통일
helenason Feb 25, 2025
aef9b77
refac: 스웨거 문서 수정
helenason Feb 25, 2025
57f96b2
refac: 불필요한 생성자 제거
helenason Feb 27, 2025
71c54b2
refac: 레디스 데이터 초기화 메서드 테스트로 이동
helenason Feb 27, 2025
c037a0f
refac: 로그인 시 응답 토큰 위치 헤더 -> 바디
helenason Feb 27, 2025
56f06c0
refac: oauth url API 응답 형식 통일
helenason Feb 27, 2025
31b5fe3
test: 응답 형태 변경에 따른 테스트 코드 수정
helenason Feb 27, 2025
4a92e28
feat: 응답 DTO 스웨거 설명 추가
helenason Feb 27, 2025
af5de97
refac: 공통 응답 클래스 data 필드 타입 변경 Object -> 제네릭
helenason Feb 27, 2025
7ad3a48
refac: refresh token 발급 방식 변경
helenason Feb 27, 2025
7ad1105
refac: 서비스 계층의 책임 분리를 위해 TokenProvider 추가
helenason Feb 27, 2025
ab11995
refac: Auth 에러 코드 통합 및 static import 제거
helenason Feb 27, 2025
bfc7fbc
refac: 커밋 7ad1105d 보완
helenason Feb 27, 2025
65c6684
refac: filter 에러 응답 형태 통일
helenason Feb 27, 2025
e20efb8
refac: 인증 도메인 용어 통일 oauth -> auth
helenason Feb 28, 2025
9902ae2
refac: 패키지 위치 통일
helenason Feb 28, 2025
a451d3e
refac: 불필요한 커스텀 예외 제거
helenason Feb 28, 2025
51e56e3
refac: 인증 관련 클래스 auth feature 패키지로 이동
helenason Feb 28, 2025
196cf63
docs: 스웨거 문서 정리
helenason Feb 28, 2025
284fc05
refac: 인증 필터에서 상황에 따른 예외 반환
helenason Feb 28, 2025
dd082d4
refac: login API HTTP method GET -> POST
helenason Feb 28, 2025
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
63 changes: 63 additions & 0 deletions src/main/java/com/evenly/took/feature/auth/api/AuthApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.evenly.took.feature.auth.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.evenly.took.feature.auth.domain.OAuthType;
import com.evenly.took.feature.auth.dto.request.RefreshTokenRequest;
import com.evenly.took.feature.auth.dto.response.AuthResponse;
import com.evenly.took.feature.auth.dto.response.OAuthUrlResponse;
import com.evenly.took.feature.auth.dto.response.TokenResponse;
import com.evenly.took.global.exception.dto.ErrorResponse;
import com.evenly.took.global.response.SuccessResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "[1. Auth]")
public interface AuthApi {

@Operation(
summary = "소셜 로그인 인가 URL 리다이렉트",
description = "지정된 OAuthType에 따른 소셜 로그인 인가 코드 요청 URL로 클라이언트를 리다이렉트합니다.")
@ApiResponses({
@ApiResponse(responseCode = "302", description = "리다이렉트 성공")
})
@GetMapping("/api/auth/{oauthType}")
SuccessResponse<OAuthUrlResponse> redirectAuthRequestUrl(
@Parameter(description = "소셜 공급자 타입 (예: GOOGLE, KAKAO, APPLE)", required = true, example = "GOOGLE")
@PathVariable OAuthType oauthType);

@Operation(
summary = "소셜 로그인 및 토큰 발급",
description = "소셜 로그인 인가 코드를 통해 토큰(Access Token & Refresh Token)을 발급받습니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "로그인 성공")
})
@PostMapping("/api/auth/login/{oauthType}")
SuccessResponse<AuthResponse> login( // TODO 에러 응답 추가
@Parameter(description = "소셜 공급자 타입 (예: GOOGLE, KAKAO, APPLE)", required = true, example = "GOOGLE")
@PathVariable OAuthType oauthType,
@Parameter(description = "소셜 서버로부터 전달받은 인가 코드", required = true)
@RequestParam String code);

@Operation(
summary = "토큰 재발급",
description = "토큰(Access Token)을 재발급받습니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "토큰 재발급 성공"),
@ApiResponse(responseCode = "401", description = "Refresh Token 만료", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/api/auth/refresh")
SuccessResponse<TokenResponse> refreshToken(
@RequestBody(description = "Access Token을 재발급 받기 위한 Refresh Token", required = true)
@org.springframework.web.bind.annotation.RequestBody RefreshTokenRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.evenly.took.feature.auth.api;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.evenly.took.feature.auth.application.AuthService;
import com.evenly.took.feature.auth.domain.OAuthType;
import com.evenly.took.feature.auth.dto.request.RefreshTokenRequest;
import com.evenly.took.feature.auth.dto.response.AuthResponse;
import com.evenly.took.feature.auth.dto.response.OAuthUrlResponse;
import com.evenly.took.feature.auth.dto.response.TokenResponse;
import com.evenly.took.global.response.SuccessResponse;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class AuthController implements AuthApi {

private final AuthService authService;

@GetMapping("/api/auth/{oauthType}")
public SuccessResponse<OAuthUrlResponse> redirectAuthRequestUrl(@PathVariable OAuthType oauthType) {
OAuthUrlResponse response = authService.getAuthCodeRequestUrl(oauthType);
return SuccessResponse.of(HttpStatus.FOUND, response);
}

@PostMapping("/api/auth/login/{oauthType}")
public SuccessResponse<AuthResponse> login(@PathVariable OAuthType oauthType, @RequestParam String code) {
AuthResponse response = authService.loginAndGenerateToken(oauthType, code);
return SuccessResponse.of(response);
}

@PostMapping("/api/auth/refresh")
public SuccessResponse<TokenResponse> refreshToken(@RequestBody RefreshTokenRequest request) {
TokenResponse response = authService.refreshToken(request);
return SuccessResponse.of(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.evenly.took.feature.auth.api;

import org.springframework.stereotype.Component;

import com.evenly.took.feature.auth.exception.AuthErrorCode;
import com.evenly.took.feature.common.exception.TookException;

import jakarta.servlet.http.HttpServletRequest;

@Component
public class HeaderHandler {

private static final String HEADER_KEY_OF_AUTH = "Authorization";
private static final String HEADER_VALUE_PREFIX_OF_AUTH = "Bearer ";

public String resolveAccessToken(HttpServletRequest request) {
String bearerToken = request.getHeader(HEADER_KEY_OF_AUTH);
validateAuthHeader(bearerToken);
return bearerToken.substring(HEADER_VALUE_PREFIX_OF_AUTH.length());
}

private void validateAuthHeader(String bearerToken) {
if (bearerToken == null || !bearerToken.startsWith(HEADER_VALUE_PREFIX_OF_AUTH)) {
throw new TookException(AuthErrorCode.JWT_UNAUTHORIZED);
}
}
}
46 changes: 0 additions & 46 deletions src/main/java/com/evenly/took/feature/auth/api/OAuthApi.java

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.evenly.took.feature.auth.application;

import org.springframework.stereotype.Service;

import com.evenly.took.feature.auth.client.AuthCodeRequestUrlProviderComposite;
import com.evenly.took.feature.auth.client.UserClientComposite;
import com.evenly.took.feature.auth.domain.OAuthType;
import com.evenly.took.feature.auth.dto.TokenDto;
import com.evenly.took.feature.auth.dto.request.RefreshTokenRequest;
import com.evenly.took.feature.auth.dto.response.AuthResponse;
import com.evenly.took.feature.auth.dto.response.OAuthUrlResponse;
import com.evenly.took.feature.auth.dto.response.TokenResponse;
import com.evenly.took.feature.user.dao.UserRepository;
import com.evenly.took.feature.user.domain.User;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthService {

private final AuthCodeRequestUrlProviderComposite authCodeComposite;
private final UserClientComposite userClientComposite;
private final UserRepository userRepository;
private final TokenProvider tokenProvider;

public OAuthUrlResponse getAuthCodeRequestUrl(OAuthType oauthType) {
String url = authCodeComposite.provide(oauthType);
return new OAuthUrlResponse(url);
}

public AuthResponse loginAndGenerateToken(OAuthType oauthType, String authCode) {
User user = userClientComposite.fetch(oauthType, authCode);
User savedUser = userRepository.findByOauthIdentifier(user.getOauthIdentifier())
.orElseGet(() -> userRepository.save(user));
TokenDto tokens = tokenProvider.provideTokens(savedUser);
return new AuthResponse(tokens, user);
}

public TokenResponse refreshToken(RefreshTokenRequest request) {
String refreshToken = request.refreshToken();
String accessToken = tokenProvider.provideAccessTokenByRefreshToken(refreshToken);
return new TokenResponse(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package com.evenly.took.global.security.jwt;

import static com.evenly.took.global.exception.auth.jwt.JwtErrorCode.*;
package com.evenly.took.feature.auth.application;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;

import org.springframework.stereotype.Component;

import com.evenly.took.feature.user.domain.User;
import com.evenly.took.global.config.properties.jwt.JwtProperties;
import com.evenly.took.global.exception.auth.oauth.InvalidJwtTokenException;
import com.evenly.took.feature.auth.exception.AuthErrorCode;
import com.evenly.took.feature.common.exception.TookException;
import com.evenly.took.global.config.properties.jwt.AuthProperties;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
Expand All @@ -26,19 +24,18 @@
@RequiredArgsConstructor
public class JwtTokenProvider {

private final JwtProperties jwtProperties;
private final AuthProperties authProperties;

public String generateAccessToken(User user) {
Claims claims = generateClaims(user);
public String generateAccessToken(String userId) {
Claims claims = generateClaims(userId);
Date now = new Date();
Date expiredAt = new Date(now.getTime() + jwtProperties.accessTokenExpirationMilliTime());
Date expiredAt = new Date(now.getTime() + authProperties.accessTokenExpirationMilliTime());
return buildAccessToken(claims, now, expiredAt);
}

private Claims generateClaims(final User user) {
Claims claims = Jwts.claims().setSubject(user.getId().toString());
claims.put("name", user.getName());
return claims;
private Claims generateClaims(String userId) {
return Jwts.claims()
.setSubject(userId);
}

private String buildAccessToken(Claims claims, Date now, Date expiredAt) {
Expand All @@ -55,15 +52,19 @@ public void validateToken(String token) {
try {
parseClaims(token);
} catch (JwtException | IllegalArgumentException e) {
log.error(JWT_UNAUTHORIZED.getMessage(), e);
throw new InvalidJwtTokenException(JWT_UNAUTHORIZED);
log.error(AuthErrorCode.INVALID_ACCESS_TOKEN.getMessage(), e);
throw new TookException(AuthErrorCode.INVALID_ACCESS_TOKEN);
}
}

public String getUserId(String token) {
return parseClaims(token)
.getBody()
.getSubject();
try {
return parseClaims(token)
.getBody()
.getSubject();
} catch (JwtException ex) {
throw new TookException(AuthErrorCode.INVALID_ACCESS_TOKEN);
}
}

private Jws<Claims> parseClaims(String token) {
Expand All @@ -75,7 +76,7 @@ private Jws<Claims> parseClaims(String token) {
}

private Key getSigningKey() {
String secret = jwtProperties.accessTokenSecret();
String secret = authProperties.accessTokenSecret();
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
}

This file was deleted.

Loading