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/#71] 내 프로필 조회 API 구현 #72

Merged
merged 18 commits into from
Feb 15, 2025
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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ dependencies {

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// Caffeine Cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine'
}

tasks.named('test') {
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/com/acon/server/global/auth/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.acon.server.global.auth;

import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// TODO: 어노테이션을 활용하여 리팩
@Configuration
@EnableCaching
public class CacheConfig {

@Value("${jwt.refresh-token-expire-time}")
private long REFRESH_TOKEN_EXPIRATION_TIME;

@Bean
public CacheManager cacheManager() {
// TODO: initialCapacity, maximumSize 설정
// 최대 용량 설정을 따로 진행하지 않음. 메모리 부족 문제 주의 필요
Caffeine<Object, Object> caffeineBuilder = Caffeine.newBuilder()
.expireAfterWrite(REFRESH_TOKEN_EXPIRATION_TIME, TimeUnit.MILLISECONDS);

CaffeineCache refreshTokenCache = new CaffeineCache(
"refreshTokenCache",
caffeineBuilder.build()
);

SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Collections.singletonList(refreshTokenCache));
return cacheManager;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import javax.crypto.SecretKey;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

Expand All @@ -31,6 +33,8 @@ public class JwtTokenProvider {

private static final String MEMBER_ID = "memberId";

private final CacheManager cacheManager;

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

Expand All @@ -50,15 +54,18 @@ public String issueAccessToken(final Authentication authentication) {
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME);
}

public String issueRefreshToken() {
public String issueRefreshToken(Long memberId) {
final Date now = new Date();

return Jwts.builder()
final String refreshToken = Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION_TIME))
.signWith(getSigningKey())
.compact();
storeRefreshToken(refreshToken, memberId);

return refreshToken;
}

public String generateToken(
Expand Down Expand Up @@ -133,4 +140,29 @@ public Claims getTokenClaims(String token, PublicKey publicKey) {
.parseClaimsJws(token)
.getBody();
}
}

// TODO: cache token 관리 로직 클래스로 분리
public void storeRefreshToken(String refreshToken, Long memberId) {
Cache cache = cacheManager.getCache("refreshTokenCache");
// 존재 유무만 확인하므로 빈 값
// TODO: Refresh Token key 수정
cache.put(refreshToken, memberId);
}

public Long validateRefreshToken(String refreshToken) {
Cache cache = cacheManager.getCache("refreshTokenCache");
// Intellij는 해당 조건이 항상 true라고 메세지를 띄우지만 cache.get(memberId)의 반환값이 존재하지 않을 시 null입니다!
if (cache.get(refreshToken) != null) {
return cache.get(refreshToken, Long.class);
}
throw new BusinessException(ErrorType.INVALID_REFRESH_TOKEN_ERROR);
}

// TODO: 블랙리스트 설정 필요
public void deleteRefreshToken(String refreshToken) {
Cache cache = cacheManager.getCache("refreshTokenCache");
validateRefreshToken(refreshToken);
cache.evict(refreshToken);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public enum ErrorType {
INVALID_REQUEST_BODY_ERROR(HttpStatus.BAD_REQUEST, 40006, "유효하지 않은 Request Body입니다. 요청 형식 또는 필드를 확인하세요."),
DATA_INTEGRITY_VIOLATION_ERROR(HttpStatus.BAD_REQUEST, 40007, "데이터 무결성 제약 조건을 위반했습니다."),
INVALID_ACCESS_TOKEN_ERROR(HttpStatus.BAD_REQUEST, 40008, "유효하지 않은 accessToken입니다."),
INVALID_REFRESH_TOKEN_ERROR(HttpStatus.BAD_REQUEST, 40088, "유효하지 않은 refreshToken입니다."),
// TODO: NonNull 필드에 null 값이 입력되었을 때 발생하는 예외 처리 추가

/* 401 Unauthorized */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import com.acon.server.member.api.request.GuidedSpotRequest;
import com.acon.server.member.api.request.LoginRequest;
import com.acon.server.member.api.request.LogoutRequest;
import com.acon.server.member.api.request.MemberAreaRequest;
import com.acon.server.member.api.request.PreferenceRequest;
import com.acon.server.member.api.request.ReissueTokenRequest;
import com.acon.server.member.api.request.WithdrawalReasonRequest;
import com.acon.server.member.api.response.AcornCountResponse;
import com.acon.server.member.api.response.LoginResponse;
import com.acon.server.member.api.response.MemberAreaResponse;
import com.acon.server.member.api.response.ProfileResponse;
import com.acon.server.member.api.response.ReissueTokenResponse;
import com.acon.server.member.application.service.MemberService;
import com.acon.server.member.domain.enums.Cuisine;
import com.acon.server.member.domain.enums.DislikeFood;
Expand Down Expand Up @@ -109,4 +114,38 @@ public ResponseEntity<AcornCountResponse> getAcornCount() {
memberService.fetchAcornCount()
);
}
}

@GetMapping(path = "/members/me", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ProfileResponse> getProfile() {
return ResponseEntity.ok(
memberService.fetchProfile()
);
}

@PostMapping(path = "/auth/logout", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> logout(
@Valid @RequestBody LogoutRequest request
) {
memberService.logout(request.refreshToken());
return ResponseEntity.ok().build();
}

@PostMapping(path = "/auth/reissue",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ReissueTokenResponse> reissueToken(
@Valid @RequestBody ReissueTokenRequest request
) {
return ResponseEntity.ok(
memberService.reissueToken(request.refreshToken())
);
}

@PostMapping(path = "/members/withdrawal", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> postWithdrawal(
@Valid @RequestBody WithdrawalReasonRequest request
) {
memberService.withdrawMember(request.reason(), request.refreshToken());
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.acon.server.member.api.request;

import jakarta.validation.constraints.NotBlank;

public record LogoutRequest(
@NotBlank(message = "refreshToken은 공백일 수 없습니다.")
String refreshToken
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.acon.server.member.api.request;

import jakarta.validation.constraints.NotBlank;

public record ReissueTokenRequest(
@NotBlank(message = "refreshToken은 공백일 수 없습니다.")
String refreshToken
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.acon.server.member.api.request;

import jakarta.validation.constraints.NotBlank;

public record WithdrawalReasonRequest(
@NotBlank(message = "탈퇴 이유는 공백일 수 없습니다.")
String reason,
@NotBlank(message = "refreshToken은 공백일 수 없습니다.")
String refreshToken
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.acon.server.member.api.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import java.util.List;
import lombok.Builder;

@Builder
@JsonInclude(Include.NON_NULL)
public record ProfileResponse(
String image,
String nickname,
int leftAcornCount,
String birthDate,
List<VerifiedArea> verifiedArea
) {

public record VerifiedArea(
Long id,
String name
) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.acon.server.member.api.response;

public record ReissueTokenResponse(
String accessToken,
String refreshToken
) {

public static ReissueTokenResponse of(
final String accessToken,
final String refreshToken
) {
return new ReissueTokenResponse(accessToken, refreshToken);
}
}
Loading