Skip to content

Commit

Permalink
Merge pull request #72 from TeamACON/feat/#71
Browse files Browse the repository at this point in the history
[FEAT/#71] 내 프로필 조회 API 구현
  • Loading branch information
gahyuun authored Feb 15, 2025
2 parents efcc2a1 + 6f5d031 commit 9dc1a6f
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 30 deletions.
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

0 comments on commit 9dc1a6f

Please sign in to comment.