From c0e1de56f7e846fc94008ea17a0de7b9c22ae67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Sun, 23 Jun 2024 18:16:08 +0900 Subject: [PATCH 1/8] feat: implement kakao login --- user-service/build.gradle.kts | 1 + .../kr/mafoo/user/config/WebFluxConfig.java | 13 ++++ .../mafoo/user/controller/AuthController.java | 5 ++ .../dto/response/KakaoLoginInfo.java | 8 ++ .../java/kr/mafoo/user/domain/AuthToken.java | 8 ++ .../kr/mafoo/user/domain/MemberEntity.java | 7 ++ .../mafoo/user/domain/SocialMemberEntity.java | 55 ++++++++++++++ .../user/domain/SocialMemberEntityKey.java | 14 ++++ .../kr/mafoo/user/enums/IdentityProvider.java | 5 ++ .../repository/SocialMemberRepository.java | 9 +++ .../kr/mafoo/user/service/AuthService.java | 76 +++++++++++++++++++ .../kr/mafoo/user/service/MemberService.java | 5 ++ .../java/kr/mafoo/user/util/IdGenerator.java | 9 +++ 13 files changed, 215 insertions(+) create mode 100644 user-service/src/main/java/kr/mafoo/user/controller/dto/response/KakaoLoginInfo.java create mode 100644 user-service/src/main/java/kr/mafoo/user/domain/AuthToken.java create mode 100644 user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntity.java create mode 100644 user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntityKey.java create mode 100644 user-service/src/main/java/kr/mafoo/user/enums/IdentityProvider.java create mode 100644 user-service/src/main/java/kr/mafoo/user/repository/SocialMemberRepository.java create mode 100644 user-service/src/main/java/kr/mafoo/user/service/AuthService.java create mode 100644 user-service/src/main/java/kr/mafoo/user/util/IdGenerator.java diff --git a/user-service/build.gradle.kts b/user-service/build.gradle.kts index 60e0093..124e39f 100644 --- a/user-service/build.gradle.kts +++ b/user-service/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { testImplementation("org.springframework.security:spring-security-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") implementation("org.projectlombok:lombok:1.18.32") + implementation("com.github.f4b6a3:ulid-creator:5.2.3") annotationProcessor("org.projectlombok:lombok:1.18.32") } diff --git a/user-service/src/main/java/kr/mafoo/user/config/WebFluxConfig.java b/user-service/src/main/java/kr/mafoo/user/config/WebFluxConfig.java index 449f4ca..8450301 100644 --- a/user-service/src/main/java/kr/mafoo/user/config/WebFluxConfig.java +++ b/user-service/src/main/java/kr/mafoo/user/config/WebFluxConfig.java @@ -1,8 +1,10 @@ package kr.mafoo.user.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; @EnableWebFlux @@ -12,4 +14,15 @@ public class WebFluxConfig implements WebFluxConfigurer { public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { configurer.addCustomResolver(new MemberIdParameterResolver()); } + + @Bean("externalWebClient") + public WebClient externalServiceWebClient() { + return WebClient.builder() + .codecs(clientCodecConfigurer -> { + clientCodecConfigurer + .defaultCodecs() + .maxInMemorySize(16 * 1024 * 1024); // 16MB + }) + .build(); + } } diff --git a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java index e5a898f..c5b9277 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java @@ -4,11 +4,16 @@ import kr.mafoo.user.controller.dto.request.KakaoLoginRequest; import kr.mafoo.user.controller.dto.request.TokenRefreshRequest; import kr.mafoo.user.controller.dto.response.LoginResponse; +import kr.mafoo.user.service.AuthService; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; +@RequiredArgsConstructor @RestController public class AuthController implements AuthApi { + private final AuthService authService; + @Override public Mono loginWithKakao(KakaoLoginRequest request) { return Mono.just(new LoginResponse("test_access_token", "test_refresh_token")); diff --git a/user-service/src/main/java/kr/mafoo/user/controller/dto/response/KakaoLoginInfo.java b/user-service/src/main/java/kr/mafoo/user/controller/dto/response/KakaoLoginInfo.java new file mode 100644 index 0000000..d6f6a1f --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/controller/dto/response/KakaoLoginInfo.java @@ -0,0 +1,8 @@ +package kr.mafoo.user.controller.dto.response; + +public record KakaoLoginInfo( + String id, + String nickname, + String email +) { +} diff --git a/user-service/src/main/java/kr/mafoo/user/domain/AuthToken.java b/user-service/src/main/java/kr/mafoo/user/domain/AuthToken.java new file mode 100644 index 0000000..bb11c50 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/domain/AuthToken.java @@ -0,0 +1,8 @@ +package kr.mafoo.user.domain; + +public record AuthToken( + String accessToken, + String refreshToken +){ + +} diff --git a/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java b/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java index 048b10e..b257aae 100644 --- a/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java +++ b/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java @@ -40,4 +40,11 @@ public boolean equals(Object obj) { public int hashCode() { return id.hashCode(); } + + public static MemberEntity newMember(String id, String name) { + MemberEntity member = new MemberEntity(); + member.id = id; + member.name = name; + return member; + } } diff --git a/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntity.java b/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntity.java new file mode 100644 index 0000000..0234c8c --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntity.java @@ -0,0 +1,55 @@ +package kr.mafoo.user.domain; + +import kr.mafoo.user.enums.IdentityProvider; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Getter +@Setter +@NoArgsConstructor +@Table("social_member") +public class SocialMemberEntity { + @Column("identity_provider") + private IdentityProvider identityProvider; + + @Column("id") + private String id; + + @CreatedDate + @Column("created_at") + private LocalDateTime createdAt; + + @Column("member_id") + private String memberId; + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + SocialMemberEntity that = (SocialMemberEntity) obj; + + return id.equals(that.id) && identityProvider.equals(that.identityProvider); + } + + @Override + public int hashCode() { + return Objects.hash(id, identityProvider); + } + + public static SocialMemberEntity newSocialMember(IdentityProvider identityProvider, String id, String memberId) { + SocialMemberEntity socialMember = new SocialMemberEntity(); + socialMember.identityProvider = identityProvider; + socialMember.id = id; + socialMember.memberId = memberId; + return socialMember; + } +} diff --git a/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntityKey.java b/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntityKey.java new file mode 100644 index 0000000..4517edb --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntityKey.java @@ -0,0 +1,14 @@ +package kr.mafoo.user.domain; + +import kr.mafoo.user.enums.IdentityProvider; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.io.Serializable; + +@RequiredArgsConstructor +@Data +public class SocialMemberEntityKey implements Serializable { + private final IdentityProvider identityProvider; + private final String id; +} diff --git a/user-service/src/main/java/kr/mafoo/user/enums/IdentityProvider.java b/user-service/src/main/java/kr/mafoo/user/enums/IdentityProvider.java new file mode 100644 index 0000000..1e05e16 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/enums/IdentityProvider.java @@ -0,0 +1,5 @@ +package kr.mafoo.user.enums; + +public enum IdentityProvider { + KAKAO +} diff --git a/user-service/src/main/java/kr/mafoo/user/repository/SocialMemberRepository.java b/user-service/src/main/java/kr/mafoo/user/repository/SocialMemberRepository.java new file mode 100644 index 0000000..fb7dce2 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/repository/SocialMemberRepository.java @@ -0,0 +1,9 @@ +package kr.mafoo.user.repository; + +import kr.mafoo.user.domain.SocialMemberEntity; +import kr.mafoo.user.domain.SocialMemberEntityKey; +import org.springframework.data.r2dbc.repository.R2dbcRepository; + +public interface SocialMemberRepository extends R2dbcRepository { + +} diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java new file mode 100644 index 0000000..a643f69 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -0,0 +1,76 @@ +package kr.mafoo.user.service; + +import kr.mafoo.user.controller.dto.response.KakaoLoginInfo; +import kr.mafoo.user.domain.AuthToken; +import kr.mafoo.user.domain.SocialMemberEntity; +import kr.mafoo.user.domain.SocialMemberEntityKey; +import kr.mafoo.user.enums.IdentityProvider; +import kr.mafoo.user.repository.SocialMemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.LinkedHashMap; + +@RequiredArgsConstructor +@Service +public class AuthService { + private final WebClient externalWebClient; + private final SocialMemberRepository socialMemberRepository; + private final MemberService memberService; + + + public Mono loginWithKakao(String code) { + return getKakaoTokenWithCode(code) + .flatMap(this::getUserInfoWithKakaoToken) + .flatMap(kakaoLoginInfo -> getOrCreateMember(IdentityProvider.KAKAO, kakaoLoginInfo.id())); + } + + private Mono generateTokenWithMemberId(String memberId) { + return Mono.empty(); + } + + private Mono getOrCreateMember(IdentityProvider provider, String id) { + SocialMemberEntityKey key = new SocialMemberEntityKey(provider, id); + return socialMemberRepository + .findById(key) + .switchIfEmpty(createNewSocialMember(provider, id)) + .flatMap(socialMember -> generateTokenWithMemberId(socialMember.getMemberId())); + } + + private Mono createNewSocialMember(IdentityProvider provider, String id) { + return memberService + .createNewMember(id) + .flatMap(newMember -> socialMemberRepository.save( + SocialMemberEntity.newSocialMember(provider, id, newMember.getId()) + )); + } + + /** + * 카카오 로그인 관련 로직 + */ + private Mono getKakaoTokenWithCode(String code) { + return externalWebClient + .post() + .uri("https://kauth.kakao.com/oauth/token") + .bodyValue("grant_type=authorization_code&client_id=client_id&redirect_uri=http://localhost:8080/login/kakao&code=" + code) + .retrieve() + .bodyToMono(LinkedHashMap.class) + .map(map -> (String) map.get("access_token")); + } + + private Mono getUserInfoWithKakaoToken(String kakaoToken){ + return externalWebClient + .get() + .uri("https://kapi.kakao.com/v2/user/me") + .headers(headers -> headers.setBearerAuth(kakaoToken)) + .retrieve() + .bodyToMono(LinkedHashMap.class) + .map(map -> new KakaoLoginInfo( + (String) map.get("id"), + (String) map.get("kakao_account.email"), + (String) map.get("properties.nickname")) + ); + } +} diff --git a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java index 94deb7b..9df77f1 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java @@ -3,6 +3,7 @@ import kr.mafoo.user.domain.MemberEntity; import kr.mafoo.user.exception.MemberNotFoundException; import kr.mafoo.user.repository.MemberRepository; +import kr.mafoo.user.util.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -22,4 +23,8 @@ public Mono getMemberByMemberId(String memberId) { .switchIfEmpty(Mono.error(new MemberNotFoundException())); } + public Mono createNewMember(String username) { + MemberEntity memberEntity = MemberEntity.newMember(IdGenerator.generate(), username); + return memberRepository.save(memberEntity); + } } diff --git a/user-service/src/main/java/kr/mafoo/user/util/IdGenerator.java b/user-service/src/main/java/kr/mafoo/user/util/IdGenerator.java new file mode 100644 index 0000000..18ac643 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/util/IdGenerator.java @@ -0,0 +1,9 @@ +package kr.mafoo.user.util; + +import com.github.f4b6a3.ulid.UlidCreator; + +public class IdGenerator { + public static String generate() { + return UlidCreator.getMonotonicUlid().toString(); + } +} From 789b376a34c4fddf34cf1caa105b57e94780d44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Sun, 23 Jun 2024 18:45:34 +0900 Subject: [PATCH 2/8] feat: implement token generation --- user-service/build.gradle.kts | 13 ++++ .../kr/mafoo/user/UserServiceApplication.java | 2 + .../mafoo/user/controller/AuthController.java | 4 +- .../kr/mafoo/user/service/AuthService.java | 11 +-- .../mafoo/user/service/JWTTokenService.java | 74 +++++++++++++++++++ .../src/main/resources/application.yaml | 7 ++ 6 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java diff --git a/user-service/build.gradle.kts b/user-service/build.gradle.kts index 124e39f..04ba7f6 100644 --- a/user-service/build.gradle.kts +++ b/user-service/build.gradle.kts @@ -34,6 +34,19 @@ dependencies { implementation("org.projectlombok:lombok:1.18.32") implementation("com.github.f4b6a3:ulid-creator:5.2.3") annotationProcessor("org.projectlombok:lombok:1.18.32") + + val jjwtVersion = "0.12.5" + implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion") + runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion") + + implementation("io.projectreactor.tools:blockhound:1.0.9.RELEASE") +} + +tasks.withType().all { + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) { + jvmArgs("-XX:+AllowRedefinitionToAddDeleteMethods") + } } tasks.withType { diff --git a/user-service/src/main/java/kr/mafoo/user/UserServiceApplication.java b/user-service/src/main/java/kr/mafoo/user/UserServiceApplication.java index f4f979d..4f01f45 100644 --- a/user-service/src/main/java/kr/mafoo/user/UserServiceApplication.java +++ b/user-service/src/main/java/kr/mafoo/user/UserServiceApplication.java @@ -2,11 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import reactor.blockhound.BlockHound; @SpringBootApplication public class UserServiceApplication { public static void main(String[] args) { + // BlockHound.install(); SpringApplication.run(UserServiceApplication.class, args); } diff --git a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java index c5b9277..ab8ef4d 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java @@ -16,7 +16,9 @@ public class AuthController implements AuthApi { @Override public Mono loginWithKakao(KakaoLoginRequest request) { - return Mono.just(new LoginResponse("test_access_token", "test_refresh_token")); + return authService + .loginWithKakao(request.code()) + .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } @Override diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index a643f69..903b00c 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -19,6 +19,7 @@ public class AuthService { private final WebClient externalWebClient; private final SocialMemberRepository socialMemberRepository; private final MemberService memberService; + private final JWTTokenService jwtTokenService; public Mono loginWithKakao(String code) { @@ -27,16 +28,16 @@ public Mono loginWithKakao(String code) { .flatMap(kakaoLoginInfo -> getOrCreateMember(IdentityProvider.KAKAO, kakaoLoginInfo.id())); } - private Mono generateTokenWithMemberId(String memberId) { - return Mono.empty(); - } - private Mono getOrCreateMember(IdentityProvider provider, String id) { SocialMemberEntityKey key = new SocialMemberEntityKey(provider, id); return socialMemberRepository .findById(key) .switchIfEmpty(createNewSocialMember(provider, id)) - .flatMap(socialMember -> generateTokenWithMemberId(socialMember.getMemberId())); + .map(socialMember -> { + String accessToken = jwtTokenService.generateAccessToken(socialMember.getMemberId()); + String refreshToken = jwtTokenService.generateRefreshToken(socialMember.getMemberId()); + return new AuthToken(accessToken, refreshToken); + }); } private Mono createNewSocialMember(IdentityProvider provider, String id) { diff --git a/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java b/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java new file mode 100644 index 0000000..875f1fb --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java @@ -0,0 +1,74 @@ +package kr.mafoo.user.service; + +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.Date; + +@RequiredArgsConstructor +@Service +public class JWTTokenService { + @Value("${app.jwt.verify-key}") + private String verifyKey; + @Value("${app.jwt.expiration.access-token}") + private Long accessTokenExpiration; + @Value("${app.jwt.expiration.refresh-token}") + private Long refreshTokenExpiration; + + private SecretKey signKey = null; + private JwtParser parser = null; + + @PostConstruct + public void initSignKey() { + signKey = new SecretKeySpec(verifyKey.getBytes(), "AES"); + parser = Jwts + .parser() + .decryptWith(signKey) + .build(); + } + + private final static String TOKEN_TYPE_HEADER_KEY = "tkn_typ"; + private final static String ACCESS_TOKEN_TYPE_VALUE = "access"; + private final static String REFRESH_TOKEN_TYPE_VALUE = "refresh"; + private final static String USER_ID_CLAIM_KEY = "user_id"; + + public String generateAccessToken(String memberId) { + return Jwts + .builder() + .header() + .add(TOKEN_TYPE_HEADER_KEY, ACCESS_TOKEN_TYPE_VALUE) + .and() + .claims().add(USER_ID_CLAIM_KEY, memberId) + .and() + .expiration(generateAccessTokenExpirationDate()) + .encryptWith(signKey, Jwts.ENC.A128CBC_HS256) + .compact(); + } + + public String generateRefreshToken(String memberId) { + return Jwts + .builder() + .header() + .add(TOKEN_TYPE_HEADER_KEY, REFRESH_TOKEN_TYPE_VALUE) + .and() + .claims().add(USER_ID_CLAIM_KEY, memberId) + .and() + .expiration(generateRefreshTokenExpirationDate()) + .encryptWith(signKey, Jwts.ENC.A128CBC_HS256) + .compact(); + } + + private Date generateAccessTokenExpirationDate() { + return new Date(System.currentTimeMillis() + 1000 * accessTokenExpiration); + } + + public Date generateRefreshTokenExpirationDate() { + return new Date(System.currentTimeMillis() + 1000 * refreshTokenExpiration); + } +} diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index 273e526..49cf8f3 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -8,3 +8,10 @@ spring: logging: level: org.springframework.data.r2dbc: DEBUG + +app: + jwt: + verify-key: askdgjasdgjasdkgasjdgkasjdgkasjd832f892sdf + expiration: + access-token: 86400 # 1 day + refresh-token: 2592000 # 30 days From 7e0435880c74b4c26def1e71c78c11ac6800f8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Sun, 23 Jun 2024 18:51:19 +0900 Subject: [PATCH 3/8] feat: implement token refreshing --- .../kr/mafoo/user/controller/AuthController.java | 4 +++- .../java/kr/mafoo/user/service/AuthService.java | 10 ++++++++++ .../kr/mafoo/user/service/JWTTokenService.java | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java index ab8ef4d..8e7649e 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java @@ -23,6 +23,8 @@ public Mono loginWithKakao(KakaoLoginRequest request) { @Override public Mono loginWithRefreshToken(TokenRefreshRequest request) { - return Mono.just(new LoginResponse("test_access_token", request.refreshToken())); + return authService + .loginWithRefreshToken(request.refreshToken()) + .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } } diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index 903b00c..4870f31 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -28,6 +28,16 @@ public Mono loginWithKakao(String code) { .flatMap(kakaoLoginInfo -> getOrCreateMember(IdentityProvider.KAKAO, kakaoLoginInfo.id())); } + public Mono loginWithRefreshToken(String refreshToken){ + return Mono + .just(jwtTokenService.extractUserIdFromRefreshToken(refreshToken)) + .map(memberId -> { + String accessToken = jwtTokenService.generateAccessToken(memberId); + String newRefreshToken = jwtTokenService.generateRefreshToken(memberId); + return new AuthToken(accessToken, newRefreshToken); + }); + } + private Mono getOrCreateMember(IdentityProvider provider, String id) { SocialMemberEntityKey key = new SocialMemberEntityKey(provider, id); return socialMemberRepository diff --git a/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java b/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java index 875f1fb..eed3e35 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java @@ -1,5 +1,7 @@ package kr.mafoo.user.service; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwe; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import jakarta.annotation.PostConstruct; @@ -64,6 +66,18 @@ public String generateRefreshToken(String memberId) { .compact(); } + public String extractUserIdFromRefreshToken(String refreshToken){ + Jwe claims = parser + .parseEncryptedClaims(refreshToken); + + String type = (String) claims.getHeader().get(TOKEN_TYPE_HEADER_KEY); + if (!type.equals(REFRESH_TOKEN_TYPE_VALUE)){ + throw new IllegalArgumentException("Invalid token type"); + } + + return claims.getPayload().get(USER_ID_CLAIM_KEY, String.class); + } + private Date generateAccessTokenExpirationDate() { return new Date(System.currentTimeMillis() + 1000 * accessTokenExpiration); } From 67984410cbba486be2a3cf14ea6f21f2ddfd529d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Sun, 23 Jun 2024 19:55:07 +0900 Subject: [PATCH 4/8] fix: fix login related issues --- user-service/build.gradle.kts | 7 +++++-- .../kr/mafoo/user/UserServiceApplication.java | 2 ++ .../config/properties/KakaoOAuthProperties.java | 12 ++++++++++++ .../java/kr/mafoo/user/domain/MemberEntity.java | 8 +++++++- .../kr/mafoo/user/domain/SocialMemberEntity.java | 14 ++++++++++++-- .../user/repository/SocialMemberRepository.java | 4 +++- .../java/kr/mafoo/user/service/AuthService.java | 15 ++++++++++----- user-service/src/main/resources/application.yaml | 12 +++++++++++- .../src/main/resources/db/migration/V1__init.sql | 13 +++++++++++++ 9 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 user-service/src/main/java/kr/mafoo/user/config/properties/KakaoOAuthProperties.java create mode 100644 user-service/src/main/resources/db/migration/V1__init.sql diff --git a/user-service/build.gradle.kts b/user-service/build.gradle.kts index 04ba7f6..2778016 100644 --- a/user-service/build.gradle.kts +++ b/user-service/build.gradle.kts @@ -17,16 +17,19 @@ repositories { } dependencies { + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.flywaydb:flyway-core") implementation("org.flywaydb:flyway-mysql") +// https://mvnrepository.com/artifact/com.mysql/mysql-connector-j + implementation("com.mysql:mysql-connector-j:8.4.0") + implementation("org.springframework:spring-jdbc") implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.5.0") - runtimeOnly("org.mariadb:r2dbc-mariadb:1.1.3") - runtimeOnly("org.mariadb.jdbc:mariadb-java-client") + implementation("io.asyncer:r2dbc-mysql:1.1.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("io.projectreactor:reactor-test") testImplementation("org.springframework.security:spring-security-test") diff --git a/user-service/src/main/java/kr/mafoo/user/UserServiceApplication.java b/user-service/src/main/java/kr/mafoo/user/UserServiceApplication.java index 4f01f45..1bee103 100644 --- a/user-service/src/main/java/kr/mafoo/user/UserServiceApplication.java +++ b/user-service/src/main/java/kr/mafoo/user/UserServiceApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import reactor.blockhound.BlockHound; +@ConfigurationPropertiesScan @SpringBootApplication public class UserServiceApplication { diff --git a/user-service/src/main/java/kr/mafoo/user/config/properties/KakaoOAuthProperties.java b/user-service/src/main/java/kr/mafoo/user/config/properties/KakaoOAuthProperties.java new file mode 100644 index 0000000..6f983ee --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/config/properties/KakaoOAuthProperties.java @@ -0,0 +1,12 @@ +package kr.mafoo.user.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; + +@ConfigurationProperties(prefix = "app.oauth.kakao") +@ConfigurationPropertiesBinding +public record KakaoOAuthProperties( + String clientId, + String redirectUri +) { +} diff --git a/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java b/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java index b257aae..1ff8c2f 100644 --- a/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java +++ b/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java @@ -5,6 +5,8 @@ import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.domain.Persistable; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; @@ -14,7 +16,7 @@ @Setter @NoArgsConstructor @Table("member") -public class MemberEntity { +public class MemberEntity implements Persistable { @Id @Column("member_id") private String id; @@ -26,6 +28,9 @@ public class MemberEntity { @Column("created_at") private LocalDateTime createdAt; + @Transient + private boolean isNew = false; + @Override public boolean equals(Object obj) { if (this == obj) return true; @@ -45,6 +50,7 @@ public static MemberEntity newMember(String id, String name) { MemberEntity member = new MemberEntity(); member.id = id; member.name = name; + member.isNew = true; return member; } } diff --git a/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntity.java b/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntity.java index 0234c8c..05054f6 100644 --- a/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntity.java +++ b/user-service/src/main/java/kr/mafoo/user/domain/SocialMemberEntity.java @@ -6,6 +6,8 @@ import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.domain.Persistable; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; @@ -16,11 +18,11 @@ @Setter @NoArgsConstructor @Table("social_member") -public class SocialMemberEntity { +public class SocialMemberEntity implements Persistable { @Column("identity_provider") private IdentityProvider identityProvider; - @Column("id") + @Column("identifier") private String id; @CreatedDate @@ -30,6 +32,9 @@ public class SocialMemberEntity { @Column("member_id") private String memberId; + @Transient + private boolean isNew = false; + @Override public boolean equals(Object obj) { if (this == obj) return true; @@ -40,6 +45,10 @@ public boolean equals(Object obj) { return id.equals(that.id) && identityProvider.equals(that.identityProvider); } + public SocialMemberEntityKey getId() { + return new SocialMemberEntityKey(identityProvider, id); + } + @Override public int hashCode() { return Objects.hash(id, identityProvider); @@ -50,6 +59,7 @@ public static SocialMemberEntity newSocialMember(IdentityProvider identityProvid socialMember.identityProvider = identityProvider; socialMember.id = id; socialMember.memberId = memberId; + socialMember.isNew = true; return socialMember; } } diff --git a/user-service/src/main/java/kr/mafoo/user/repository/SocialMemberRepository.java b/user-service/src/main/java/kr/mafoo/user/repository/SocialMemberRepository.java index fb7dce2..1ba57ab 100644 --- a/user-service/src/main/java/kr/mafoo/user/repository/SocialMemberRepository.java +++ b/user-service/src/main/java/kr/mafoo/user/repository/SocialMemberRepository.java @@ -2,8 +2,10 @@ import kr.mafoo.user.domain.SocialMemberEntity; import kr.mafoo.user.domain.SocialMemberEntityKey; +import kr.mafoo.user.enums.IdentityProvider; import org.springframework.data.r2dbc.repository.R2dbcRepository; +import reactor.core.publisher.Mono; public interface SocialMemberRepository extends R2dbcRepository { - + Mono findByIdentityProviderAndId(IdentityProvider identityProvider, String id); } diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index 4870f31..1d040bd 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -1,5 +1,6 @@ package kr.mafoo.user.service; +import kr.mafoo.user.config.properties.KakaoOAuthProperties; import kr.mafoo.user.controller.dto.response.KakaoLoginInfo; import kr.mafoo.user.domain.AuthToken; import kr.mafoo.user.domain.SocialMemberEntity; @@ -20,6 +21,7 @@ public class AuthService { private final SocialMemberRepository socialMemberRepository; private final MemberService memberService; private final JWTTokenService jwtTokenService; + private final KakaoOAuthProperties kakaoOAuthProperties; public Mono loginWithKakao(String code) { @@ -39,9 +41,8 @@ public Mono loginWithRefreshToken(String refreshToken){ } private Mono getOrCreateMember(IdentityProvider provider, String id) { - SocialMemberEntityKey key = new SocialMemberEntityKey(provider, id); return socialMemberRepository - .findById(key) + .findByIdentityProviderAndId(provider, id) .switchIfEmpty(createNewSocialMember(provider, id)) .map(socialMember -> { String accessToken = jwtTokenService.generateAccessToken(socialMember.getMemberId()); @@ -64,8 +65,12 @@ private Mono createNewSocialMember(IdentityProvider provider private Mono getKakaoTokenWithCode(String code) { return externalWebClient .post() - .uri("https://kauth.kakao.com/oauth/token") - .bodyValue("grant_type=authorization_code&client_id=client_id&redirect_uri=http://localhost:8080/login/kakao&code=" + code) + .uri("https://kauth.kakao.com/oauth/token" + "?grant_type=authorization_code&client_id=" + + kakaoOAuthProperties.clientId() + + "&redirect_uri=" + + kakaoOAuthProperties.redirectUri() + +"&code=" + + code) .retrieve() .bodyToMono(LinkedHashMap.class) .map(map -> (String) map.get("access_token")); @@ -79,7 +84,7 @@ private Mono getUserInfoWithKakaoToken(String kakaoToken){ .retrieve() .bodyToMono(LinkedHashMap.class) .map(map -> new KakaoLoginInfo( - (String) map.get("id"), + String.valueOf((Long) map.get("id")), (String) map.get("kakao_account.email"), (String) map.get("properties.nickname")) ); diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index 49cf8f3..6bcd8a4 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -1,6 +1,12 @@ spring: application: name: mafoo-user-service + flyway: + url: ${FLYWAY_URL} + baseline-on-migrate: true + enabled: true + user: ${MYSQL_USERNAME} + password: ${MYSQL_PASSWORD} r2dbc: url: ${MYSQL_URL} username: ${MYSQL_USERNAME} @@ -10,8 +16,12 @@ logging: org.springframework.data.r2dbc: DEBUG app: + oauth: + kakao: + client-id: ${KAKAO_CLIENT_ID} + redirect-uri: ${KAKAO_REDIRECT_URL} jwt: - verify-key: askdgjasdgjasdkgasjdgkasjdgkasjd832f892sdf + verify-key: ${JWT_VERIFY_KEY} expiration: access-token: 86400 # 1 day refresh-token: 2592000 # 30 days diff --git a/user-service/src/main/resources/db/migration/V1__init.sql b/user-service/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 0000000..8f1f511 --- /dev/null +++ b/user-service/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,13 @@ +CREATE TABLE member( + `member_id` CHAR(26) PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE social_member( + `identity_provider` VARCHAR(64) NOT NULL, + `identifier` VARCHAR(255) NOT NULL, + `member_id` CHAR(26) NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`identity_provider`, `identifier`) +); From 53dcf3a988617d19078584809670144f3e246a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Sun, 23 Jun 2024 20:16:53 +0900 Subject: [PATCH 5/8] feat: update to non-blocking call --- .../mafoo/user/config/JacksonSerializer.java | 72 +++++++++++++++++++ .../mafoo/user/service/JWTTokenService.java | 7 ++ 2 files changed, 79 insertions(+) create mode 100644 user-service/src/main/java/kr/mafoo/user/config/JacksonSerializer.java diff --git a/user-service/src/main/java/kr/mafoo/user/config/JacksonSerializer.java b/user-service/src/main/java/kr/mafoo/user/config/JacksonSerializer.java new file mode 100644 index 0000000..16ce785 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/config/JacksonSerializer.java @@ -0,0 +1,72 @@ +package kr.mafoo.user.config; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.jsonwebtoken.io.AbstractSerializer; +import io.jsonwebtoken.lang.Assert; + +import java.io.OutputStream; + +public class JacksonSerializer extends AbstractSerializer { + + static final String MODULE_ID = "jjwt-jackson"; + static final Module MODULE; + + static { + SimpleModule module = new SimpleModule(MODULE_ID); + // module.addSerializer(JacksonSupplierSerializer.INSTANCE); + MODULE = module; + } + + static final ObjectMapper DEFAULT_OBJECT_MAPPER = newObjectMapper(); + + /** + * Creates and returns a new ObjectMapper with the {@code jjwt-jackson} module registered and + * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true) and + * {@code DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES} disabled (set to false). + * + * @return a new ObjectMapper with the {@code jjwt-jackson} module registered and + * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true) and + * {@code DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES} disabled (set to false). + * + * @since 0.12.4 + */ + // package protected on purpose, do not expose to the public API + static ObjectMapper newObjectMapper() { + return new ObjectMapper() + .registerModule(MODULE) + .configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true) // https://github.com/jwtk/jjwt/issues/877 + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // https://github.com/jwtk/jjwt/issues/893 + } + + protected final ObjectMapper objectMapper; + + /** + * Constructor using JJWT's default {@link ObjectMapper} singleton for serialization. + */ + public JacksonSerializer() { + this(DEFAULT_OBJECT_MAPPER); + } + + /** + * Creates a new Jackson Serializer that uses the specified {@link ObjectMapper} for serialization. + * + * @param objectMapper the ObjectMapper to use for serialization. + */ + public JacksonSerializer(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "ObjectMapper cannot be null."); + this.objectMapper = objectMapper.registerModule(MODULE); + } + + @Override + protected void doSerialize(T t, OutputStream out) throws Exception { + Assert.notNull(out, "OutputStream cannot be null."); + ObjectWriter writer = this.objectMapper.writer().without(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + writer.writeValue(out, t); + } +} diff --git a/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java b/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java index eed3e35..d71a140 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java @@ -5,6 +5,7 @@ import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import jakarta.annotation.PostConstruct; +import kr.mafoo.user.config.JacksonSerializer; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -23,9 +24,13 @@ public class JWTTokenService { @Value("${app.jwt.expiration.refresh-token}") private Long refreshTokenExpiration; + + private SecretKey signKey = null; private JwtParser parser = null; + private JacksonSerializer jacksonSerializer = new JacksonSerializer(); + @PostConstruct public void initSignKey() { signKey = new SecretKeySpec(verifyKey.getBytes(), "AES"); @@ -50,6 +55,7 @@ public String generateAccessToken(String memberId) { .and() .expiration(generateAccessTokenExpirationDate()) .encryptWith(signKey, Jwts.ENC.A128CBC_HS256) + .json(jacksonSerializer) .compact(); } @@ -63,6 +69,7 @@ public String generateRefreshToken(String memberId) { .and() .expiration(generateRefreshTokenExpirationDate()) .encryptWith(signKey, Jwts.ENC.A128CBC_HS256) + .json(jacksonSerializer) .compact(); } From ba972584fc822a1822c6bb84e9e561067f7ece90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Sun, 23 Jun 2024 20:55:39 +0900 Subject: [PATCH 6/8] feat: add error handling --- .../kr/mafoo/user/exception/ErrorCode.java | 8 ++++++- .../user/exception/InvalidTokenException.java | 7 +++++++ .../exception/KakaoLoginFailedException.java | 7 +++++++ .../user/exception/TokenExpiredException.java | 7 +++++++ .../exception/TokenTypeMismatchException.java | 7 +++++++ .../kr/mafoo/user/service/AuthService.java | 6 ++++-- .../mafoo/user/service/JWTTokenService.java | 21 ++++++++++++------- 7 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 user-service/src/main/java/kr/mafoo/user/exception/InvalidTokenException.java create mode 100644 user-service/src/main/java/kr/mafoo/user/exception/KakaoLoginFailedException.java create mode 100644 user-service/src/main/java/kr/mafoo/user/exception/TokenExpiredException.java create mode 100644 user-service/src/main/java/kr/mafoo/user/exception/TokenTypeMismatchException.java diff --git a/user-service/src/main/java/kr/mafoo/user/exception/ErrorCode.java b/user-service/src/main/java/kr/mafoo/user/exception/ErrorCode.java index 59f0902..5f05daa 100644 --- a/user-service/src/main/java/kr/mafoo/user/exception/ErrorCode.java +++ b/user-service/src/main/java/kr/mafoo/user/exception/ErrorCode.java @@ -6,7 +6,13 @@ @Getter @RequiredArgsConstructor public enum ErrorCode { - MEMBER_NOT_FOUND("ME0001", "사용자를 찾을 수 없습니다"); + MEMBER_NOT_FOUND("ME0001", "사용자를 찾을 수 없습니다"), + KAKAO_LOGIN_FAILED("EX0001", "카카오 로그인에 실패했습니다"), + + TOKEN_TYPE_MISMATCH("AU0001", "토큰 타입이 일치하지 않습니다. (아마 AccessToken?)"), + TOKEN_EXPIRED("AU0002", "토큰이 만료되었습니다"), + TOKEN_INVALID("AU0003", "토큰이 유효하지 않습니다"), + ; private final String code; private final String message; } diff --git a/user-service/src/main/java/kr/mafoo/user/exception/InvalidTokenException.java b/user-service/src/main/java/kr/mafoo/user/exception/InvalidTokenException.java new file mode 100644 index 0000000..2124ad0 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/exception/InvalidTokenException.java @@ -0,0 +1,7 @@ +package kr.mafoo.user.exception; + +public class InvalidTokenException extends DomainException { + public InvalidTokenException() { + super(ErrorCode.TOKEN_INVALID); + } +} diff --git a/user-service/src/main/java/kr/mafoo/user/exception/KakaoLoginFailedException.java b/user-service/src/main/java/kr/mafoo/user/exception/KakaoLoginFailedException.java new file mode 100644 index 0000000..74489c3 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/exception/KakaoLoginFailedException.java @@ -0,0 +1,7 @@ +package kr.mafoo.user.exception; + +public class KakaoLoginFailedException extends DomainException { + public KakaoLoginFailedException() { + super(ErrorCode.KAKAO_LOGIN_FAILED); + } +} diff --git a/user-service/src/main/java/kr/mafoo/user/exception/TokenExpiredException.java b/user-service/src/main/java/kr/mafoo/user/exception/TokenExpiredException.java new file mode 100644 index 0000000..5ab3d90 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/exception/TokenExpiredException.java @@ -0,0 +1,7 @@ +package kr.mafoo.user.exception; + +public class TokenExpiredException extends DomainException { + public TokenExpiredException() { + super(ErrorCode.TOKEN_EXPIRED); + } +} diff --git a/user-service/src/main/java/kr/mafoo/user/exception/TokenTypeMismatchException.java b/user-service/src/main/java/kr/mafoo/user/exception/TokenTypeMismatchException.java new file mode 100644 index 0000000..98831b5 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/exception/TokenTypeMismatchException.java @@ -0,0 +1,7 @@ +package kr.mafoo.user.exception; + +public class TokenTypeMismatchException extends DomainException { + public TokenTypeMismatchException() { + super(ErrorCode.TOKEN_TYPE_MISMATCH); + } +} diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index 1d040bd..cde0c7e 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -4,8 +4,8 @@ import kr.mafoo.user.controller.dto.response.KakaoLoginInfo; import kr.mafoo.user.domain.AuthToken; import kr.mafoo.user.domain.SocialMemberEntity; -import kr.mafoo.user.domain.SocialMemberEntityKey; import kr.mafoo.user.enums.IdentityProvider; +import kr.mafoo.user.exception.KakaoLoginFailedException; import kr.mafoo.user.repository.SocialMemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -32,7 +32,7 @@ public Mono loginWithKakao(String code) { public Mono loginWithRefreshToken(String refreshToken){ return Mono - .just(jwtTokenService.extractUserIdFromRefreshToken(refreshToken)) + .fromCallable(() -> jwtTokenService.extractUserIdFromRefreshToken(refreshToken)) .map(memberId -> { String accessToken = jwtTokenService.generateAccessToken(memberId); String newRefreshToken = jwtTokenService.generateRefreshToken(memberId); @@ -72,6 +72,7 @@ private Mono getKakaoTokenWithCode(String code) { +"&code=" + code) .retrieve() + .onStatus(status -> !status.is2xxSuccessful(), (res) -> Mono.error(new KakaoLoginFailedException())) .bodyToMono(LinkedHashMap.class) .map(map -> (String) map.get("access_token")); } @@ -82,6 +83,7 @@ private Mono getUserInfoWithKakaoToken(String kakaoToken){ .uri("https://kapi.kakao.com/v2/user/me") .headers(headers -> headers.setBearerAuth(kakaoToken)) .retrieve() + .onStatus(status -> !status.is2xxSuccessful(), (res) -> Mono.error(new KakaoLoginFailedException())) .bodyToMono(LinkedHashMap.class) .map(map -> new KakaoLoginInfo( String.valueOf((Long) map.get("id")), diff --git a/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java b/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java index d71a140..e2dd56a 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/JWTTokenService.java @@ -1,11 +1,11 @@ package kr.mafoo.user.service; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwe; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.*; import jakarta.annotation.PostConstruct; import kr.mafoo.user.config.JacksonSerializer; +import kr.mafoo.user.exception.InvalidTokenException; +import kr.mafoo.user.exception.TokenExpiredException; +import kr.mafoo.user.exception.TokenTypeMismatchException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -74,12 +74,19 @@ public String generateRefreshToken(String memberId) { } public String extractUserIdFromRefreshToken(String refreshToken){ - Jwe claims = parser - .parseEncryptedClaims(refreshToken); + Jwe claims; + try { + claims = parser + .parseEncryptedClaims(refreshToken); + } catch(ExpiredJwtException e){ + throw new TokenExpiredException(); + } catch (Exception e){ + throw new InvalidTokenException(); + } String type = (String) claims.getHeader().get(TOKEN_TYPE_HEADER_KEY); if (!type.equals(REFRESH_TOKEN_TYPE_VALUE)){ - throw new IllegalArgumentException("Invalid token type"); + throw new TokenTypeMismatchException(); } return claims.getPayload().get(USER_ID_CLAIM_KEY, String.class); From b53570ac28ded00d343c70741017801f2dda0939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=98=81=EB=AF=BC?= Date: Sun, 23 Jun 2024 21:18:59 +0900 Subject: [PATCH 7/8] feat: add nickname fetch feature --- .../kr/mafoo/user/service/AuthService.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index cde0c7e..4e3a92e 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -27,7 +27,11 @@ public class AuthService { public Mono loginWithKakao(String code) { return getKakaoTokenWithCode(code) .flatMap(this::getUserInfoWithKakaoToken) - .flatMap(kakaoLoginInfo -> getOrCreateMember(IdentityProvider.KAKAO, kakaoLoginInfo.id())); + .flatMap(kakaoLoginInfo -> getOrCreateMember( + IdentityProvider.KAKAO, + kakaoLoginInfo.id(), + kakaoLoginInfo.nickname() + )); } public Mono loginWithRefreshToken(String refreshToken){ @@ -40,10 +44,10 @@ public Mono loginWithRefreshToken(String refreshToken){ }); } - private Mono getOrCreateMember(IdentityProvider provider, String id) { + private Mono getOrCreateMember(IdentityProvider provider, String id, String username) { return socialMemberRepository .findByIdentityProviderAndId(provider, id) - .switchIfEmpty(createNewSocialMember(provider, id)) + .switchIfEmpty(createNewSocialMember(provider, id, username)) .map(socialMember -> { String accessToken = jwtTokenService.generateAccessToken(socialMember.getMemberId()); String refreshToken = jwtTokenService.generateRefreshToken(socialMember.getMemberId()); @@ -51,9 +55,9 @@ private Mono getOrCreateMember(IdentityProvider provider, String id) }); } - private Mono createNewSocialMember(IdentityProvider provider, String id) { + private Mono createNewSocialMember(IdentityProvider provider, String id, String username) { return memberService - .createNewMember(id) + .createNewMember(username) .flatMap(newMember -> socialMemberRepository.save( SocialMemberEntity.newSocialMember(provider, id, newMember.getId()) )); @@ -86,9 +90,9 @@ private Mono getUserInfoWithKakaoToken(String kakaoToken){ .onStatus(status -> !status.is2xxSuccessful(), (res) -> Mono.error(new KakaoLoginFailedException())) .bodyToMono(LinkedHashMap.class) .map(map -> new KakaoLoginInfo( - String.valueOf((Long) map.get("id")), - (String) map.get("kakao_account.email"), - (String) map.get("properties.nickname")) - ); + String.valueOf((Long) map.get("id")), + (String) ((LinkedHashMap)map.get("properties")).get("nickname"), + (String) map.get("kakao_account.email") + )); } } From f0e336d2d0fe32eaeaf867ac3268834ec40d85dc Mon Sep 17 00:00:00 2001 From: ChuYong Date: Sat, 29 Jun 2024 13:08:06 +0900 Subject: [PATCH 8/8] feat: add kakao client secret --- .../mafoo/user/config/properties/KakaoOAuthProperties.java | 3 ++- .../src/main/java/kr/mafoo/user/service/AuthService.java | 7 +++++-- user-service/src/main/resources/application.yaml | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/config/properties/KakaoOAuthProperties.java b/user-service/src/main/java/kr/mafoo/user/config/properties/KakaoOAuthProperties.java index 6f983ee..fabc6db 100644 --- a/user-service/src/main/java/kr/mafoo/user/config/properties/KakaoOAuthProperties.java +++ b/user-service/src/main/java/kr/mafoo/user/config/properties/KakaoOAuthProperties.java @@ -7,6 +7,7 @@ @ConfigurationPropertiesBinding public record KakaoOAuthProperties( String clientId, - String redirectUri + String redirectUri, + String clientSecret ) { } diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index 4e3a92e..534c139 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -74,7 +74,10 @@ private Mono getKakaoTokenWithCode(String code) { + "&redirect_uri=" + kakaoOAuthProperties.redirectUri() +"&code=" - + code) + + code + + "&client_secret=" + + kakaoOAuthProperties.clientSecret() + ) .retrieve() .onStatus(status -> !status.is2xxSuccessful(), (res) -> Mono.error(new KakaoLoginFailedException())) .bodyToMono(LinkedHashMap.class) @@ -90,7 +93,7 @@ private Mono getUserInfoWithKakaoToken(String kakaoToken){ .onStatus(status -> !status.is2xxSuccessful(), (res) -> Mono.error(new KakaoLoginFailedException())) .bodyToMono(LinkedHashMap.class) .map(map -> new KakaoLoginInfo( - String.valueOf((Long) map.get("id")), + String.valueOf(map.get("id")), (String) ((LinkedHashMap)map.get("properties")).get("nickname"), (String) map.get("kakao_account.email") )); diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index 6bcd8a4..beaf435 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -20,6 +20,7 @@ app: kakao: client-id: ${KAKAO_CLIENT_ID} redirect-uri: ${KAKAO_REDIRECT_URL} + client-secret: ${KAKAO_CLIENT_SECRET} jwt: verify-key: ${JWT_VERIFY_KEY} expiration: