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
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ dependencies {
implementation group: 'com.google.http-client', name: 'google-http-client-jackson2', version: '1.25.0'
implementation group: 'com.google.collections', name: 'google-collections', version: '1.0'

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'

implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '4.1.3'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.recipe.app.src.common.client.apple.dto;

import com.recipe.app.src.user.domain.NicknameGenerator;
import com.recipe.app.src.user.domain.User;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -16,7 +17,7 @@ public User toEntity(String fcmToken) {

return User.builder()
.socialId("apple_" + sub)
.nickname(name != null ? name : "Apple User")
.nickname(NicknameGenerator.generate())
.email(email)
.deviceToken(fcmToken)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.recipe.app.src.common.client.google.dto;

import com.recipe.app.src.user.domain.NicknameGenerator;
import com.recipe.app.src.user.domain.User;
import lombok.Getter;

Expand All @@ -14,7 +15,7 @@ public User toEntity(String fcmToken) {

return User.builder()
.socialId("google_" + sub)
.nickname(name)
.nickname(NicknameGenerator.generate())
.email(email)
.deviceToken(fcmToken)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.recipe.app.src.common.client.kakao.dto;

import com.recipe.app.src.user.domain.NicknameGenerator;
import com.recipe.app.src.user.domain.User;
import lombok.Getter;

Expand All @@ -13,7 +14,7 @@ public User toEntity(String fcmToken) {

return User.builder()
.socialId("kakao_" + id)
.nickname(kakao_account.getNickname())
.nickname(NicknameGenerator.generate())
.email(kakao_account.getEmail())
.deviceToken(fcmToken)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.recipe.app.src.common.client.naver.dto;

import com.recipe.app.src.user.domain.NicknameGenerator;
import com.recipe.app.src.user.domain.User;
import lombok.Getter;

Expand All @@ -15,7 +16,7 @@ public User toEntity(String fcmToken) {

return User.builder()
.socialId("naver_" + id)
.nickname(name)
.nickname(NicknameGenerator.generate())
.email(email)
.phoneNumber(mobile)
.deviceToken(fcmToken)
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/com/recipe/app/src/common/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.recipe.app.src.common.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
public class CacheConfig {

/**
* Access Token 블랙리스트 전용 Caffeine Cache
* JWT exp와 동일하게 24시간 유지
*/
@Bean(name = "accessTokenBlacklistCache")
public com.github.benmanes.caffeine.cache.Cache<String, String> accessTokenBlacklistCache() {
return Caffeine.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS)
.maximumSize(10000)
.build();
}
}
30 changes: 0 additions & 30 deletions src/main/java/com/recipe/app/src/common/config/RedisConfig.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/fridges/**").authenticated()
.requestMatchers("/fridges/basket/**").authenticated()
.requestMatchers("/ingredients/**").authenticated()
.requestMatchers("/recipes/public/**").permitAll()
.requestMatchers("/recipes/**").authenticated()
.anyRequest().permitAll())
.addFilterBefore(new JwtFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class)
Expand Down
49 changes: 16 additions & 33 deletions src/main/java/com/recipe/app/src/common/utils/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
package com.recipe.app.src.common.utils;

import com.github.benmanes.caffeine.cache.Cache;
import com.recipe.app.src.common.client.apple.dto.ApplePublicKeyResponse;
import io.jsonwebtoken.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.SignatureException;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.time.Duration;
import java.util.Base64;
import java.util.Date;

@Service
public class JwtUtil {

private final RedisTemplate<String, String> redisTemplate;
private final Cache<String, String> accessTokenBlacklistCache;
private final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
private final static String TOKEN_KEY = "userId";
private final static String REFRESH_TOKEN_KEY_PREFIX = "refresh_token_user_id_";
private final static String ACCESS_TOKEN_BLACKLIST_VALUE = "access_token_blacklist";
private final static String TOKEN_HEADER = "Authorization";
@Value("${jwt.secret}")
Expand All @@ -38,8 +41,8 @@ public class JwtUtil {
@Value("${jwt.refresh-token-validity-in-ms}")
private long refreshTokenValidMillisecond;

public JwtUtil(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
public JwtUtil(@Qualifier("accessTokenBlacklistCache") Cache<String, String> accessTokenBlacklistCache) {
this.accessTokenBlacklistCache = accessTokenBlacklistCache;
}

public String createAccessToken(Long userId) {
Expand All @@ -60,16 +63,12 @@ public String createRefreshToken(Long userId) {
Date now = new Date();
Key key = new SecretKeySpec(Base64.getDecoder().decode(this.secretKey), SignatureAlgorithm.HS256.getJcaName());

String token = Jwts.builder()
return Jwts.builder()
.claim(TOKEN_KEY, userId)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + refreshTokenValidMillisecond))
.signWith(key)
.compact();

redisTemplate.opsForValue().set(REFRESH_TOKEN_KEY_PREFIX + userId, token, Duration.ofMillis(refreshTokenValidMillisecond));

return token;
}

public String resolveAccessToken(HttpServletRequest request) {
Expand All @@ -89,10 +88,9 @@ public long getUserId(String token) {
.get(TOKEN_KEY, Long.class);
}

@Transactional(readOnly = true)
public boolean isValidAccessToken(String accessToken) {

if (StringUtils.hasText(redisTemplate.opsForValue().get(accessToken))) {
if (accessTokenBlacklistCache.getIfPresent(accessToken) != null) {
return false;
}

Expand All @@ -101,15 +99,7 @@ public boolean isValidAccessToken(String accessToken) {

public boolean isValidRefreshToken(String refreshToken) {

if (isValidToken(refreshToken)) {

long userId = getUserId(refreshToken);
String foundRefreshToken = redisTemplate.opsForValue().get(REFRESH_TOKEN_KEY_PREFIX + userId);

return refreshToken.equals(foundRefreshToken);
}

return false;
return isValidToken(refreshToken);
}

private boolean isValidToken(String token) {
Expand All @@ -129,16 +119,9 @@ private boolean isValidToken(String token) {
return false;
}

@Transactional
public void removeRefreshToken(Long userId) {

redisTemplate.delete(REFRESH_TOKEN_KEY_PREFIX + userId);
}

@Transactional
public void setAccessTokenBlacklist(String accessToken) {

redisTemplate.opsForValue().set(accessToken, ACCESS_TOKEN_BLACKLIST_VALUE, Duration.ofMillis(accessTokenValidMillisecond));
accessTokenBlacklistCache.put(accessToken, ACCESS_TOKEN_BLACKLIST_VALUE);
}

public Claims parseAppleIdToken(String idToken, ApplePublicKeyResponse publicKey) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/recipe/app/src/file/S3FileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public S3FileService(AmazonS3 s3Client) {

public String uploadFile(MultipartFile file) throws IOException {

String fileName = UUID.randomUUID().toString() + "-" + file.getOriginalFilename();
String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(file.getContentType());
objectMetadata.setContentLength(file.getSize());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.recipe.app.src.recipe.api;

import com.recipe.app.src.recipe.application.RecipeSearchService;
import com.recipe.app.src.recipe.application.dto.RecipeDetailResponse;
import com.recipe.app.src.recipe.application.dto.RecommendedRecipesResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

@Tag(name = "공개 레시피 Controller")
@RestController
@RequestMapping("/recipes/public")
public class PublicRecipeController {

private final RecipeSearchService recipeSearchService;

public PublicRecipeController(RecipeSearchService recipeSearchService) {
this.recipeSearchService = recipeSearchService;
}

@Operation(summary = "공개 레시피 상세 조회 API (로그인 불필요)")
@GetMapping("/{recipeId}")
public RecipeDetailResponse getPublicRecipe(@PathVariable long recipeId) {

return recipeSearchService.findPublicRecipeDetail(recipeId);
}

@Operation(summary = "공개 추천 레시피 목록 조회 API (로그인 불필요)")
@GetMapping("/recommendation")
public RecommendedRecipesResponse getPublicRecipesByIngredients(
@Parameter(example = "감자,당근,양파", name = "재료 목록 (쉼표로 구분)")
@RequestParam(value = "ingredients") String ingredients,
@Parameter(example = "0", name = "마지막 조회 레시피 아이디")
@RequestParam(value = "startAfter", defaultValue = "0") long startAfter,
@Parameter(example = "20", name = "사이즈")
@RequestParam(value = "size", defaultValue = "20") int size) {

List<String> ingredientNames = Arrays.stream(ingredients.split(","))
.map(String::trim)
.toList();

return recipeSearchService.findPublicRecommendedRecipesByIngredients(ingredientNames, startAfter, size);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,38 @@ public RecommendedRecipesResponse findRecommendedRecipesByUserFridge(User user,

return RecommendedRecipesResponse.from(recipes, recipePostUsers, recipeScraps, user, ingredientNamesInFridge, lastRecipe, size);
}

@Transactional(readOnly = true)
public RecipeDetailResponse findPublicRecipeDetail(long recipeId) {

Recipe recipe = recipeRepository.findById(recipeId)
.orElseThrow(() -> {
throw new NotFoundRecipeException();
});

User postUser = null;
if (recipe.getUserId() != null) {
postUser = userService.findByUserId(recipe.getUserId());
}

List<String> emptyIngredientList = List.of();

return RecipeDetailResponse.from(recipe, false, postUser, emptyIngredientList);
}

@Transactional(readOnly = true)
public RecommendedRecipesResponse findPublicRecommendedRecipesByIngredients(List<String> ingredientNames, long lastRecipeId, int size) {

Recipes recipes = new Recipes(recipeRepository.findRecipesInFridge(ingredientNames));

List<User> recipePostUsers = userService.findByUserIds(recipes.getUserIds());

List<RecipeScrap> recipeScraps = recipeScrapService.findByRecipeIds(recipes.getRecipeIds());

Recipe lastRecipe = recipeRepository.findById(lastRecipeId).orElse(null);

User anonymousUser = new User();

return RecommendedRecipesResponse.from(recipes, recipePostUsers, recipeScraps, anonymousUser, ingredientNames, lastRecipe, size);
}
}
Loading
Loading