diff --git a/src/main/java/coffeemeet/server/auth/RefreshTokenRepository.java b/src/main/java/coffeemeet/server/auth/RefreshTokenRepository.java new file mode 100644 index 00000000..e9e53608 --- /dev/null +++ b/src/main/java/coffeemeet/server/auth/RefreshTokenRepository.java @@ -0,0 +1,8 @@ +package coffeemeet.server.auth; + +import coffeemeet.server.auth.domain.RefreshToken; +import org.springframework.data.repository.CrudRepository; + +public interface RefreshTokenRepository extends CrudRepository { + +} diff --git a/src/main/java/coffeemeet/server/auth/domain/RefreshToken.java b/src/main/java/coffeemeet/server/auth/domain/RefreshToken.java new file mode 100644 index 00000000..1b20301e --- /dev/null +++ b/src/main/java/coffeemeet/server/auth/domain/RefreshToken.java @@ -0,0 +1,23 @@ +package coffeemeet.server.auth.domain; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@Getter +@RedisHash(value = "refresh", timeToLive = 1209600) +public class RefreshToken { + + @Id + private Long userId; + + private String value; + + @Builder + private RefreshToken(Long userId, String value) { + this.userId = userId; + this.value = value; + } + +} diff --git a/src/main/java/coffeemeet/server/certification/controller/CertificationController.java b/src/main/java/coffeemeet/server/certification/controller/CertificationController.java index 59eb9616..33544eb7 100644 --- a/src/main/java/coffeemeet/server/certification/controller/CertificationController.java +++ b/src/main/java/coffeemeet/server/certification/controller/CertificationController.java @@ -3,10 +3,11 @@ import static org.springframework.http.HttpStatus.CREATED; import coffeemeet.server.certification.service.CertificationService; +import coffeemeet.server.common.annotation.Login; import coffeemeet.server.common.util.FileUtils; +import coffeemeet.server.user.dto.AuthInfo; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; @@ -21,17 +22,16 @@ public class CertificationController { private final CertificationService certificationService; - @PostMapping("/users/{userId}/business-card") + @PostMapping("/users/business-card") @ResponseStatus(CREATED) public void uploadBusinessCard( - @PathVariable("userId") - @NotNull(message = "유저 ID는 null일 수 없습니다.") - long userId, + @Login + AuthInfo authInfo, @RequestPart("businessCard") @NotNull(message = "명함 이미지는 null일 수 없습니다.") MultipartFile businessCard ) { - certificationService.uploadBusinessCard(userId, + certificationService.uploadBusinessCard(authInfo.userId(), FileUtils.convertMultipartFileToFile(businessCard)); } diff --git a/src/main/java/coffeemeet/server/common/UserArgumentResolver.java b/src/main/java/coffeemeet/server/common/UserArgumentResolver.java new file mode 100644 index 00000000..3fb8b07e --- /dev/null +++ b/src/main/java/coffeemeet/server/common/UserArgumentResolver.java @@ -0,0 +1,57 @@ +package coffeemeet.server.common; + +import coffeemeet.server.auth.RefreshTokenRepository; +import coffeemeet.server.auth.domain.RefreshToken; +import coffeemeet.server.auth.utils.JwtTokenProvider; +import coffeemeet.server.common.annotation.Login; +import coffeemeet.server.user.dto.AuthInfo; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class UserArgumentResolver implements HandlerMethodArgumentResolver { + + public static final String USER_AUTHENTICATION_FAILED_MESSAGE = "사용자(%s)의 갱신 토큰이 존재하지 않습니다."; + private static final String HEADER_AUTHENTICATION_FAILED_MESSAGE = "(%s)는 잘못된 권한 헤더입니다."; + + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenRepository refreshTokenRepository; + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(AuthInfo.class) && parameter.hasParameterAnnotation( + Login.class); + } + + @Override + public AuthInfo resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest(); + String authHeader = httpServletRequest.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + Long userId = jwtTokenProvider.extractUserId(token); + RefreshToken refreshToken = getRefreshToken(userId); + return new AuthInfo(userId, refreshToken.getValue()); + } + throw new IllegalArgumentException( + String.format(HEADER_AUTHENTICATION_FAILED_MESSAGE, authHeader) + ); + } + + private RefreshToken getRefreshToken(Long userId) { + return refreshTokenRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException(String.format( + USER_AUTHENTICATION_FAILED_MESSAGE, userId))); + } + +} diff --git a/src/main/java/coffeemeet/server/common/annotation/Login.java b/src/main/java/coffeemeet/server/common/annotation/Login.java new file mode 100644 index 00000000..7acf35b2 --- /dev/null +++ b/src/main/java/coffeemeet/server/common/annotation/Login.java @@ -0,0 +1,12 @@ +package coffeemeet.server.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Login { + +} diff --git a/src/main/java/coffeemeet/server/common/config/AuthWebConfig.java b/src/main/java/coffeemeet/server/common/config/AuthWebConfig.java index 38715672..43a234d2 100644 --- a/src/main/java/coffeemeet/server/common/config/AuthWebConfig.java +++ b/src/main/java/coffeemeet/server/common/config/AuthWebConfig.java @@ -1,10 +1,14 @@ package coffeemeet.server.common.config; +import coffeemeet.server.auth.RefreshTokenRepository; import coffeemeet.server.auth.utils.JwtTokenProvider; import coffeemeet.server.auth.utils.converter.OAuthProviderConverter; +import coffeemeet.server.common.UserArgumentResolver; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @@ -12,10 +16,16 @@ public class AuthWebConfig implements WebMvcConfigurer { private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenRepository refreshTokenRepository; @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new OAuthProviderConverter()); } + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new UserArgumentResolver(jwtTokenProvider, refreshTokenRepository)); + } + } diff --git a/src/main/java/coffeemeet/server/common/util/FileUtils.java b/src/main/java/coffeemeet/server/common/util/FileUtils.java index caf7f615..c0753eea 100644 --- a/src/main/java/coffeemeet/server/common/util/FileUtils.java +++ b/src/main/java/coffeemeet/server/common/util/FileUtils.java @@ -13,14 +13,6 @@ public final class FileUtils { private static final String MULTIPART_FILE_TRANSFER_ERROR = "MULTIPART FILE을 FILE로 변환 중 오류가 발생했습니다."; private static final String FILE_DELETE_ERROR = "FILE 삭제 중 오류가 발생했습니다."; - public static class FileIOException extends RuntimeException { - - public FileIOException(String message, Throwable e) { - super(message, e); - } - - } - public static File convertMultipartFileToFile(MultipartFile multipartFile) { try { File file = File.createTempFile("temp", multipartFile.getOriginalFilename()); @@ -39,4 +31,12 @@ public static void delete(File file) { } } + public static class FileIOException extends RuntimeException { + + public FileIOException(String message, Throwable e) { + super(message, e); + } + + } + } diff --git a/src/main/java/coffeemeet/server/user/dto/AuthInfo.java b/src/main/java/coffeemeet/server/user/dto/AuthInfo.java new file mode 100644 index 00000000..fac09343 --- /dev/null +++ b/src/main/java/coffeemeet/server/user/dto/AuthInfo.java @@ -0,0 +1,5 @@ +package coffeemeet.server.user.dto; + +public record AuthInfo(Long userId, String refreshToken) { + +} diff --git a/src/test/java/coffeemeet/server/common/config/ControllerTestConfig.java b/src/test/java/coffeemeet/server/common/config/ControllerTestConfig.java index f9f30ee2..32e9ec5b 100644 --- a/src/test/java/coffeemeet/server/common/config/ControllerTestConfig.java +++ b/src/test/java/coffeemeet/server/common/config/ControllerTestConfig.java @@ -3,6 +3,7 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import coffeemeet.server.auth.RefreshTokenRepository; import coffeemeet.server.auth.utils.JwtTokenProvider; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; @@ -27,6 +28,9 @@ public abstract class ControllerTestConfig { @MockBean protected JwtTokenProvider jwtTokenProvider; + @MockBean + protected RefreshTokenRepository refreshTokenRepository; + @BeforeEach void setUp(WebApplicationContext ctx, RestDocumentationContextProvider restDocumentation) { mockMvc = MockMvcBuilders.webAppContextSetup(ctx)