From 7df66dc8c21842f92b7f1fc893071699730e16fb Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sat, 15 Nov 2025 10:02:44 +0900 Subject: [PATCH 01/16] =?UTF-8?q?build:=20spring-security,=20jwt,=20webcli?= =?UTF-8?q?ent=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f8208f3..4d5d1bb 100644 --- a/build.gradle +++ b/build.gradle @@ -31,13 +31,23 @@ dependencies { implementation 'com.mysql:mysql-connector-j' - // https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core + // geometry implementation 'org.hibernate.orm:hibernate-spatial:6.6.34.Final' - // https://mvnrepository.com/artifact/org.locationtech.jts/jts-core implementation 'org.locationtech.jts:jts-core:1.19.0' // Swagger-ui implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0" + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // JWT + implementation 'io.jsonwebtoken:jwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // WebClient + implementation 'org.springframework.boot:spring-boot-starter-webflux' } tasks.named('test') { From 7fa03468d380c98806c9fd0d67aab0b5a917c2b5 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 16 Nov 2025 12:19:23 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API=20=EB=B0=8F=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +-- .../common/dto/response/JwtResponse.java | 15 ++++++ .../user/controller/UserController.java | 24 +++++++++ .../photoliner/domain/user/model/User.java | 2 + .../user/repository/UserRepository.java | 8 +++ .../domain/user/service/UserService.java | 22 +++++++++ .../kakao/login/client/KakaoAuthClient.java | 44 +++++++++++++++++ .../request/KakaoAuthorizationRequest.java | 13 +++++ .../dto/request/KakaoOauthTokenRequest.java | 14 ++++++ .../dto/response/KakaoOauthTokenResponse.java | 15 ++++++ .../dto/response/KakaoProfileResponse.java | 27 ++++++++++ .../kakao/login/service/KakaoAuthService.java | 49 +++++++++++++++++++ .../db/migration/V3__alter_users_table.sql | 2 + 13 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 src/main/java/kr/kro/photoliner/common/dto/response/JwtResponse.java create mode 100644 src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoAuthorizationRequest.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoOauthTokenRequest.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoOauthTokenResponse.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoProfileResponse.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java create mode 100644 src/main/resources/db/migration/V3__alter_users_table.sql diff --git a/build.gradle b/build.gradle index 4d5d1bb..ae6cbad 100644 --- a/build.gradle +++ b/build.gradle @@ -42,9 +42,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' // JWT - implementation 'io.jsonwebtoken:jwt-api:0.11.5' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' +// implementation 'io.jsonwebtoken:jwt-api:0.13.0' +// runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0' +// runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0' // WebClient implementation 'org.springframework.boot:spring-boot-starter-webflux' diff --git a/src/main/java/kr/kro/photoliner/common/dto/response/JwtResponse.java b/src/main/java/kr/kro/photoliner/common/dto/response/JwtResponse.java new file mode 100644 index 0000000..68aaf84 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/common/dto/response/JwtResponse.java @@ -0,0 +1,15 @@ +package kr.kro.photoliner.common.dto.response; + +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; + +public record JwtResponse( + String accessToken, + String refreshToken +) { + public static JwtResponse from(KakaoOauthTokenResponse oauthTokenResponse) { + return new JwtResponse( + oauthTokenResponse.accessToken(), + oauthTokenResponse.refreshToken() + ); + } +} diff --git a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java index 86e5c1c..d381415 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java +++ b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java @@ -1,12 +1,36 @@ package kr.kro.photoliner.domain.user.controller; +import java.net.URI; +import kr.kro.photoliner.common.dto.response.JwtResponse; import kr.kro.photoliner.domain.user.service.UserService; +import kr.kro.photoliner.global.kakao.login.service.KakaoAuthService; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor +@RequestMapping("/api/v1/users") public class UserController { private final UserService userService; + private final KakaoAuthService kakaoAuthService; + + @GetMapping("/login/kakao") + public ResponseEntity login(@RequestParam(value = "code") String authorizationCode) { + return ResponseEntity + .ok(userService.getAccessToken(authorizationCode)); + } + + @GetMapping("/login/kakao/authorization") + public ResponseEntity authorize() { + return ResponseEntity + .status(HttpStatus.FOUND) + .location(URI.create(kakaoAuthService.getAuthorizationRedirectUrl())) + .build(); + } } diff --git a/src/main/java/kr/kro/photoliner/domain/user/model/User.java b/src/main/java/kr/kro/photoliner/domain/user/model/User.java index 2ecfffe..6e54a4a 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/model/User.java +++ b/src/main/java/kr/kro/photoliner/domain/user/model/User.java @@ -26,4 +26,6 @@ public class User extends BaseEntity { @Column(name = "name", nullable = false) private String name; + @Column(name = "email", nullable = false) + private String email; } diff --git a/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java b/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..17d0468 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java @@ -0,0 +1,8 @@ +package kr.kro.photoliner.domain.user.repository; + +import kr.kro.photoliner.domain.user.model.User; +import org.springframework.data.repository.Repository; + +public interface UserRepository extends Repository { + boolean existsByEmail(String email); +} diff --git a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java index 792b38c..25b42af 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java +++ b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java @@ -1,10 +1,32 @@ package kr.kro.photoliner.domain.user.service; +import kr.kro.photoliner.common.dto.response.JwtResponse; +import kr.kro.photoliner.domain.user.repository.UserRepository; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; +import kr.kro.photoliner.global.kakao.login.service.KakaoAuthService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class UserService { + private final UserRepository userRepository; + private final KakaoAuthService kakaoAuthService; + public JwtResponse getAccessToken(String authorizationCode) { + KakaoOauthTokenResponse tokenResponse = kakaoAuthService.getTokenByAuthorizationCode(authorizationCode); + KakaoProfileResponse.InnerKakaoAccount account = kakaoAuthService.getKakaoUserProfile( + tokenResponse.accessToken()); + + if (!userRepository.existsByEmail(account.email())) { + signup(); + } + + return JwtResponse.from(tokenResponse); + } + + private void signup() { + + } } diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java new file mode 100644 index 0000000..f33ed9a --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java @@ -0,0 +1,44 @@ +package kr.kro.photoliner.global.kakao.login.client; + +import java.nio.charset.StandardCharsets; +import kr.kro.photoliner.global.kakao.login.dto.request.KakaoOauthTokenRequest; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +@Component +public class KakaoAuthClient { + private static final String BEARER_TOKEN_PREFIX = "Bearer "; + private static final String GET_OAUTH_TOKEN_URL = "https://kauth.kakao.com/oauth/token"; + private static final String GET_USER_PROFILE_URL = "https://kapi.kakao.com/v2/user/me"; + + + public WebClient getWebClient(String baseUrl) { + return WebClient.builder() + .baseUrl(baseUrl) + .build(); + } + + public KakaoOauthTokenResponse getOauthToken(KakaoOauthTokenRequest request) { + return getWebClient(GET_OAUTH_TOKEN_URL).post() + .uri(ub -> ub.path("/oauth/token").build()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .acceptCharset(StandardCharsets.UTF_8) + .bodyValue(request) + .retrieve() + .bodyToMono(KakaoOauthTokenResponse.class) + .block(); + } + + public KakaoProfileResponse getKakaoUserProfile(String accessToken) { + return getWebClient(GET_USER_PROFILE_URL).get() + .header("Authorization", BEARER_TOKEN_PREFIX + accessToken) + .retrieve() + .bodyToMono(KakaoProfileResponse.class) + .block(); + } + + +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoAuthorizationRequest.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoAuthorizationRequest.java new file mode 100644 index 0000000..3ac57e7 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoAuthorizationRequest.java @@ -0,0 +1,13 @@ +package kr.kro.photoliner.global.kakao.login.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(SnakeCaseStrategy.class) +public record KakaoAuthorizationRequest( + String code, + String error, + String errorDescription, + String state +) { +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoOauthTokenRequest.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoOauthTokenRequest.java new file mode 100644 index 0000000..f7d2588 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoOauthTokenRequest.java @@ -0,0 +1,14 @@ +package kr.kro.photoliner.global.kakao.login.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(SnakeCaseStrategy.class) +public record KakaoOauthTokenRequest( + String grantType, + String clientId, + String redirectUri, + String code, + String clientSecret +) { +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoOauthTokenResponse.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoOauthTokenResponse.java new file mode 100644 index 0000000..e8ffbf0 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoOauthTokenResponse.java @@ -0,0 +1,15 @@ +package kr.kro.photoliner.global.kakao.login.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(SnakeCaseStrategy.class) +public record KakaoOauthTokenResponse( + String tokenType, + String accessToken, + String idToken, + Integer expiresIn, + String refreshToken, + String scope +) { +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoProfileResponse.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoProfileResponse.java new file mode 100644 index 0000000..883620a --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoProfileResponse.java @@ -0,0 +1,27 @@ +package kr.kro.photoliner.global.kakao.login.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(SnakeCaseStrategy.class) +public record KakaoProfileResponse( + Long id, + InnerKakaoAccount kakaoAccount +) { + public record InnerKakaoAccount( + InnerKakaoProfile profile, + String name, + Boolean isEmailValid, + Boolean isEmailVerified, + String email + ) { + public record InnerKakaoProfile( + String nickname, + String thumbnailImageUrl, + String profileImageUrl, + Boolean isDefaultImage + ) { + + } + } +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java new file mode 100644 index 0000000..1bdda8b --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java @@ -0,0 +1,49 @@ +package kr.kro.photoliner.global.kakao.login.service; + +import kr.kro.photoliner.global.kakao.login.client.KakaoAuthClient; +import kr.kro.photoliner.global.kakao.login.dto.request.KakaoOauthTokenRequest; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class KakaoAuthService { + private final static String DEFAULT_GRANT_TYPE = "authorization_code"; + private final static String AUTHORIZATION_REDIRECT_URI = "https://kauth.kakao.com/oauth/authorize"; + private final KakaoAuthClient kakaoAuthClient; + + private String restApiKey; + private String redirectUri; + + public String getAuthorizationRedirectUrl() { + return createAuthorizationRedirectUri(restApiKey, redirectUri); + } + + public KakaoOauthTokenResponse getTokenByAuthorizationCode(String authorizationCode) { + return kakaoAuthClient.getOauthToken( + new KakaoOauthTokenRequest( + DEFAULT_GRANT_TYPE, + restApiKey, + redirectUri, + authorizationCode, + null + ) + ); + } + + public KakaoProfileResponse.InnerKakaoAccount getKakaoUserProfile(String accessToken) { + return kakaoAuthClient + .getKakaoUserProfile(accessToken) + .kakaoAccount(); + } + + + private String createAuthorizationRedirectUri(String restApiKey, String redirectUri) { + return AUTHORIZATION_REDIRECT_URI + + "?response_type=code" + + "&client_id=" + restApiKey + + "&redirect_uri=" + redirectUri; + } +} diff --git a/src/main/resources/db/migration/V3__alter_users_table.sql b/src/main/resources/db/migration/V3__alter_users_table.sql new file mode 100644 index 0000000..4496cb7 --- /dev/null +++ b/src/main/resources/db/migration/V3__alter_users_table.sql @@ -0,0 +1,2 @@ +alter table users + add column email varchar(30) not null From cd1afaab6bea3f8e13e847b4db02278d6c0af102 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 16 Nov 2025 12:20:41 +0900 Subject: [PATCH 03/16] =?UTF-8?q?fix(KakaoAuthClient):=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20API=20=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20URL=20path=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photoliner/global/kakao/login/client/KakaoAuthClient.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java index f33ed9a..cf9bfad 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java @@ -23,7 +23,6 @@ public WebClient getWebClient(String baseUrl) { public KakaoOauthTokenResponse getOauthToken(KakaoOauthTokenRequest request) { return getWebClient(GET_OAUTH_TOKEN_URL).post() - .uri(ub -> ub.path("/oauth/token").build()) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .acceptCharset(StandardCharsets.UTF_8) .bodyValue(request) From dddafa02e3120997a388dbd76fe8ea380505df7c Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 16 Nov 2025 15:17:13 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor(KakaoApiUrlConstant):=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20API=20Url=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kakao/login/client/KakaoAuthClient.java | 22 +++++++++---------- .../login/constant/KakaoApiUrlConstant.java | 7 ++++++ .../kakao/login/service/KakaoAuthService.java | 4 ++-- 3 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java index cf9bfad..7e64275 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java @@ -1,6 +1,7 @@ package kr.kro.photoliner.global.kakao.login.client; import java.nio.charset.StandardCharsets; +import kr.kro.photoliner.global.kakao.login.constant.KakaoApiUrlConstant; import kr.kro.photoliner.global.kakao.login.dto.request.KakaoOauthTokenRequest; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; @@ -11,18 +12,10 @@ @Component public class KakaoAuthClient { private static final String BEARER_TOKEN_PREFIX = "Bearer "; - private static final String GET_OAUTH_TOKEN_URL = "https://kauth.kakao.com/oauth/token"; - private static final String GET_USER_PROFILE_URL = "https://kapi.kakao.com/v2/user/me"; - - - public WebClient getWebClient(String baseUrl) { - return WebClient.builder() - .baseUrl(baseUrl) - .build(); - } public KakaoOauthTokenResponse getOauthToken(KakaoOauthTokenRequest request) { - return getWebClient(GET_OAUTH_TOKEN_URL).post() + return getWebClient(KakaoApiUrlConstant.GET_OAUTH_TOKEN_URL) + .post() .contentType(MediaType.APPLICATION_FORM_URLENCODED) .acceptCharset(StandardCharsets.UTF_8) .bodyValue(request) @@ -32,12 +25,17 @@ public KakaoOauthTokenResponse getOauthToken(KakaoOauthTokenRequest request) { } public KakaoProfileResponse getKakaoUserProfile(String accessToken) { - return getWebClient(GET_USER_PROFILE_URL).get() + return getWebClient(KakaoApiUrlConstant.GET_USER_PROFILE) + .get() .header("Authorization", BEARER_TOKEN_PREFIX + accessToken) .retrieve() .bodyToMono(KakaoProfileResponse.class) .block(); } - + private WebClient getWebClient(String baseUrl) { + return WebClient.builder() + .baseUrl(baseUrl) + .build(); + } } diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java b/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java new file mode 100644 index 0000000..5fd92c2 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java @@ -0,0 +1,7 @@ +package kr.kro.photoliner.global.kakao.login.constant; + +public final class KakaoApiUrlConstant { + public static final String AUTHORIZATION_REDIRECT_URL = "https://kauth.kakao.com/oauth/authorize"; + public static final String GET_OAUTH_TOKEN_URL = "https://kauth.kakao.com/oauth/token"; + public static final String GET_USER_PROFILE = "https://kapi.kakao.com/v2/user/me"; +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java index 1bdda8b..dd93f90 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java @@ -1,6 +1,7 @@ package kr.kro.photoliner.global.kakao.login.service; import kr.kro.photoliner.global.kakao.login.client.KakaoAuthClient; +import kr.kro.photoliner.global.kakao.login.constant.KakaoApiUrlConstant; import kr.kro.photoliner.global.kakao.login.dto.request.KakaoOauthTokenRequest; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; @@ -11,7 +12,6 @@ @RequiredArgsConstructor public class KakaoAuthService { private final static String DEFAULT_GRANT_TYPE = "authorization_code"; - private final static String AUTHORIZATION_REDIRECT_URI = "https://kauth.kakao.com/oauth/authorize"; private final KakaoAuthClient kakaoAuthClient; private String restApiKey; @@ -41,7 +41,7 @@ public KakaoProfileResponse.InnerKakaoAccount getKakaoUserProfile(String accessT private String createAuthorizationRedirectUri(String restApiKey, String redirectUri) { - return AUTHORIZATION_REDIRECT_URI + return KakaoApiUrlConstant.AUTHORIZATION_REDIRECT_URL + "?response_type=code" + "&client_id=" + restApiKey + "&redirect_uri=" + redirectUri; From 5b41a8808d61ca778ac8b02ff577903b8539f7e3 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 16 Nov 2025 15:22:26 +0900 Subject: [PATCH 05/16] =?UTF-8?q?fix:=20yml=EC=97=90=EC=84=9C=20key?= =?UTF-8?q?=EA=B0=92=20=EB=B0=9B=EC=95=84=EC=98=AC=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/kakao/login/service/KakaoAuthService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java index dd93f90..d9ceaec 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java @@ -6,6 +6,7 @@ import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @@ -14,7 +15,9 @@ public class KakaoAuthService { private final static String DEFAULT_GRANT_TYPE = "authorization_code"; private final KakaoAuthClient kakaoAuthClient; + @Value("${kakao.api.rest-api-key}") private String restApiKey; + @Value("${kakao.api.login-oauth.redirect-url}") private String redirectUri; public String getAuthorizationRedirectUrl() { From b5cab08abb11dc082a1b812349a30aef125799e2 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 16 Nov 2025 15:38:47 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photoliner/domain/user/model/User.java | 20 +++++++++++++++++-- .../user/repository/UserRepository.java | 2 ++ .../domain/user/service/UserService.java | 11 +++++----- .../kakao/login/service/KakaoAuthService.java | 5 ++--- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/user/model/User.java b/src/main/java/kr/kro/photoliner/domain/user/model/User.java index 6e54a4a..06fd57b 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/model/User.java +++ b/src/main/java/kr/kro/photoliner/domain/user/model/User.java @@ -7,13 +7,15 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import kr.kro.photoliner.common.model.BaseEntity; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; +import lombok.AccessLevel; import lombok.Getter; -import lombok.Setter; +import lombok.NoArgsConstructor; @Entity @Table(name = "users") @Getter -@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class User extends BaseEntity { @Id @@ -28,4 +30,18 @@ public class User extends BaseEntity { @Column(name = "email", nullable = false) private String email; + + public User(String username, String name, String email) { + this.username = username; + this.name = name; + this.email = email; + } + + public static User from(KakaoProfileResponse profileResponse) { + return new User( + profileResponse.id().toString(), + profileResponse.kakaoAccount().profile().nickname(), + profileResponse.kakaoAccount().email() + ); + } } diff --git a/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java b/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java index 17d0468..3b75ca5 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java +++ b/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java @@ -5,4 +5,6 @@ public interface UserRepository extends Repository { boolean existsByEmail(String email); + + User save(User user); } diff --git a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java index 25b42af..504f913 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java +++ b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java @@ -1,6 +1,7 @@ package kr.kro.photoliner.domain.user.service; import kr.kro.photoliner.common.dto.response.JwtResponse; +import kr.kro.photoliner.domain.user.model.User; import kr.kro.photoliner.domain.user.repository.UserRepository; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; @@ -16,17 +17,17 @@ public class UserService { public JwtResponse getAccessToken(String authorizationCode) { KakaoOauthTokenResponse tokenResponse = kakaoAuthService.getTokenByAuthorizationCode(authorizationCode); - KakaoProfileResponse.InnerKakaoAccount account = kakaoAuthService.getKakaoUserProfile( + KakaoProfileResponse profileResponse = kakaoAuthService.getKakaoUserProfile( tokenResponse.accessToken()); - if (!userRepository.existsByEmail(account.email())) { - signup(); + if (!userRepository.existsByEmail(profileResponse.kakaoAccount().email())) { + signup(User.from(profileResponse)); } return JwtResponse.from(tokenResponse); } - private void signup() { - + private void signup(User user) { + userRepository.save(user); } } diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java index d9ceaec..079f95b 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java @@ -36,10 +36,9 @@ public KakaoOauthTokenResponse getTokenByAuthorizationCode(String authorizationC ); } - public KakaoProfileResponse.InnerKakaoAccount getKakaoUserProfile(String accessToken) { + public KakaoProfileResponse getKakaoUserProfile(String accessToken) { return kakaoAuthClient - .getKakaoUserProfile(accessToken) - .kakaoAccount(); + .getKakaoUserProfile(accessToken); } From a3650873d3a3170c952226e2610d89ff16265232 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Mon, 17 Nov 2025 20:18:01 +0900 Subject: [PATCH 07/16] =?UTF-8?q?style:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95:=20getAccessToekn=20->=20oAuthLo?= =?UTF-8?q?gin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kro/photoliner/domain/user/controller/UserController.java | 2 +- .../java/kr/kro/photoliner/domain/user/service/UserService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java index d381415..d6bf84e 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java +++ b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java @@ -23,7 +23,7 @@ public class UserController { @GetMapping("/login/kakao") public ResponseEntity login(@RequestParam(value = "code") String authorizationCode) { return ResponseEntity - .ok(userService.getAccessToken(authorizationCode)); + .ok(userService.oAuthLogin(authorizationCode)); } @GetMapping("/login/kakao/authorization") diff --git a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java index 504f913..efac839 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java +++ b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java @@ -15,7 +15,7 @@ public class UserService { private final UserRepository userRepository; private final KakaoAuthService kakaoAuthService; - public JwtResponse getAccessToken(String authorizationCode) { + public JwtResponse oAuthLogin(String authorizationCode) { KakaoOauthTokenResponse tokenResponse = kakaoAuthService.getTokenByAuthorizationCode(authorizationCode); KakaoProfileResponse profileResponse = kakaoAuthService.getKakaoUserProfile( tokenResponse.accessToken()); From 4594989f7d834b7ef5aabaf88ce8ad16eb89da93 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Mon, 17 Nov 2025 20:21:11 +0900 Subject: [PATCH 08/16] =?UTF-8?q?style:=20createAuthorizationRedirectUri?= =?UTF-8?q?=20=EB=A7=A4=EA=B0=9C=EB=B3=80=EC=88=98=EB=AA=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/kakao/login/service/KakaoAuthService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java index 079f95b..9b4ecf3 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java @@ -42,10 +42,10 @@ public KakaoProfileResponse getKakaoUserProfile(String accessToken) { } - private String createAuthorizationRedirectUri(String restApiKey, String redirectUri) { + private String createAuthorizationRedirectUri(String restApiKey, String redirectUrlWhenComplete) { return KakaoApiUrlConstant.AUTHORIZATION_REDIRECT_URL + "?response_type=code" + "&client_id=" + restApiKey - + "&redirect_uri=" + redirectUri; + + "&redirect_uri=" + redirectUrlWhenComplete; } } From 43cd6ef32a389d1efb8ca9c1800f3b78e4175b07 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 23 Nov 2025 10:41:00 +0900 Subject: [PATCH 09/16] =?UTF-8?q?build:=20weblcient=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../kro/photoliner/domain/user/model/User.java | 18 ++++++++++++++++++ .../domain/user/repository/UserRepository.java | 3 +++ .../resources/application-local-example.yml | 7 +++++++ ...ers_table.sql => V9__alter_users_table.sql} | 0 5 files changed, 31 insertions(+) rename src/main/resources/db/migration/{V3__alter_users_table.sql => V9__alter_users_table.sql} (100%) diff --git a/build.gradle b/build.gradle index 7d21ce8..0f08de7 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,9 @@ dependencies { // 썸네일 라이브러리 implementation 'net.coobird:thumbnailator:0.4.20' + + // WebClient + implementation 'org.springframework.boot:spring-boot-starter-webflux' } tasks.named('test') { diff --git a/src/main/java/kr/kro/photoliner/domain/user/model/User.java b/src/main/java/kr/kro/photoliner/domain/user/model/User.java index 1689729..be795b6 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/model/User.java +++ b/src/main/java/kr/kro/photoliner/domain/user/model/User.java @@ -7,6 +7,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import kr.kro.photoliner.common.model.BaseEntity; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -30,4 +31,21 @@ public class User extends BaseEntity { @Column(name = "name", nullable = false) private String name; + @Column(name = "email", nullable = false) + private String email; + + public User(String username, String name, String email) { + this.username = username; + this.name = name; + this.email = email; + } + + public static User from(KakaoProfileResponse profileResponse) { + return new User( + profileResponse.id().toString(), + profileResponse.kakaoAccount().profile().nickname(), + profileResponse.kakaoAccount().email() + ); + } + } diff --git a/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java b/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java index ab06bd9..7f5703c 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java +++ b/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java @@ -5,6 +5,9 @@ import org.springframework.data.repository.Repository; public interface UserRepository extends Repository { + boolean existsByEmail(String email); + User save(User user); + Optional findUserById(Long userId); } diff --git a/src/main/resources/application-local-example.yml b/src/main/resources/application-local-example.yml index cad07ec..27de4e1 100644 --- a/src/main/resources/application-local-example.yml +++ b/src/main/resources/application-local-example.yml @@ -11,3 +11,10 @@ photo: cors: allow-url: url + + +kakao: + api: + rest-api-key: + login-oauth: + redirect-url: http://localhost:8080/api/v1/login/kakao diff --git a/src/main/resources/db/migration/V3__alter_users_table.sql b/src/main/resources/db/migration/V9__alter_users_table.sql similarity index 100% rename from src/main/resources/db/migration/V3__alter_users_table.sql rename to src/main/resources/db/migration/V9__alter_users_table.sql From eb3e143ece20f2b2417e7e64a9f8ff5094cf7cba Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 23 Nov 2025 12:56:17 +0900 Subject: [PATCH 10/16] =?UTF-8?q?fix:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/controller/UserController.java | 10 +++++++++- .../global/kakao/login/client/KakaoAuthClient.java | 14 +++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java index d6bf84e..5e21597 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java +++ b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java @@ -19,11 +19,19 @@ public class UserController { private final UserService userService; private final KakaoAuthService kakaoAuthService; + private static final String LOGIN_REDIRECT_URL = "http://localhost:5173/login/kakao"; @GetMapping("/login/kakao") public ResponseEntity login(@RequestParam(value = "code") String authorizationCode) { + JwtResponse jwtResponse = userService.oAuthLogin(authorizationCode); + + String redirectUrl = LOGIN_REDIRECT_URL + "#accessToken=" + jwtResponse.accessToken() + "&refreshToken=" + + jwtResponse.refreshToken(); + return ResponseEntity - .ok(userService.oAuthLogin(authorizationCode)); + .status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); } @GetMapping("/login/kakao/authorization") diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java index 7e64275..0475ba7 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java @@ -7,6 +7,9 @@ import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; @Component @@ -14,11 +17,20 @@ public class KakaoAuthClient { private static final String BEARER_TOKEN_PREFIX = "Bearer "; public KakaoOauthTokenResponse getOauthToken(KakaoOauthTokenRequest request) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", request.grantType()); + formData.add("client_id", request.clientId()); + formData.add("redirect_uri", request.redirectUri()); + formData.add("code", request.code()); + formData.add("client_secret", request.clientSecret()); + + System.out.println(formData); + return getWebClient(KakaoApiUrlConstant.GET_OAUTH_TOKEN_URL) .post() .contentType(MediaType.APPLICATION_FORM_URLENCODED) .acceptCharset(StandardCharsets.UTF_8) - .bodyValue(request) + .body(BodyInserters.fromFormData(formData)) .retrieve() .bodyToMono(KakaoOauthTokenResponse.class) .block(); From 47549aa8bb9212381fceebab6f8632e8d96eea4b Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 23 Nov 2025 16:45:29 +0900 Subject: [PATCH 11/16] =?UTF-8?q?feat:=20JWT=ED=86=A0=ED=81=B0=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 + .../common/dto/response/JwtResponse.java | 12 +-- .../user/controller/UserController.java | 3 +- .../dto/request/UserRefreshTokenRequest.java | 12 +++ .../user/repository/UserRepository.java | 4 +- .../domain/user/service/UserService.java | 13 +-- .../photoliner/global/auth/AuthContext.java | 24 +++++ .../auth/AuthenticationInterceptor.java | 29 ++++++ .../photoliner/global/auth/JwtProvider.java | 95 +++++++++++++++++++ .../global/code/ApiResponseCode.java | 5 +- .../kakao/login/client/KakaoAuthClient.java | 34 ++++++- .../login/constant/KakaoApiUrlConstant.java | 5 +- .../dto/request/KakaoTokenRefreshRequest.java | 13 +++ .../KakaoAccessTokenInfoResponse.java | 12 +++ .../response/KakaoTokenRefreshResponse.java | 15 +++ .../kakao/login/service/KakaoAuthService.java | 6 +- 16 files changed, 261 insertions(+), 26 deletions(-) create mode 100644 src/main/java/kr/kro/photoliner/domain/user/dto/request/UserRefreshTokenRequest.java create mode 100644 src/main/java/kr/kro/photoliner/global/auth/AuthContext.java create mode 100644 src/main/java/kr/kro/photoliner/global/auth/AuthenticationInterceptor.java create mode 100644 src/main/java/kr/kro/photoliner/global/auth/JwtProvider.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoTokenRefreshRequest.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoAccessTokenInfoResponse.java create mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoTokenRefreshResponse.java diff --git a/build.gradle b/build.gradle index 0f08de7..c4451f7 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,11 @@ dependencies { // WebClient implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' } tasks.named('test') { diff --git a/src/main/java/kr/kro/photoliner/common/dto/response/JwtResponse.java b/src/main/java/kr/kro/photoliner/common/dto/response/JwtResponse.java index 68aaf84..da49bd6 100644 --- a/src/main/java/kr/kro/photoliner/common/dto/response/JwtResponse.java +++ b/src/main/java/kr/kro/photoliner/common/dto/response/JwtResponse.java @@ -1,15 +1,7 @@ package kr.kro.photoliner.common.dto.response; -import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; - public record JwtResponse( - String accessToken, - String refreshToken + String accessToken ) { - public static JwtResponse from(KakaoOauthTokenResponse oauthTokenResponse) { - return new JwtResponse( - oauthTokenResponse.accessToken(), - oauthTokenResponse.refreshToken() - ); - } + } diff --git a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java index 5e21597..44478b8 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java +++ b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java @@ -25,8 +25,7 @@ public class UserController { public ResponseEntity login(@RequestParam(value = "code") String authorizationCode) { JwtResponse jwtResponse = userService.oAuthLogin(authorizationCode); - String redirectUrl = LOGIN_REDIRECT_URL + "#accessToken=" + jwtResponse.accessToken() + "&refreshToken=" - + jwtResponse.refreshToken(); + String redirectUrl = LOGIN_REDIRECT_URL + "#accessToken=" + jwtResponse.accessToken(); return ResponseEntity .status(HttpStatus.FOUND) diff --git a/src/main/java/kr/kro/photoliner/domain/user/dto/request/UserRefreshTokenRequest.java b/src/main/java/kr/kro/photoliner/domain/user/dto/request/UserRefreshTokenRequest.java new file mode 100644 index 0000000..88b91a2 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/user/dto/request/UserRefreshTokenRequest.java @@ -0,0 +1,12 @@ +package kr.kro.photoliner.domain.user.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.validation.constraints.NotNull; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record UserRefreshTokenRequest( + @NotNull(message = "refresh token 을 입력해주세요.") + String refreshToken +) { +} diff --git a/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java b/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java index 7f5703c..ac2062c 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java +++ b/src/main/java/kr/kro/photoliner/domain/user/repository/UserRepository.java @@ -8,6 +8,8 @@ public interface UserRepository extends Repository { boolean existsByEmail(String email); User save(User user); - + Optional findUserById(Long userId); + + Optional findUserByEmail(String email); } diff --git a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java index efac839..585f232 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java +++ b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java @@ -3,6 +3,7 @@ import kr.kro.photoliner.common.dto.response.JwtResponse; import kr.kro.photoliner.domain.user.model.User; import kr.kro.photoliner.domain.user.repository.UserRepository; +import kr.kro.photoliner.global.auth.JwtProvider; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; import kr.kro.photoliner.global.kakao.login.service.KakaoAuthService; @@ -14,20 +15,20 @@ public class UserService { private final UserRepository userRepository; private final KakaoAuthService kakaoAuthService; + private final JwtProvider jwtProvider; public JwtResponse oAuthLogin(String authorizationCode) { KakaoOauthTokenResponse tokenResponse = kakaoAuthService.getTokenByAuthorizationCode(authorizationCode); KakaoProfileResponse profileResponse = kakaoAuthService.getKakaoUserProfile( tokenResponse.accessToken()); - if (!userRepository.existsByEmail(profileResponse.kakaoAccount().email())) { - signup(User.from(profileResponse)); - } + User user = userRepository.findUserByEmail(profileResponse.kakaoAccount().email()) + .orElse(signup(User.from(profileResponse))); - return JwtResponse.from(tokenResponse); + return new JwtResponse(jwtProvider.createAccessToken(user)); } - private void signup(User user) { - userRepository.save(user); + private User signup(User user) { + return userRepository.save(user); } } diff --git a/src/main/java/kr/kro/photoliner/global/auth/AuthContext.java b/src/main/java/kr/kro/photoliner/global/auth/AuthContext.java new file mode 100644 index 0000000..349c0ac --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/auth/AuthContext.java @@ -0,0 +1,24 @@ +package kr.kro.photoliner.global.auth; + +import kr.kro.photoliner.global.code.ApiResponseCode; +import kr.kro.photoliner.global.exception.CustomException; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +public class AuthContext { + + private Long userId; + + public Long getUserId() { + if (userId == null) { + throw CustomException.of(ApiResponseCode.NOT_FOUND_USER, "userId is null"); + } + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } +} diff --git a/src/main/java/kr/kro/photoliner/global/auth/AuthenticationInterceptor.java b/src/main/java/kr/kro/photoliner/global/auth/AuthenticationInterceptor.java new file mode 100644 index 0000000..43a7f6b --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/auth/AuthenticationInterceptor.java @@ -0,0 +1,29 @@ +package kr.kro.photoliner.global.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@RequiredArgsConstructor +public class AuthenticationInterceptor implements HandlerInterceptor { + + private final JwtProvider jwtProvider; + private final AuthContext authContext; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String accessToken = jwtProvider.extractAccessToken(request); + if (accessToken == null) { + return false; + } + jwtProvider.validateToken(accessToken); + Long userId = jwtProvider.getUserId(accessToken); + authContext.setUserId(userId); + return true; + } + + +} diff --git a/src/main/java/kr/kro/photoliner/global/auth/JwtProvider.java b/src/main/java/kr/kro/photoliner/global/auth/JwtProvider.java new file mode 100644 index 0000000..39ed953 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/auth/JwtProvider.java @@ -0,0 +1,95 @@ +package kr.kro.photoliner.global.auth; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import java.time.Instant; +import java.util.Base64; +import java.util.Date; +import javax.crypto.SecretKey; +import kr.kro.photoliner.domain.user.model.User; +import kr.kro.photoliner.global.code.ApiResponseCode; +import kr.kro.photoliner.global.exception.CustomException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class JwtProvider { + + private static final String BEARER_TYPE = "Bearer "; + private static final int BEARER_TYPE_LEN = 7; + + private final String secretKey; + private final Long accessTokenExpirationTime; + + public JwtProvider( + @Value("${jwt.secret-key}") String secretKey, + @Value("${jwt.access-token.expiration-time}") Long accessTokenExpirationTime + ) { + this.secretKey = secretKey; + this.accessTokenExpirationTime = accessTokenExpirationTime; + } + + public String extractAccessToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_TYPE)) { + return bearerToken.substring(BEARER_TYPE_LEN); + } + return null; + } + + public Long getUserId(String token) { + try { + String userId = Jwts.parser() + .verifyWith(getSecretKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .get("id") + .toString(); + return Long.parseLong(userId); + } catch (JwtException e) { + throw CustomException.of(ApiResponseCode.TOKEN_PARSE_ERROR); + } + } + + public void validateToken(String token) { + try { + Jwts.parser().setSigningKey(secretKey).build().parseClaimsJws(token); + } catch (io.jsonwebtoken.security.SignatureException | MalformedJwtException e) { + throw CustomException.of(ApiResponseCode.INVALID_JWT_TOKEN); + } catch (ExpiredJwtException e) { + throw CustomException.of(ApiResponseCode.EXPIRED_JWT_TOKEN); + } catch (UnsupportedJwtException | IllegalArgumentException e) { + throw CustomException.of(ApiResponseCode.TOKEN_PARSE_ERROR); + } + } + + public String createAccessToken(User user) { + if (user == null) { + throw CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user: " + null); + } + SecretKey key = getSecretKey(); + return Jwts.builder() + .signWith(key) + .header() + .add("typ", "JWT") + .add("alg", key.getAlgorithm()) + .and() + .claim("id", user.getId()) + .expiration(Date.from(Instant.now().plusMillis(accessTokenExpirationTime))) + .compact(); + } + + private SecretKey getSecretKey() { + String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes()); + return Keys.hmacShaKeyFor(encoded.getBytes()); + } +} diff --git a/src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java b/src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java index f783982..11ee644 100644 --- a/src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java +++ b/src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java @@ -29,6 +29,8 @@ public enum ApiResponseCode { * 401 Unauthorized (인증 필요) */ WITHDRAWN_USER(HttpStatus.UNAUTHORIZED, "탈퇴한 계정입니다."), + EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), + INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다."), /** * 403 Forbidden (인가 필요) @@ -63,7 +65,8 @@ public enum ApiResponseCode { FILE_STORE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 저장 중 오류가 발생했습니다."), FILE_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 생성 중 오류가 발생했습니다."), DIRECTORY_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "폴더 생성 중 오류가 발생했습니다."), - FILE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제 중 오류가 발생했습니다."); + FILE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제 중 오류가 발생했습니다."), + TOKEN_PARSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "토큰 파싱 중 오류가 발생했습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java index 0475ba7..eb4c215 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java @@ -3,8 +3,11 @@ import java.nio.charset.StandardCharsets; import kr.kro.photoliner.global.kakao.login.constant.KakaoApiUrlConstant; import kr.kro.photoliner.global.kakao.login.dto.request.KakaoOauthTokenRequest; +import kr.kro.photoliner.global.kakao.login.dto.request.KakaoTokenRefreshRequest; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoAccessTokenInfoResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; +import kr.kro.photoliner.global.kakao.login.dto.response.KakaoTokenRefreshResponse; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; @@ -24,9 +27,7 @@ public KakaoOauthTokenResponse getOauthToken(KakaoOauthTokenRequest request) { formData.add("code", request.code()); formData.add("client_secret", request.clientSecret()); - System.out.println(formData); - - return getWebClient(KakaoApiUrlConstant.GET_OAUTH_TOKEN_URL) + return getWebClient(KakaoApiUrlConstant.OAUTH_TOKEN) .post() .contentType(MediaType.APPLICATION_FORM_URLENCODED) .acceptCharset(StandardCharsets.UTF_8) @@ -45,6 +46,33 @@ public KakaoProfileResponse getKakaoUserProfile(String accessToken) { .block(); } + public KakaoAccessTokenInfoResponse verifyAccessToken(String accessToken) { + return getWebClient(KakaoApiUrlConstant.VERIFY_ACCESS_TOKEN) + .get() + .header("Authorization", BEARER_TOKEN_PREFIX + accessToken) + .retrieve() + .bodyToMono(KakaoAccessTokenInfoResponse.class) + .block(); + } + + public KakaoTokenRefreshResponse refreshToken(KakaoTokenRefreshRequest request) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", request.grantType()); + formData.add("client_id", request.clientId()); + formData.add("refresh_token", request.refreshToken()); + formData.add("client_secret", request.clientSecret()); + + return getWebClient(KakaoApiUrlConstant.OAUTH_TOKEN) + .post() + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .acceptCharset(StandardCharsets.UTF_8) + .body(BodyInserters.fromFormData(formData)) + .retrieve() + .bodyToMono(KakaoTokenRefreshResponse.class) + .block(); + } + + private WebClient getWebClient(String baseUrl) { return WebClient.builder() .baseUrl(baseUrl) diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java b/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java index 5fd92c2..a3c69ec 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java @@ -1,7 +1,8 @@ package kr.kro.photoliner.global.kakao.login.constant; public final class KakaoApiUrlConstant { - public static final String AUTHORIZATION_REDIRECT_URL = "https://kauth.kakao.com/oauth/authorize"; - public static final String GET_OAUTH_TOKEN_URL = "https://kauth.kakao.com/oauth/token"; + public static final String AUTHORIZATION_REDIRECT = "https://kauth.kakao.com/oauth/authorize"; + public static final String OAUTH_TOKEN = "https://kauth.kakao.com/oauth/token"; public static final String GET_USER_PROFILE = "https://kapi.kakao.com/v2/user/me"; + public static final String VERIFY_ACCESS_TOKEN = "https://kapi.kakao.com/v1/user/access_token_info"; } diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoTokenRefreshRequest.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoTokenRefreshRequest.java new file mode 100644 index 0000000..22a674c --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoTokenRefreshRequest.java @@ -0,0 +1,13 @@ +package kr.kro.photoliner.global.kakao.login.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(SnakeCaseStrategy.class) +public record KakaoTokenRefreshRequest( + String grantType, + String clientId, + String refreshToken, + String clientSecret +) { +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoAccessTokenInfoResponse.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoAccessTokenInfoResponse.java new file mode 100644 index 0000000..c9c4d84 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoAccessTokenInfoResponse.java @@ -0,0 +1,12 @@ +package kr.kro.photoliner.global.kakao.login.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(SnakeCaseStrategy.class) +public record KakaoAccessTokenInfoResponse( + Long id, + Integer expiresIn, + Integer appId +) { +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoTokenRefreshResponse.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoTokenRefreshResponse.java new file mode 100644 index 0000000..1f2f334 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoTokenRefreshResponse.java @@ -0,0 +1,15 @@ +package kr.kro.photoliner.global.kakao.login.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(SnakeCaseStrategy.class) +public record KakaoTokenRefreshResponse( + String tokenType, + String accessToken, + String idToken, + Integer expiresIn, + String refreshToken, + Integer refreshTokenExpiresIn +) { +} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java index 9b4ecf3..44f819f 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java @@ -41,9 +41,13 @@ public KakaoProfileResponse getKakaoUserProfile(String accessToken) { .getKakaoUserProfile(accessToken); } + public void verifyAccessToken(String accessToken) { + kakaoAuthClient.verifyAccessToken(accessToken); + } + private String createAuthorizationRedirectUri(String restApiKey, String redirectUrlWhenComplete) { - return KakaoApiUrlConstant.AUTHORIZATION_REDIRECT_URL + return KakaoApiUrlConstant.AUTHORIZATION_REDIRECT + "?response_type=code" + "&client_id=" + restApiKey + "&redirect_uri=" + redirectUrlWhenComplete; From f8e6fca609267802ee0317193d4eeb3d42ecf4c9 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 23 Nov 2025 18:48:58 +0900 Subject: [PATCH 12/16] =?UTF-8?q?feat:=20ArgumentResolver=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20resovler=20=EB=82=B4=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9D=B8=EA=B0=80=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +++ .../domain/user/service/UserService.java | 6 +-- .../kr/kro/photoliner/global/auth/Auth.java | 13 ++++++ .../photoliner/global/auth/AuthContext.java | 24 ---------- .../auth/AuthenticationInterceptor.java | 29 ------------ .../photoliner/global/auth/JwtProvider.java | 20 +++++--- .../global/auth/UserArgumentResolver.java | 46 +++++++++++++++++++ .../global/config/WebMvcConfig.java | 10 ++++ 8 files changed, 92 insertions(+), 63 deletions(-) create mode 100644 src/main/java/kr/kro/photoliner/global/auth/Auth.java delete mode 100644 src/main/java/kr/kro/photoliner/global/auth/AuthContext.java delete mode 100644 src/main/java/kr/kro/photoliner/global/auth/AuthenticationInterceptor.java create mode 100644 src/main/java/kr/kro/photoliner/global/auth/UserArgumentResolver.java diff --git a/build.gradle b/build.gradle index c4451f7..712f887 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,13 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' + + // Netty MacOS + implementation('io.netty:netty-resolver-dns-native-macos') { + artifact { + classifier = "osx-aarch_64" // Apple Silicon + } + } } tasks.named('test') { diff --git a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java index 585f232..56b539a 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java +++ b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java @@ -19,11 +19,11 @@ public class UserService { public JwtResponse oAuthLogin(String authorizationCode) { KakaoOauthTokenResponse tokenResponse = kakaoAuthService.getTokenByAuthorizationCode(authorizationCode); - KakaoProfileResponse profileResponse = kakaoAuthService.getKakaoUserProfile( - tokenResponse.accessToken()); + String accessToken = tokenResponse.accessToken(); + KakaoProfileResponse profileResponse = kakaoAuthService.getKakaoUserProfile(accessToken); User user = userRepository.findUserByEmail(profileResponse.kakaoAccount().email()) - .orElse(signup(User.from(profileResponse))); + .orElseGet(() -> signup(User.from(profileResponse))); return new JwtResponse(jwtProvider.createAccessToken(user)); } diff --git a/src/main/java/kr/kro/photoliner/global/auth/Auth.java b/src/main/java/kr/kro/photoliner/global/auth/Auth.java new file mode 100644 index 0000000..325453a --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/auth/Auth.java @@ -0,0 +1,13 @@ +package kr.kro.photoliner.global.auth; + +import io.swagger.v3.oas.annotations.Hidden; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Hidden +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Auth { +} diff --git a/src/main/java/kr/kro/photoliner/global/auth/AuthContext.java b/src/main/java/kr/kro/photoliner/global/auth/AuthContext.java deleted file mode 100644 index 349c0ac..0000000 --- a/src/main/java/kr/kro/photoliner/global/auth/AuthContext.java +++ /dev/null @@ -1,24 +0,0 @@ -package kr.kro.photoliner.global.auth; - -import kr.kro.photoliner.global.code.ApiResponseCode; -import kr.kro.photoliner.global.exception.CustomException; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -public class AuthContext { - - private Long userId; - - public Long getUserId() { - if (userId == null) { - throw CustomException.of(ApiResponseCode.NOT_FOUND_USER, "userId is null"); - } - return userId; - } - - public void setUserId(Long userId) { - this.userId = userId; - } -} diff --git a/src/main/java/kr/kro/photoliner/global/auth/AuthenticationInterceptor.java b/src/main/java/kr/kro/photoliner/global/auth/AuthenticationInterceptor.java deleted file mode 100644 index 43a7f6b..0000000 --- a/src/main/java/kr/kro/photoliner/global/auth/AuthenticationInterceptor.java +++ /dev/null @@ -1,29 +0,0 @@ -package kr.kro.photoliner.global.auth; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -@Component -@RequiredArgsConstructor -public class AuthenticationInterceptor implements HandlerInterceptor { - - private final JwtProvider jwtProvider; - private final AuthContext authContext; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - String accessToken = jwtProvider.extractAccessToken(request); - if (accessToken == null) { - return false; - } - jwtProvider.validateToken(accessToken); - Long userId = jwtProvider.getUserId(accessToken); - authContext.setUserId(userId); - return true; - } - - -} diff --git a/src/main/java/kr/kro/photoliner/global/auth/JwtProvider.java b/src/main/java/kr/kro/photoliner/global/auth/JwtProvider.java index 39ed953..5d36443 100644 --- a/src/main/java/kr/kro/photoliner/global/auth/JwtProvider.java +++ b/src/main/java/kr/kro/photoliner/global/auth/JwtProvider.java @@ -8,7 +8,7 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; -import jakarta.servlet.http.HttpServletRequest; +import io.jsonwebtoken.security.WeakKeyException; import java.time.Instant; import java.util.Base64; import java.util.Date; @@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import org.springframework.web.context.request.WebRequest; @Component public class JwtProvider { @@ -37,7 +38,7 @@ public JwtProvider( this.accessTokenExpirationTime = accessTokenExpirationTime; } - public String extractAccessToken(HttpServletRequest request) { + public String extractAccessToken(WebRequest request) { String bearerToken = request.getHeader(AUTHORIZATION); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_TYPE)) { return bearerToken.substring(BEARER_TYPE_LEN); @@ -62,13 +63,18 @@ public Long getUserId(String token) { public void validateToken(String token) { try { - Jwts.parser().setSigningKey(secretKey).build().parseClaimsJws(token); - } catch (io.jsonwebtoken.security.SignatureException | MalformedJwtException e) { - throw CustomException.of(ApiResponseCode.INVALID_JWT_TOKEN); + Jwts.parser() + .verifyWith(getSecretKey()) + .build() + .parseSignedClaims(token); + } catch (io.jsonwebtoken.security.SignatureException | MalformedJwtException | WeakKeyException e) { + throw CustomException.of(ApiResponseCode.INVALID_JWT_TOKEN, e.getMessage(), e); } catch (ExpiredJwtException e) { - throw CustomException.of(ApiResponseCode.EXPIRED_JWT_TOKEN); + throw CustomException.of(ApiResponseCode.EXPIRED_JWT_TOKEN, e.getMessage(), e); } catch (UnsupportedJwtException | IllegalArgumentException e) { - throw CustomException.of(ApiResponseCode.TOKEN_PARSE_ERROR); + throw CustomException.of(ApiResponseCode.TOKEN_PARSE_ERROR, e.getMessage(), e); + } catch (Exception e) { + throw e; } } diff --git a/src/main/java/kr/kro/photoliner/global/auth/UserArgumentResolver.java b/src/main/java/kr/kro/photoliner/global/auth/UserArgumentResolver.java new file mode 100644 index 0000000..81b2517 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/auth/UserArgumentResolver.java @@ -0,0 +1,46 @@ +package kr.kro.photoliner.global.auth; + +import java.util.Objects; +import kr.kro.photoliner.domain.user.model.User; +import kr.kro.photoliner.domain.user.repository.UserRepository; +import kr.kro.photoliner.global.code.ApiResponseCode; +import kr.kro.photoliner.global.exception.CustomException; +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 { + private final UserRepository userRepository; + private final JwtProvider jwtProvider; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Auth.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + Auth authAt = parameter.getParameterAnnotation(Auth.class); + Objects.requireNonNull(authAt); + + String token = jwtProvider.extractAccessToken(webRequest); + jwtProvider.validateToken(token); + Long userId = jwtProvider.getUserId(token); + + User user = userRepository.findUserById(userId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + userId)); + + return user.getId(); + } +} diff --git a/src/main/java/kr/kro/photoliner/global/config/WebMvcConfig.java b/src/main/java/kr/kro/photoliner/global/config/WebMvcConfig.java index f75222f..efab5a9 100644 --- a/src/main/java/kr/kro/photoliner/global/config/WebMvcConfig.java +++ b/src/main/java/kr/kro/photoliner/global/config/WebMvcConfig.java @@ -1,8 +1,11 @@ package kr.kro.photoliner.global.config; +import java.util.List; +import kr.kro.photoliner.global.auth.UserArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -18,6 +21,8 @@ public class WebMvcConfig implements WebMvcConfigurer { @Value("${photo.upload.base-dir}") private String baseDir; + private final UserArgumentResolver userArgumentResolver; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -36,4 +41,9 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { .resourceChain(true) .addResolver(new PathResourceResolver()); } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userArgumentResolver); + } } From 55c324cfc291d1763c5b93955564d7480b3391b4 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 23 Nov 2025 18:59:01 +0900 Subject: [PATCH 13/16] =?UTF-8?q?feat:=20@auth=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../album/controller/AlbumController.java | 29 ++++++++++++------- .../album/dto/request/AlbumCreateRequest.java | 3 -- .../domain/album/service/AlbumService.java | 6 ++-- .../photo/controller/PhotoController.java | 22 +++++++++----- .../dto/request/PhotoMarkersRequest.java | 4 --- .../domain/photo/service/PhotoService.java | 6 ++-- .../global/auth/UserArgumentResolver.java | 1 + 7 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/album/controller/AlbumController.java b/src/main/java/kr/kro/photoliner/domain/album/controller/AlbumController.java index 12acd42..cb01ed4 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/controller/AlbumController.java +++ b/src/main/java/kr/kro/photoliner/domain/album/controller/AlbumController.java @@ -12,6 +12,7 @@ import kr.kro.photoliner.domain.album.dto.response.AlbumPhotoMarkersResponse; import kr.kro.photoliner.domain.album.dto.response.AlbumsResponse; import kr.kro.photoliner.domain.album.service.AlbumService; +import kr.kro.photoliner.global.auth.Auth; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -25,7 +26,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -37,16 +37,17 @@ public class AlbumController { @PostMapping public ResponseEntity createAlbum( - @Valid @RequestBody AlbumCreateRequest request + @Valid @RequestBody AlbumCreateRequest request, + @Auth Long userId ) { - AlbumCreateResponse response = albumService.createAlbum(request); + AlbumCreateResponse response = albumService.createAlbum(userId, request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } @GetMapping public ResponseEntity getAlbums( - @RequestParam Long userId, - @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable, + @Auth Long userId ) { return ResponseEntity.ok(albumService.getAlbums(userId, pageable)); } @@ -54,7 +55,8 @@ public ResponseEntity getAlbums( @PatchMapping("/{albumId}/title") public ResponseEntity updateAlbumTitle( @PathVariable Long albumId, - @RequestBody @Valid AlbumTitleUpdateRequest request + @RequestBody @Valid AlbumTitleUpdateRequest request, + @Auth Long userId ) { albumService.updateAlbumTitle(albumId, request); return ResponseEntity.noContent().build(); @@ -62,7 +64,8 @@ public ResponseEntity updateAlbumTitle( @DeleteMapping public ResponseEntity deletePhoto( - @Valid @RequestBody AlbumDeleteRequest request + @Valid @RequestBody AlbumDeleteRequest request, + @Auth Long userId ) { albumService.deleteAlbums(request); return ResponseEntity.noContent().build(); @@ -71,7 +74,8 @@ public ResponseEntity deletePhoto( @GetMapping("/{albumId}/photos") public ResponseEntity getAlbumItems( @PathVariable Long albumId, - @PageableDefault Pageable pageable + @PageableDefault Pageable pageable, + @Auth Long userId ) { return ResponseEntity.ok(albumService.getAlbumPhotoItems(albumId, pageable)); } @@ -79,7 +83,8 @@ public ResponseEntity getAlbumItems( @PostMapping("/{albumId}/photos") public ResponseEntity createAlbumItems( @PathVariable Long albumId, - @RequestBody @Valid AlbumItemCreateRequest request + @RequestBody @Valid AlbumItemCreateRequest request, + @Auth Long userId ) { albumService.createAlbumItems(albumId, request); return ResponseEntity.noContent().build(); @@ -88,7 +93,8 @@ public ResponseEntity createAlbumItems( @DeleteMapping("/{albumId}/photos") public ResponseEntity deleteAlbumItems( @PathVariable Long albumId, - @RequestBody @Valid AlbumItemDeleteRequest request + @RequestBody @Valid AlbumItemDeleteRequest request, + @Auth Long userId ) { albumService.deleteAlbumItems(albumId, request); return ResponseEntity.noContent().build(); @@ -97,7 +103,8 @@ public ResponseEntity deleteAlbumItems( @GetMapping("/{albumId}/markers") public ResponseEntity getAlbumPhotoMarkers( @PathVariable Long albumId, - @Valid AlbumPhotoMarkersRequest request + @Valid AlbumPhotoMarkersRequest request, + @Auth Long userId ) { return ResponseEntity.ok(albumService.getAlbumPhotoMarkers(albumId, request)); } diff --git a/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumCreateRequest.java b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumCreateRequest.java index 953c4dd..d69e65f 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumCreateRequest.java +++ b/src/main/java/kr/kro/photoliner/domain/album/dto/request/AlbumCreateRequest.java @@ -1,11 +1,8 @@ package kr.kro.photoliner.domain.album.dto.request; import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; public record AlbumCreateRequest( - @NotNull - Long userId, @NotEmpty String title ) { diff --git a/src/main/java/kr/kro/photoliner/domain/album/service/AlbumService.java b/src/main/java/kr/kro/photoliner/domain/album/service/AlbumService.java index b05b778..e65a5e9 100644 --- a/src/main/java/kr/kro/photoliner/domain/album/service/AlbumService.java +++ b/src/main/java/kr/kro/photoliner/domain/album/service/AlbumService.java @@ -37,9 +37,9 @@ public class AlbumService { private final GeometryFactory geometryFactory; @Transactional - public AlbumCreateResponse createAlbum(AlbumCreateRequest request) { - User user = userRepository.findUserById(request.userId()) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + request.userId())); + public AlbumCreateResponse createAlbum(Long userId, AlbumCreateRequest request) { + User user = userRepository.findUserById(userId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + userId)); Album album = Album.builder() .title(request.title()) .user(user) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index 14fb26f..8ee9cea 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -11,6 +11,7 @@ import kr.kro.photoliner.domain.photo.dto.response.PhotosResponse; import kr.kro.photoliner.domain.photo.service.PhotoService; import kr.kro.photoliner.domain.photo.service.PhotoUploadService; +import kr.kro.photoliner.global.auth.Auth; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -25,7 +26,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -40,21 +40,24 @@ public class PhotoController { @GetMapping public ResponseEntity getPhotos( - @RequestParam Long userId, + @Auth Long userId, @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable ) { return ResponseEntity.ok(photoService.getPhotosByIds(userId, pageable)); } @GetMapping("/markers") - public ResponseEntity getPhotoMarkers(@Valid PhotoMarkersRequest request) { - return ResponseEntity.ok(photoService.getPhotoMarkers(request)); + public ResponseEntity getPhotoMarkers( + @Valid PhotoMarkersRequest request, + @Auth Long userId + ) { + return ResponseEntity.ok(photoService.getPhotoMarkers(userId, request)); } @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity uploadPhotos( - @RequestParam("userId") Long userId, - @RequestPart("files") List files + @RequestPart("files") List files, + @Auth Long userId ) { PhotoUploadResponse response = photoUploadService.uploadPhotos(userId, files); return ResponseEntity.status(HttpStatus.CREATED).body(response); @@ -62,6 +65,7 @@ public ResponseEntity uploadPhotos( @PatchMapping("/{photoId}/captured-date") public ResponseEntity updatePhotoCapturedDate( + @Auth Long userId, @PathVariable Long photoId, @Valid @RequestBody PhotoCapturedDateUpdateRequest request ) { @@ -72,7 +76,8 @@ public ResponseEntity updatePhotoCapturedDate( @PatchMapping("/{photoId}/location") public ResponseEntity updatePhotoLocation( @PathVariable Long photoId, - @Valid @RequestBody PhotoLocationUpdateRequest request + @Valid @RequestBody PhotoLocationUpdateRequest request, + @Auth Long userId ) { photoService.updatePhotoLocation(photoId, request); return ResponseEntity.noContent().build(); @@ -80,7 +85,8 @@ public ResponseEntity updatePhotoLocation( @DeleteMapping public ResponseEntity deletePhoto( - @Valid @RequestBody DeletePhotosRequest request + @Valid @RequestBody DeletePhotosRequest request, + @Auth Long userId ) { photoService.deletePhotos(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/PhotoMarkersRequest.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/PhotoMarkersRequest.java index 8dab443..ac18d4d 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/PhotoMarkersRequest.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/PhotoMarkersRequest.java @@ -2,13 +2,9 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; import org.locationtech.jts.geom.Coordinate; public record PhotoMarkersRequest( - @NotNull @Min(0) - Long userId, - @Min(0) @Max(90) double swLat, diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index 380951c..cc4081f 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -1,7 +1,6 @@ package kr.kro.photoliner.domain.photo.service; import java.util.List; -import kr.kro.photoliner.domain.album.repository.AlbumPhotoRepository; import kr.kro.photoliner.domain.photo.dto.request.DeletePhotosRequest; import kr.kro.photoliner.domain.photo.dto.request.PhotoCapturedDateUpdateRequest; import kr.kro.photoliner.domain.photo.dto.request.PhotoLocationUpdateRequest; @@ -27,7 +26,6 @@ public class PhotoService { private final PhotoRepository photoRepository; - private final AlbumPhotoRepository albumPhotoRepository; private final GeometryFactory geometryFactory; private final FileStorage fileStorage; @@ -37,11 +35,11 @@ public PhotosResponse getPhotosByIds(Long userId, Pageable pageable) { } @Transactional(readOnly = true) - public PhotoMarkersResponse getPhotoMarkers(PhotoMarkersRequest request) { + public PhotoMarkersResponse getPhotoMarkers(Long userId, PhotoMarkersRequest request) { Point sw = geometryFactory.createPoint(request.getSouthWestCoordinate()); Point ne = geometryFactory.createPoint(request.getNorthEastCoordinate()); - Photos photos = photoRepository.getByUserIdInBox(request.userId(), sw, ne); + Photos photos = photoRepository.getByUserIdInBox(userId, sw, ne); return PhotoMarkersResponse.from(photos); } diff --git a/src/main/java/kr/kro/photoliner/global/auth/UserArgumentResolver.java b/src/main/java/kr/kro/photoliner/global/auth/UserArgumentResolver.java index 81b2517..da20406 100644 --- a/src/main/java/kr/kro/photoliner/global/auth/UserArgumentResolver.java +++ b/src/main/java/kr/kro/photoliner/global/auth/UserArgumentResolver.java @@ -35,6 +35,7 @@ public Object resolveArgument( Objects.requireNonNull(authAt); String token = jwtProvider.extractAccessToken(webRequest); + System.out.println("resolver: " + token); jwtProvider.validateToken(token); Long userId = jwtProvider.getUserId(token); From 5516b5a3326d50eb3dc89f49bafe20dd0a2537b7 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 23 Nov 2025 19:56:31 +0900 Subject: [PATCH 14/16] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/photo/controller/PhotoController.java | 3 ++- .../photo/dto/request/CreatePhotosRequest.java | 4 ---- .../domain/photo/service/PhotoService.java | 4 ++-- .../domain/user/controller/UserController.java | 9 +++++++++ .../user/dto/response/UserInfoResponse.java | 16 ++++++++++++++++ .../domain/user/service/UserService.java | 9 +++++++++ 6 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 src/main/java/kr/kro/photoliner/domain/user/dto/response/UserInfoResponse.java diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index 6d0bec1..7bf1a2f 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -69,7 +69,8 @@ public ResponseEntity createPhotos( @Valid @RequestBody CreatePhotosRequest request, @Auth Long userId ) { - photoService.createPhotos(request); + System.out.println("create Photos: " + userId); + photoService.createPhotos(userId, request); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java index 06e3a43..72d975a 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java @@ -11,10 +11,6 @@ import org.locationtech.jts.geom.Coordinate; public record CreatePhotosRequest( - @NotNull - Long userId, - - @NotNull @NotEmpty List photos ) { diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index 9dc6151..9ef2e64 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -54,10 +54,10 @@ public PhotoMarkersResponse getPhotoMarkers(Long userId, PhotoMarkersRequest req } @Transactional - public void createPhotos(CreatePhotosRequest request) { + public void createPhotos(Long userId, CreatePhotosRequest request) { List photos = request.photos().stream() .map(photo -> Photo.builder() - .userId(request.userId()) + .userId(userId) .fileName(photo.fileName()) .filePath(cdnURL + ORIGINAL_BASE_PATH + photo.uploadFileName()) .thumbnailPath(cdnURL + THUMBNAIL_BASE_PATH + photo.uploadFileName()) diff --git a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java index 44478b8..4ac50dc 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java +++ b/src/main/java/kr/kro/photoliner/domain/user/controller/UserController.java @@ -2,7 +2,9 @@ import java.net.URI; import kr.kro.photoliner.common.dto.response.JwtResponse; +import kr.kro.photoliner.domain.user.dto.response.UserInfoResponse; import kr.kro.photoliner.domain.user.service.UserService; +import kr.kro.photoliner.global.auth.Auth; import kr.kro.photoliner.global.kakao.login.service.KakaoAuthService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -40,4 +42,11 @@ public ResponseEntity authorize() { .location(URI.create(kakaoAuthService.getAuthorizationRedirectUrl())) .build(); } + + @GetMapping("/info") + public ResponseEntity getUserInfo( + @Auth Long userId + ) { + return ResponseEntity.ok(userService.getUserInfo(userId)); + } } diff --git a/src/main/java/kr/kro/photoliner/domain/user/dto/response/UserInfoResponse.java b/src/main/java/kr/kro/photoliner/domain/user/dto/response/UserInfoResponse.java new file mode 100644 index 0000000..29f77f5 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/user/dto/response/UserInfoResponse.java @@ -0,0 +1,16 @@ +package kr.kro.photoliner.domain.user.dto.response; + +import kr.kro.photoliner.domain.user.model.User; + +public record UserInfoResponse( + String name, + String email +) { + + public static UserInfoResponse from(User user) { + return new UserInfoResponse( + user.getName(), + user.getEmail() + ); + } +} diff --git a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java index 56b539a..68b8b10 100644 --- a/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java +++ b/src/main/java/kr/kro/photoliner/domain/user/service/UserService.java @@ -1,9 +1,12 @@ package kr.kro.photoliner.domain.user.service; import kr.kro.photoliner.common.dto.response.JwtResponse; +import kr.kro.photoliner.domain.user.dto.response.UserInfoResponse; import kr.kro.photoliner.domain.user.model.User; import kr.kro.photoliner.domain.user.repository.UserRepository; import kr.kro.photoliner.global.auth.JwtProvider; +import kr.kro.photoliner.global.code.ApiResponseCode; +import kr.kro.photoliner.global.exception.CustomException; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; import kr.kro.photoliner.global.kakao.login.service.KakaoAuthService; @@ -17,6 +20,12 @@ public class UserService { private final KakaoAuthService kakaoAuthService; private final JwtProvider jwtProvider; + public UserInfoResponse getUserInfo(Long userId) { + User user = userRepository.findUserById(userId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + userId)); + return UserInfoResponse.from(user); + } + public JwtResponse oAuthLogin(String authorizationCode) { KakaoOauthTokenResponse tokenResponse = kakaoAuthService.getTokenByAuthorizationCode(authorizationCode); String accessToken = tokenResponse.accessToken(); From e42f8295f49e9023c6812a26345e6716b4987707 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 23 Nov 2025 20:27:12 +0900 Subject: [PATCH 15/16] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photo/controller/PhotoController.java | 1 - .../kakao/login/client/KakaoAuthClient.java | 30 ------------------- .../login/constant/KakaoApiUrlConstant.java | 1 - .../request/KakaoAuthorizationRequest.java | 13 -------- .../dto/request/KakaoTokenRefreshRequest.java | 13 -------- .../KakaoAccessTokenInfoResponse.java | 12 -------- .../response/KakaoTokenRefreshResponse.java | 15 ---------- .../kakao/login/service/KakaoAuthService.java | 5 ---- 8 files changed, 90 deletions(-) delete mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoAuthorizationRequest.java delete mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoTokenRefreshRequest.java delete mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoAccessTokenInfoResponse.java delete mode 100644 src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoTokenRefreshResponse.java diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index 7bf1a2f..0e733eb 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -69,7 +69,6 @@ public ResponseEntity createPhotos( @Valid @RequestBody CreatePhotosRequest request, @Auth Long userId ) { - System.out.println("create Photos: " + userId); photoService.createPhotos(userId, request); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java index eb4c215..8569770 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/client/KakaoAuthClient.java @@ -3,11 +3,8 @@ import java.nio.charset.StandardCharsets; import kr.kro.photoliner.global.kakao.login.constant.KakaoApiUrlConstant; import kr.kro.photoliner.global.kakao.login.dto.request.KakaoOauthTokenRequest; -import kr.kro.photoliner.global.kakao.login.dto.request.KakaoTokenRefreshRequest; -import kr.kro.photoliner.global.kakao.login.dto.response.KakaoAccessTokenInfoResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoOauthTokenResponse; import kr.kro.photoliner.global.kakao.login.dto.response.KakaoProfileResponse; -import kr.kro.photoliner.global.kakao.login.dto.response.KakaoTokenRefreshResponse; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; @@ -46,33 +43,6 @@ public KakaoProfileResponse getKakaoUserProfile(String accessToken) { .block(); } - public KakaoAccessTokenInfoResponse verifyAccessToken(String accessToken) { - return getWebClient(KakaoApiUrlConstant.VERIFY_ACCESS_TOKEN) - .get() - .header("Authorization", BEARER_TOKEN_PREFIX + accessToken) - .retrieve() - .bodyToMono(KakaoAccessTokenInfoResponse.class) - .block(); - } - - public KakaoTokenRefreshResponse refreshToken(KakaoTokenRefreshRequest request) { - MultiValueMap formData = new LinkedMultiValueMap<>(); - formData.add("grant_type", request.grantType()); - formData.add("client_id", request.clientId()); - formData.add("refresh_token", request.refreshToken()); - formData.add("client_secret", request.clientSecret()); - - return getWebClient(KakaoApiUrlConstant.OAUTH_TOKEN) - .post() - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .acceptCharset(StandardCharsets.UTF_8) - .body(BodyInserters.fromFormData(formData)) - .retrieve() - .bodyToMono(KakaoTokenRefreshResponse.class) - .block(); - } - - private WebClient getWebClient(String baseUrl) { return WebClient.builder() .baseUrl(baseUrl) diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java b/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java index a3c69ec..bae4c7e 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/constant/KakaoApiUrlConstant.java @@ -4,5 +4,4 @@ public final class KakaoApiUrlConstant { public static final String AUTHORIZATION_REDIRECT = "https://kauth.kakao.com/oauth/authorize"; public static final String OAUTH_TOKEN = "https://kauth.kakao.com/oauth/token"; public static final String GET_USER_PROFILE = "https://kapi.kakao.com/v2/user/me"; - public static final String VERIFY_ACCESS_TOKEN = "https://kapi.kakao.com/v1/user/access_token_info"; } diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoAuthorizationRequest.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoAuthorizationRequest.java deleted file mode 100644 index 3ac57e7..0000000 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoAuthorizationRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package kr.kro.photoliner.global.kakao.login.dto.request; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -@JsonNaming(SnakeCaseStrategy.class) -public record KakaoAuthorizationRequest( - String code, - String error, - String errorDescription, - String state -) { -} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoTokenRefreshRequest.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoTokenRefreshRequest.java deleted file mode 100644 index 22a674c..0000000 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/request/KakaoTokenRefreshRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package kr.kro.photoliner.global.kakao.login.dto.request; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -@JsonNaming(SnakeCaseStrategy.class) -public record KakaoTokenRefreshRequest( - String grantType, - String clientId, - String refreshToken, - String clientSecret -) { -} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoAccessTokenInfoResponse.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoAccessTokenInfoResponse.java deleted file mode 100644 index c9c4d84..0000000 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoAccessTokenInfoResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package kr.kro.photoliner.global.kakao.login.dto.response; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -@JsonNaming(SnakeCaseStrategy.class) -public record KakaoAccessTokenInfoResponse( - Long id, - Integer expiresIn, - Integer appId -) { -} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoTokenRefreshResponse.java b/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoTokenRefreshResponse.java deleted file mode 100644 index 1f2f334..0000000 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/dto/response/KakaoTokenRefreshResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package kr.kro.photoliner.global.kakao.login.dto.response; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -@JsonNaming(SnakeCaseStrategy.class) -public record KakaoTokenRefreshResponse( - String tokenType, - String accessToken, - String idToken, - Integer expiresIn, - String refreshToken, - Integer refreshTokenExpiresIn -) { -} diff --git a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java index 44f819f..e5149c1 100644 --- a/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java +++ b/src/main/java/kr/kro/photoliner/global/kakao/login/service/KakaoAuthService.java @@ -41,11 +41,6 @@ public KakaoProfileResponse getKakaoUserProfile(String accessToken) { .getKakaoUserProfile(accessToken); } - public void verifyAccessToken(String accessToken) { - kakaoAuthClient.verifyAccessToken(accessToken); - } - - private String createAuthorizationRedirectUri(String restApiKey, String redirectUrlWhenComplete) { return KakaoApiUrlConstant.AUTHORIZATION_REDIRECT + "?response_type=code" From c4ad3bfbb35a9c8bdadefd4a989a744c36a077a9 Mon Sep 17 00:00:00 2001 From: chanrhan Date: Sun, 23 Nov 2025 20:40:25 +0900 Subject: [PATCH 16/16] =?UTF-8?q?chore:=20yml=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 11 +++++++++++ src/main/resources/application-local-example.yml | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 8cefaea..00b665f 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -31,3 +31,14 @@ cloud: bucket: ${BUCKET_NAME} cdn: base-url: ${CDN_URL} + +kakao: + api: + rest-api-key: ${REST_API_KEY} + login-oauth: + redirect-url: ${REDIRECT_URL} + +jwt: + secret-key: ${SECRET_KEY} + access-token: + expiration-time: ${EXPIRATION_TIME} diff --git a/src/main/resources/application-local-example.yml b/src/main/resources/application-local-example.yml index 48a2d46..7928171 100644 --- a/src/main/resources/application-local-example.yml +++ b/src/main/resources/application-local-example.yml @@ -22,6 +22,11 @@ cloud: kakao: api: - rest-api-key: + rest-api-key: rest api key login-oauth: redirect-url: http://localhost:8080/api/v1/login/kakao + +jwt: + secret-key: secret key + access-token: + expiration-time: time