diff --git a/src/main/java/com/apps/pochak/PochakApplication.java b/src/main/java/com/apps/pochak/PochakApplication.java index 861236c1..64cf1407 100644 --- a/src/main/java/com/apps/pochak/PochakApplication.java +++ b/src/main/java/com/apps/pochak/PochakApplication.java @@ -21,7 +21,6 @@ public static void main(String[] args) { @PostConstruct public void init() { - // timezone 설정 TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); } } diff --git a/src/main/java/com/apps/pochak/alarm/service/AlarmService.java b/src/main/java/com/apps/pochak/alarm/service/AlarmService.java index 2d6edb5b..00d843a7 100644 --- a/src/main/java/com/apps/pochak/alarm/service/AlarmService.java +++ b/src/main/java/com/apps/pochak/alarm/service/AlarmService.java @@ -4,7 +4,7 @@ import com.apps.pochak.alarm.domain.repository.AlarmRepository; import com.apps.pochak.alarm.dto.response.AlarmElements; import com.apps.pochak.global.api_payload.code.BaseCode; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -19,17 +19,17 @@ @RequiredArgsConstructor public class AlarmService { private final AlarmRepository alarmRepository; - private final JwtService jwtService; + private final JwtProvider jwtProvider; @Transactional(readOnly = true) public AlarmElements getAllAlarms(Pageable pageable) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Page alarmPage = alarmRepository.getAllAlarm(loginMember.getId(), pageable); return new AlarmElements(alarmPage); } public BaseCode checkAlarm(Long alarmId) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Alarm alarm = alarmRepository.findAlarmById(alarmId, loginMember); alarm.setIsChecked(true); return SUCCESS_CHECK_ALARM; diff --git a/src/main/java/com/apps/pochak/block/service/BlockService.java b/src/main/java/com/apps/pochak/block/service/BlockService.java index 5fa40d9a..558c03ca 100644 --- a/src/main/java/com/apps/pochak/block/service/BlockService.java +++ b/src/main/java/com/apps/pochak/block/service/BlockService.java @@ -6,7 +6,7 @@ import com.apps.pochak.follow.domain.repository.FollowRepository; import com.apps.pochak.global.api_payload.exception.GeneralException; import com.apps.pochak.like.domain.repository.LikeRepository; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.member.domain.repository.MemberRepository; import com.apps.pochak.post.domain.repository.PostRepository; @@ -29,10 +29,10 @@ public class BlockService { private final LikeRepository likeRepository; private final PostRepository postRepository; - private final JwtService jwtService; + private final JwtProvider jwtProvider; public void blockMember(String handle) { - Member blocker = jwtService.getLoginMember(); + Member blocker = jwtProvider.getLoginMember(); Member blockedMember = memberRepository.findByHandle(handle, blocker); if (blocker.getId().equals(blockedMember.getId())) { @@ -62,7 +62,7 @@ public BlockElements getBlockedMember( final String handle, final Pageable pageable ) { - Member loginMember = jwtService.getLoginMember(); + Member loginMember = jwtProvider.getLoginMember(); Member member = memberRepository.findByHandleWithoutLogin(handle); if (!member.equals(loginMember)) { @@ -80,7 +80,7 @@ public void cancelBlock( final String handle, final String blockedMemberHandle ) { - Member loginMember = jwtService.getLoginMember(); + Member loginMember = jwtProvider.getLoginMember(); Member blocker = memberRepository.findByHandleWithoutLogin(handle); if (!loginMember.equals(blocker)) { diff --git a/src/main/java/com/apps/pochak/comment/service/CommentService.java b/src/main/java/com/apps/pochak/comment/service/CommentService.java index daecd203..91458938 100644 --- a/src/main/java/com/apps/pochak/comment/service/CommentService.java +++ b/src/main/java/com/apps/pochak/comment/service/CommentService.java @@ -10,7 +10,7 @@ import com.apps.pochak.comment.dto.response.CommentElements; import com.apps.pochak.comment.dto.response.ParentCommentElement; import com.apps.pochak.global.api_payload.exception.GeneralException; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.post.domain.Post; import com.apps.pochak.post.domain.repository.PostRepository; @@ -37,14 +37,14 @@ public class CommentService { private final PostRepository postRepository; private final AlarmRepository alarmRepository; - private final JwtService jwtService; + private final JwtProvider jwtProvider; @Transactional(readOnly = true) public CommentElements getComments( final Long postId, final Pageable pageable ) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Post post = postRepository.findPublicPostById(postId, loginMember); final Page commentList = commentRepository.findParentCommentByPost(post, loginMember, pageable); return new CommentElements(loginMember, commentList); @@ -56,7 +56,7 @@ public ParentCommentElement getChildCommentsByParentCommentId( final Long parentCommentId, final Pageable pageable ) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Comment comment = commentRepository.findParentCommentById(parentCommentId, loginMember) .orElseThrow(() -> new GeneralException(INVALID_POST_ID)); return new ParentCommentElement(comment, toPageRequest(pageable)); @@ -66,7 +66,7 @@ public void saveComment( final Long postId, final CommentUploadRequest request ) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Post post = postRepository.findPublicPostById(postId, loginMember); if (request.checkChildComment()) { @@ -179,7 +179,7 @@ public void deleteComment( } private void checkAuthorized(final Comment comment) { - Member member = jwtService.getLoginMember(); + Member member = jwtProvider.getLoginMember(); if (comment.isOwner(member)) return; Post post = comment.getPost(); diff --git a/src/main/java/com/apps/pochak/follow/service/FollowService.java b/src/main/java/com/apps/pochak/follow/service/FollowService.java index 8250ea16..a783028e 100644 --- a/src/main/java/com/apps/pochak/follow/service/FollowService.java +++ b/src/main/java/com/apps/pochak/follow/service/FollowService.java @@ -1,13 +1,12 @@ package com.apps.pochak.follow.service; -import com.apps.pochak.alarm.domain.Alarm; import com.apps.pochak.alarm.domain.FollowAlarm; import com.apps.pochak.alarm.domain.repository.AlarmRepository; import com.apps.pochak.follow.domain.Follow; import com.apps.pochak.follow.domain.repository.FollowRepository; import com.apps.pochak.global.api_payload.code.BaseCode; import com.apps.pochak.global.api_payload.exception.GeneralException; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.member.domain.repository.CustomMemberRepository; import com.apps.pochak.member.domain.repository.MemberRepository; @@ -19,7 +18,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; import java.util.Optional; import static com.apps.pochak.global.BaseEntityStatus.ACTIVE; @@ -35,10 +33,10 @@ public class FollowService { private final AlarmRepository alarmRepository; private final MemberRepository memberRepository; private final CustomMemberRepository customMemberRepository; - private final JwtService jwtService; + private final JwtProvider jwtProvider; public BaseCode follow(final String handle) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Member member = memberRepository.findByHandle(handle, loginMember); if (loginMember.getId().equals(member.getId())) { @@ -95,7 +93,7 @@ public BaseCode deleteFollower(final String handle, final String followerHandle ) { // TODO: Refactor permission checking part using annotations. - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); if (!loginMember.getHandle().equals(handle)) { throw new GeneralException(_UNAUTHORIZED); } @@ -113,7 +111,7 @@ public BaseCode deleteFollower(final String handle, public MemberElements getFollowings(final String handle, final Pageable pageable ) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Member member = memberRepository.findByHandle(handle, loginMember); final Page followingPage = customMemberRepository.findFollowingsAndIsFollow( member, @@ -128,7 +126,7 @@ public MemberElements getFollowings(final String handle, public MemberElements getFollowers(final String handle, final Pageable pageable ) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Member member = memberRepository.findByHandle(handle, loginMember); final Page followerPage = customMemberRepository.findFollowersAndIsFollow( member, diff --git a/src/main/java/com/apps/pochak/global/api_payload/code/status/ErrorStatus.java b/src/main/java/com/apps/pochak/global/api_payload/code/status/ErrorStatus.java index c0c5f8ff..693376ac 100644 --- a/src/main/java/com/apps/pochak/global/api_payload/code/status/ErrorStatus.java +++ b/src/main/java/com/apps/pochak/global/api_payload/code/status/ErrorStatus.java @@ -6,6 +6,8 @@ import lombok.Getter; import org.springframework.http.HttpStatus; +import java.security.NoSuchAlgorithmException; + import static org.springframework.http.HttpStatus.*; @Getter @@ -43,17 +45,24 @@ public enum ErrorStatus implements BaseErrorCode { INVALID_REFRESH_TOKEN(BAD_REQUEST, "LOGIN4002", "잘못된 리프레시 토큰입니다."), INVALID_TOKEN_SIGNATURE(BAD_REQUEST, "LOGIN4003", "잘못된 토큰 서명입니다."), UNSUPPORTED_TOKEN(BAD_REQUEST, "LOGIN4004", "지원하지 않는 형식의 토큰입니다."), - MALFORMED_TOKEN(BAD_REQUEST, "LOGIN4005", "유효하지 않은 구성의 토큰입니다."), NULL_TOKEN(BAD_REQUEST, "LOGIN4006", "토큰이 존재하지 않습니다."), EXIST_USER(BAD_REQUEST, "LOGIN4007", "존재하는 유저입니다."), NULL_REFRESH_TOKEN(BAD_REQUEST, "LOGIN4008", "리프레시 토큰이 존재하지 않습니다."), EXPIRED_ACCESS_TOKEN(UNAUTHORIZED, "LOGIN4009", "만료된 액세스 토큰입니다."), EXPIRED_REFRESH_TOKEN(UNAUTHORIZED, "LOGIN4010", "만료된 리프레시 토큰입니다."), - INVALID_PUBLIC_KEY(BAD_REQUEST, "LOGIN4011", "공개키를 가져올 수 없습니다."), - INVALID_USER_INFO(BAD_REQUEST, "LOGIN4012", "유저 정보를 가져올 수 없습니다."), - INVALID_OAUTH_TOKEN(BAD_REQUEST, "LOGIN4013", "토큰을 가져올 수 없습니다."), + INVALID_USER_INFO(BAD_REQUEST, "LOGIN4011", "유저 정보를 가져올 수 없습니다."), + INVALID_OAUTH_TOKEN(BAD_REQUEST, "LOGIN4012", "토큰을 가져올 수 없습니다."), FAIL_VALIDATE_TOKEN(BAD_REQUEST, "LOGIN4013", "토큰 유효성 검사 중 오류가 발생했습니다."), + // Apple Login + FAIL_VALIDATE_PUBLIC_KEY(BAD_REQUEST, "APPLE4001", "애플로그인 공개키 조회에 실패하였습니다."), + MALFORMED_TOKEN(BAD_REQUEST, "APPLE4002", "유효하지 않은 구성의 토큰입니다."), + INVALID_PUBLIC_KEY(BAD_REQUEST, "APPLE4003", "공개키를 가져올 수 없습니다."), + JSON_PROCESSING_EXCEPTION(INTERNAL_SERVER_ERROR, "APPLE4004", "idToken 파싱에 실패하였습니다."), + NO_SUCH_ALGORITHM(INTERNAL_SERVER_ERROR, "APPLE5001", "Null algorithm name"), + INVALID_KEY_SPEC(INTERNAL_SERVER_ERROR, "APPLE5002", "Could not generate public key."), + + // Member INVALID_MEMBER_ID(BAD_REQUEST, "MEMBER4001", "유효하지 않은 멤버의 아이디입니다."), INVALID_MEMBER_HANDLE(BAD_REQUEST, "MEMBER4002", "유효하지 않은 멤버의 handle입니다."), diff --git a/src/main/java/com/apps/pochak/global/api_payload/exception/handler/AuthenticationException.java b/src/main/java/com/apps/pochak/global/api_payload/exception/handler/AuthenticationException.java new file mode 100644 index 00000000..c0045f5d --- /dev/null +++ b/src/main/java/com/apps/pochak/global/api_payload/exception/handler/AuthenticationException.java @@ -0,0 +1,11 @@ +package com.apps.pochak.global.api_payload.exception.handler; + +import com.apps.pochak.global.api_payload.code.BaseErrorCode; +import com.apps.pochak.global.api_payload.exception.GeneralException; + +public class AuthenticationException extends GeneralException { + + public AuthenticationException(BaseErrorCode code) { + super(code); + } +} \ No newline at end of file diff --git a/src/main/java/com/apps/pochak/like/service/LikeService.java b/src/main/java/com/apps/pochak/like/service/LikeService.java index bc51cff2..64e22924 100644 --- a/src/main/java/com/apps/pochak/like/service/LikeService.java +++ b/src/main/java/com/apps/pochak/like/service/LikeService.java @@ -7,7 +7,7 @@ import com.apps.pochak.like.domain.repository.LikeRepository; import com.apps.pochak.like.dto.response.LikeElement; import com.apps.pochak.like.dto.response.LikeElements; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.post.domain.Post; import com.apps.pochak.post.domain.repository.PostRepository; @@ -32,10 +32,10 @@ public class LikeService { private final PostRepository postRepository; private final TagRepository tagRepository; private final AlarmRepository alarmRepository; - private final JwtService jwtService; + private final JwtProvider jwtProvider; public void likePost(final Long postId) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Post post = postRepository.findPostById(postId, loginMember); final Optional optionalLike = likeRepository.findByLikeMemberAndLikedPost(loginMember, post); @@ -89,7 +89,7 @@ private void deleteAlarm(LikeEntity like) { @Transactional(readOnly = true) public LikeElements getMemberLikedPost(final Long postId) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Post likedPost = postRepository.findPostById(postId, loginMember); final List likeElements = likeRepository.findLikesAndIsFollow( diff --git a/src/main/java/com/apps/pochak/login/config/SecurityConfig.java b/src/main/java/com/apps/pochak/login/config/SecurityConfig.java index 2e9ef7e2..6ccb8f99 100644 --- a/src/main/java/com/apps/pochak/login/config/SecurityConfig.java +++ b/src/main/java/com/apps/pochak/login/config/SecurityConfig.java @@ -1,7 +1,7 @@ package com.apps.pochak.login.config; -import com.apps.pochak.login.jwt.JwtAuthorizationFilter; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.filter.JwtAuthorizationFilter; +import com.apps.pochak.login.provider.JwtProvider; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,7 +18,7 @@ @RequiredArgsConstructor public class SecurityConfig { - private final JwtService jwtService; + private final JwtProvider jwtProvider; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -38,6 +38,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public JwtAuthorizationFilter jwtAuthorizationFilter() { - return new JwtAuthorizationFilter(jwtService); + return new JwtAuthorizationFilter(jwtProvider); } } diff --git a/src/main/java/com/apps/pochak/login/controller/OAuthController.java b/src/main/java/com/apps/pochak/login/controller/OAuthController.java index ef991606..05d66228 100644 --- a/src/main/java/com/apps/pochak/login/controller/OAuthController.java +++ b/src/main/java/com/apps/pochak/login/controller/OAuthController.java @@ -2,20 +2,16 @@ import com.apps.pochak.global.api_payload.ApiResponse; import com.apps.pochak.login.dto.request.MemberInfoRequest; +import com.apps.pochak.login.dto.response.AccessTokenResponse; import com.apps.pochak.login.dto.response.OAuthMemberResponse; -import com.apps.pochak.login.dto.response.PostTokenResponse; -import com.apps.pochak.login.jwt.JwtHeaderUtil; -import com.apps.pochak.login.jwt.JwtService; -import com.apps.pochak.login.oauth.AppleOAuthService; -import com.apps.pochak.login.oauth.GoogleOAuthService; -import com.apps.pochak.login.oauth.OAuthService; -import com.fasterxml.jackson.core.JsonProcessingException; +import com.apps.pochak.login.provider.JwtProvider; +import com.apps.pochak.login.service.AppleOAuthService; +import com.apps.pochak.login.service.GoogleOAuthService; +import com.apps.pochak.login.service.OAuthService; +import com.apps.pochak.login.util.JwtHeaderUtil; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - import static com.apps.pochak.global.Constant.HEADER_APPLE_AUTHORIZATION_CODE; import static com.apps.pochak.global.Constant.HEADER_IDENTITY_TOKEN; import static com.apps.pochak.global.api_payload.code.status.SuccessStatus.SUCCESS_LOG_OUT; @@ -24,7 +20,7 @@ @RestController @RequiredArgsConstructor public class OAuthController { - private final JwtService jwtService; + private final JwtProvider jwtProvider; private final OAuthService oAuthService; private final AppleOAuthService appleOAuthService; private final GoogleOAuthService googleOAuthService; @@ -35,14 +31,15 @@ public ApiResponse signup(@ModelAttribute final MemberInfoR } @PostMapping("/api/v2/refresh") - public ApiResponse refresh() { + public ApiResponse refresh() { return ApiResponse.onSuccess(oAuthService.reissueAccessToken()); } @PostMapping("/apple/login") - public ApiResponse appleOAuthRequest(@RequestHeader(HEADER_IDENTITY_TOKEN) String idToken, - @RequestHeader(HEADER_APPLE_AUTHORIZATION_CODE) String authorizationCode) - throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException { + public ApiResponse appleOAuthRequest( + @RequestHeader(HEADER_IDENTITY_TOKEN) String idToken, + @RequestHeader(HEADER_APPLE_AUTHORIZATION_CODE) String authorizationCode + ) { return ApiResponse.onSuccess(appleOAuthService.login(idToken, authorizationCode)); } @@ -54,7 +51,7 @@ public ApiResponse googleOAuthRequest(@PathVariable String accessToken) { @GetMapping("/api/v2/logout") public ApiResponse logout() { String accessToken = JwtHeaderUtil.getAccessToken(); - String id = jwtService.getSubject(accessToken); + String id = jwtProvider.getSubject(accessToken); oAuthService.logout(id); return ApiResponse.of(SUCCESS_LOG_OUT); } @@ -62,7 +59,7 @@ public ApiResponse logout() { @DeleteMapping("/api/v2/signout") public ApiResponse signout() { String accessToken = JwtHeaderUtil.getAccessToken(); - String id = jwtService.getSubject(accessToken); + String id = jwtProvider.getSubject(accessToken); oAuthService.signout(id); return ApiResponse.of(SUCCESS_SIGN_OUT); } diff --git a/src/main/java/com/apps/pochak/login/dto/response/AppleTokenResponse.java b/src/main/java/com/apps/pochak/login/dto/apple/AppleTokenResponse.java similarity index 93% rename from src/main/java/com/apps/pochak/login/dto/response/AppleTokenResponse.java rename to src/main/java/com/apps/pochak/login/dto/apple/AppleTokenResponse.java index b29e7496..250e0942 100644 --- a/src/main/java/com/apps/pochak/login/dto/response/AppleTokenResponse.java +++ b/src/main/java/com/apps/pochak/login/dto/apple/AppleTokenResponse.java @@ -1,4 +1,4 @@ -package com.apps.pochak.login.dto.response; +package com.apps.pochak.login.dto.apple; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AccessLevel; diff --git a/src/main/java/com/apps/pochak/login/dto/apple/key/ApplePublicKey.java b/src/main/java/com/apps/pochak/login/dto/apple/key/ApplePublicKey.java new file mode 100644 index 00000000..8d931f84 --- /dev/null +++ b/src/main/java/com/apps/pochak/login/dto/apple/key/ApplePublicKey.java @@ -0,0 +1,25 @@ +package com.apps.pochak.login.dto.apple.key; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class ApplePublicKey { + @JsonProperty + private String kty; + + @JsonProperty + private String kid; + + @JsonProperty + private String use; + + @JsonProperty + private String alg; + + @JsonProperty + private String n; + + @JsonProperty + private String e; +} diff --git a/src/main/java/com/apps/pochak/login/dto/apple/key/ApplePublicKeyResponse.java b/src/main/java/com/apps/pochak/login/dto/apple/key/ApplePublicKeyResponse.java new file mode 100644 index 00000000..8a78b128 --- /dev/null +++ b/src/main/java/com/apps/pochak/login/dto/apple/key/ApplePublicKeyResponse.java @@ -0,0 +1,24 @@ +package com.apps.pochak.login.dto.apple.key; + +import com.apps.pochak.global.api_payload.exception.handler.AuthenticationException; +import lombok.AccessLevel; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +import static com.apps.pochak.global.api_payload.code.status.ErrorStatus.FAIL_VALIDATE_PUBLIC_KEY; + +@Data +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ApplePublicKeyResponse { + + private List keys; + + public ApplePublicKey getMatchedKeyBy(final String kid, final String alg) { + return this.keys.stream() + .filter(key -> key.getKid().equals(kid) && key.getAlg().equals(alg)) + .findAny() + .orElseThrow(() -> new AuthenticationException(FAIL_VALIDATE_PUBLIC_KEY)); + } +} diff --git a/src/main/java/com/apps/pochak/login/dto/response/GoogleMemberResponse.java b/src/main/java/com/apps/pochak/login/dto/google/GoogleMemberResponse.java similarity index 90% rename from src/main/java/com/apps/pochak/login/dto/response/GoogleMemberResponse.java rename to src/main/java/com/apps/pochak/login/dto/google/GoogleMemberResponse.java index 44bf0e2f..e3f862b8 100644 --- a/src/main/java/com/apps/pochak/login/dto/response/GoogleMemberResponse.java +++ b/src/main/java/com/apps/pochak/login/dto/google/GoogleMemberResponse.java @@ -1,4 +1,4 @@ -package com.apps.pochak.login.dto.response; +package com.apps.pochak.login.dto.google; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/apps/pochak/login/dto/response/GoogleTokenResponse.java b/src/main/java/com/apps/pochak/login/dto/google/GoogleTokenResponse.java similarity index 93% rename from src/main/java/com/apps/pochak/login/dto/response/GoogleTokenResponse.java rename to src/main/java/com/apps/pochak/login/dto/google/GoogleTokenResponse.java index 64071314..db8fe9c5 100644 --- a/src/main/java/com/apps/pochak/login/dto/response/GoogleTokenResponse.java +++ b/src/main/java/com/apps/pochak/login/dto/google/GoogleTokenResponse.java @@ -1,4 +1,4 @@ -package com.apps.pochak.login.dto.response; +package com.apps.pochak.login.dto.google; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/apps/pochak/login/dto/request/UserInfoRequest.java b/src/main/java/com/apps/pochak/login/dto/request/UserInfoRequest.java deleted file mode 100644 index b8f329a9..00000000 --- a/src/main/java/com/apps/pochak/login/dto/request/UserInfoRequest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.apps.pochak.login.dto.request; - -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.web.multipart.MultipartFile; - -@Data -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class UserInfoRequest { - - private String name; - private String email; - private String handle; - private String message; - private String socialId; - private String socialType; - private String socialRefreshToken; - private MultipartFile profileImage; - - @Builder - public UserInfoRequest(String name, String email, String handle, String message, String socialId, String socialType, String socialRefreshToken, MultipartFile profileImage) { - this.name = name; - this.email = email; - this.handle = handle; - this.message = message; - this.socialId = socialId; - this.socialType = socialType; - this.profileImage = profileImage; - this.socialRefreshToken = socialRefreshToken; - } -} diff --git a/src/main/java/com/apps/pochak/login/dto/response/PostTokenResponse.java b/src/main/java/com/apps/pochak/login/dto/response/AccessTokenResponse.java similarity index 79% rename from src/main/java/com/apps/pochak/login/dto/response/PostTokenResponse.java rename to src/main/java/com/apps/pochak/login/dto/response/AccessTokenResponse.java index 971afa91..bf05a526 100644 --- a/src/main/java/com/apps/pochak/login/dto/response/PostTokenResponse.java +++ b/src/main/java/com/apps/pochak/login/dto/response/AccessTokenResponse.java @@ -5,6 +5,6 @@ @Data @Builder -public class PostTokenResponse { +public class AccessTokenResponse { private String accessToken; } diff --git a/src/main/java/com/apps/pochak/login/dto/response/ApplePublicKeyResponse.java b/src/main/java/com/apps/pochak/login/dto/response/ApplePublicKeyResponse.java deleted file mode 100644 index 2f1e22af..00000000 --- a/src/main/java/com/apps/pochak/login/dto/response/ApplePublicKeyResponse.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.apps.pochak.login.dto.response; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AccessLevel; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; -import java.util.Optional; - -@Data -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ApplePublicKeyResponse { - - private List keys; - - @Getter - public static class Key { - @JsonProperty - private String kty; - @JsonProperty - private String kid; - @JsonProperty - private String use; - @JsonProperty - private String alg; - @JsonProperty - private String n; - @JsonProperty - private String e; - } - - public Optional getMatchedKeyBy(String kid, String alg) { - return this.keys.stream() - .filter(key -> key.getKid().equals(kid) && key.getAlg().equals(alg)) - .findFirst(); - } -} diff --git a/src/main/java/com/apps/pochak/login/dto/response/GoogleUserResponse.java b/src/main/java/com/apps/pochak/login/dto/response/GoogleUserResponse.java deleted file mode 100644 index cee8d99d..00000000 --- a/src/main/java/com/apps/pochak/login/dto/response/GoogleUserResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.apps.pochak.login.dto.response; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Data; - -@Data -@JsonIgnoreProperties(ignoreUnknown = true) -public class GoogleUserResponse { - // TODO: User -> Member 이름 변경 - public String id; - public String name; - public String email; -} diff --git a/src/main/java/com/apps/pochak/login/dto/response/OAuthResponse.java b/src/main/java/com/apps/pochak/login/dto/response/OAuthResponse.java deleted file mode 100644 index 6d5b3c91..00000000 --- a/src/main/java/com/apps/pochak/login/dto/response/OAuthResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.apps.pochak.login.dto.response; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class OAuthResponse { - private String id; - private String name; - private String email; - private String socialType; - private String accessToken; - private String refreshToken; - private Boolean isNewMember; -} diff --git a/src/main/java/com/apps/pochak/login/jwt/JwtAuthorizationFilter.java b/src/main/java/com/apps/pochak/login/filter/JwtAuthorizationFilter.java similarity index 90% rename from src/main/java/com/apps/pochak/login/jwt/JwtAuthorizationFilter.java rename to src/main/java/com/apps/pochak/login/filter/JwtAuthorizationFilter.java index 4b6486d4..e05ff5c7 100644 --- a/src/main/java/com/apps/pochak/login/jwt/JwtAuthorizationFilter.java +++ b/src/main/java/com/apps/pochak/login/filter/JwtAuthorizationFilter.java @@ -1,7 +1,8 @@ -package com.apps.pochak.login.jwt; +package com.apps.pochak.login.filter; import com.apps.pochak.global.api_payload.code.ErrorReasonDTO; import com.apps.pochak.global.api_payload.exception.GeneralException; +import com.apps.pochak.login.provider.JwtProvider; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; @@ -30,7 +31,7 @@ @RequiredArgsConstructor public class JwtAuthorizationFilter extends OncePerRequestFilter { - private final JwtService jwtService; + private final JwtProvider jwtProvider; @Override protected void doFilterInternal( @@ -48,7 +49,7 @@ protected void doFilterInternal( try { if (headerValue != null && headerValue.startsWith(TOKEN_PREFIX)) { String accessToken = headerValue.substring(TOKEN_PREFIX.length()); - if (jwtService.validateAccessToken(accessToken)) { + if (jwtProvider.validateAccessToken(accessToken)) { Authentication authentication = getAuthentication(accessToken); SecurityContextHolder.getContext().setAuthentication(authentication); } @@ -61,7 +62,7 @@ protected void doFilterInternal( } private Authentication getAuthentication(final String accessToken) { - Claims claims = jwtService.getTokenClaims(accessToken); + Claims claims = jwtProvider.getTokenClaims(accessToken); Collection authorities = Arrays.stream(new String[]{claims.get(AUTHORITIES_KEY).toString()}) .map(SimpleGrantedAuthority::new) @@ -82,6 +83,7 @@ private void exceptionHandler( ObjectMapper mapper = new ObjectMapper(); String result = mapper.writeValueAsString(errorReasonDTO); + response.setStatus(errorReasonDTO.getHttpStatus().value()); response.getWriter().write(result); } } diff --git a/src/main/java/com/apps/pochak/login/jwt/JwtService.java b/src/main/java/com/apps/pochak/login/provider/JwtProvider.java similarity index 94% rename from src/main/java/com/apps/pochak/login/jwt/JwtService.java rename to src/main/java/com/apps/pochak/login/provider/JwtProvider.java index 5e023a96..85b3d8fc 100644 --- a/src/main/java/com/apps/pochak/login/jwt/JwtService.java +++ b/src/main/java/com/apps/pochak/login/provider/JwtProvider.java @@ -1,9 +1,8 @@ -package com.apps.pochak.login.jwt; +package com.apps.pochak.login.provider; import com.apps.pochak.global.api_payload.exception.handler.ExpiredPeriodJwtException; import com.apps.pochak.global.api_payload.exception.handler.InvalidJwtException; -import com.apps.pochak.global.api_payload.exception.handler.RefreshTokenException; -import com.apps.pochak.login.dto.response.PostTokenResponse; +import com.apps.pochak.login.util.JwtHeaderUtil; import com.apps.pochak.member.domain.Member; import com.apps.pochak.member.domain.repository.MemberRepository; import io.jsonwebtoken.*; @@ -12,7 +11,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import java.security.Key; import java.util.Base64; @@ -22,9 +21,9 @@ import static com.apps.pochak.global.api_payload.code.status.ErrorStatus.*; @Getter -@Service +@Component @RequiredArgsConstructor -public class JwtService { +public class JwtProvider { public static final String EMPTY_SUBJECT = ""; private final MemberRepository memberRepository; private final long accessTokenExpirationTime = 1000L * 60 * 60; // 1H diff --git a/src/main/java/com/apps/pochak/login/oauth/AppleOAuthService.java b/src/main/java/com/apps/pochak/login/service/AppleOAuthService.java similarity index 77% rename from src/main/java/com/apps/pochak/login/oauth/AppleOAuthService.java rename to src/main/java/com/apps/pochak/login/service/AppleOAuthService.java index 78d50c1d..1b0cff3d 100644 --- a/src/main/java/com/apps/pochak/login/oauth/AppleOAuthService.java +++ b/src/main/java/com/apps/pochak/login/service/AppleOAuthService.java @@ -1,17 +1,16 @@ -package com.apps.pochak.login.oauth; +package com.apps.pochak.login.service; import com.apps.pochak.global.api_payload.exception.handler.AppleOAuthException; -import com.apps.pochak.login.dto.response.ApplePublicKeyResponse; -import com.apps.pochak.login.dto.response.AppleTokenResponse; +import com.apps.pochak.login.dto.apple.AppleTokenResponse; +import com.apps.pochak.login.dto.apple.key.ApplePublicKeyResponse; import com.apps.pochak.login.dto.response.OAuthMemberResponse; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; +import com.apps.pochak.login.util.ApplePublicKeyGenerator; +import com.apps.pochak.login.util.JwtValidator; import com.apps.pochak.member.domain.Member; import com.apps.pochak.member.domain.SocialType; import com.apps.pochak.member.domain.repository.MemberRepository; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.*; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; @@ -23,24 +22,19 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.io.IOException; import java.io.Reader; import java.io.StringReader; -import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Paths; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.RSAPublicKeySpec; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.Base64; import java.util.Date; import java.util.Map; @@ -50,9 +44,9 @@ @Service @RequiredArgsConstructor public class AppleOAuthService { - - private final ObjectMapper objectMapper; - private final JwtService jwtService; + private final JwtProvider jwtProvider; + private final ApplePublicKeyGenerator applePublicKeyGenerator; + private final JwtValidator jwtValidator; private final MemberRepository memberRepository; @Value("${oauth2.apple.key-id}") @@ -67,9 +61,9 @@ public class AppleOAuthService { private String KEY_ID_PATH; @Transactional - public OAuthMemberResponse login(String idToken, String authorizationCode) throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException { - Map idTokenHeaderMap = getHeaderFromIdToken(idToken); - Claims claims = verifyIdToken(idTokenHeaderMap.get("kid"), idTokenHeaderMap.get("alg"), idToken); + public OAuthMemberResponse login(String idToken, String authorizationCode) { + Map tokenHeaders = jwtValidator.parseHeaders(idToken); + Claims claims = verifyIdToken(tokenHeaders, idToken); String sub = String.valueOf(claims.get("sub")); String email = String.valueOf(claims.get("email")); @@ -83,15 +77,14 @@ public OAuthMemberResponse login(String idToken, String authorizationCode) throw .email(email) .socialType(SocialType.APPLE.name()) .refreshToken(appleRefreshToken) - .isNewMember(false) + .isNewMember(true) .build(); } - String appRefreshToken = jwtService.createRefreshToken(); - String appAccessToken = jwtService.createAccessToken(member.getId().toString()); + String appRefreshToken = jwtProvider.createRefreshToken(); + String appAccessToken = jwtProvider.createAccessToken(member.getId().toString()); member.updateRefreshToken(appRefreshToken); - memberRepository.save(member); return OAuthMemberResponse.builder() .socialId(sub) @@ -103,19 +96,10 @@ public OAuthMemberResponse login(String idToken, String authorizationCode) throw .build(); } - /** - * Get alg, kid From JWT Header - */ - private Map getHeaderFromIdToken(String idToken) throws JsonProcessingException { - String idTokenHeader = idToken.substring(0, idToken.indexOf(".")); - String decodeIdTokenHeader = new String(Base64.getDecoder().decode((idTokenHeader))); - return objectMapper.readValue(decodeIdTokenHeader, Map.class); - } - /** * Get Public Key */ - private Claims verifyIdToken(String kid, String alg, String idToken) throws NoSuchAlgorithmException, InvalidKeySpecException { + private Claims verifyIdToken(Map tokenHeaders, String idToken) { try { WebClient webClient = WebClient .builder() @@ -135,22 +119,9 @@ private Claims verifyIdToken(String kid, String alg, String idToken) throws NoSu .toStream() .findFirst() .orElseThrow(() -> new AppleOAuthException(INVALID_PUBLIC_KEY)); - ApplePublicKeyResponse.Key key = publicKeyResponse.getMatchedKeyBy(kid, alg) - .orElseThrow(() -> new NullPointerException("Failed get public key from apple's id server.")); - - byte[] n = Base64.getDecoder().decode(key.getN()); - byte[] e = Base64.getDecoder().decode(key.getE()); - - RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(new BigInteger(1, n), new BigInteger(1, e)); - KeyFactory keyFactory = KeyFactory.getInstance(key.getKty()); - PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); - - return Jwts.parserBuilder() - .setSigningKey(publicKey) - .build() - .parseClaimsJws(idToken) - .getBody(); + PublicKey publicKey = applePublicKeyGenerator.generatePublicKey(tokenHeaders, publicKeyResponse); + return jwtValidator.getTokenClaims(idToken, publicKey); } catch (MalformedJwtException e) { throw new AppleOAuthException(MALFORMED_TOKEN); } catch (ExpiredJwtException e) { @@ -162,30 +133,6 @@ private Claims verifyIdToken(String kid, String alg, String idToken) throws NoSu } } - private PrivateKey getPrivateKey() throws IOException { - ClassPathResource resource = new ClassPathResource(KEY_ID_PATH); - String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI()))); - Reader pemReader = new StringReader(privateKey); - PEMParser pemParser = new PEMParser(pemReader); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); - PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject(); - return converter.getPrivateKey(object); - } - - private String makeClientSecret() throws IOException { - Date expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant()); - return Jwts.builder() - .setHeaderParam("kid", KEY_ID) - .setHeaderParam("alg", "ES256") - .setIssuer(TEAM_ID) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(expirationDate) - .setAudience("https://appleid.apple.com") - .setSubject(CLIENT_ID) - .signWith(getPrivateKey(), SignatureAlgorithm.ES256) - .compact(); - } - /** * Get Apple Refresh Token * For Delete Account @@ -225,6 +172,30 @@ private String getAppleRefreshToken(String authorizationCode) { return appleTokenResponse.getRefreshToken(); } + private String makeClientSecret() throws IOException { + Date expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant()); + return Jwts.builder() + .setHeaderParam("kid", KEY_ID) + .setHeaderParam("alg", "ES256") + .setIssuer(TEAM_ID) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(expirationDate) + .setAudience("https://appleid.apple.com") + .setSubject(CLIENT_ID) + .signWith(getPrivateKey(), SignatureAlgorithm.ES256) + .compact(); + } + + private PrivateKey getPrivateKey() throws IOException { + ClassPathResource resource = new ClassPathResource(KEY_ID_PATH); + String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI()))); + Reader pemReader = new StringReader(privateKey); + PEMParser pemParser = new PEMParser(pemReader); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject(); + return converter.getPrivateKey(object); + } + /** * Revoke Apple Login */ diff --git a/src/main/java/com/apps/pochak/login/oauth/GoogleOAuthService.java b/src/main/java/com/apps/pochak/login/service/GoogleOAuthService.java similarity index 87% rename from src/main/java/com/apps/pochak/login/oauth/GoogleOAuthService.java rename to src/main/java/com/apps/pochak/login/service/GoogleOAuthService.java index 82257832..ec84adda 100644 --- a/src/main/java/com/apps/pochak/login/oauth/GoogleOAuthService.java +++ b/src/main/java/com/apps/pochak/login/service/GoogleOAuthService.java @@ -1,10 +1,10 @@ -package com.apps.pochak.login.oauth; +package com.apps.pochak.login.service; import com.apps.pochak.global.api_payload.exception.handler.GoogleOAuthException; -import com.apps.pochak.login.dto.response.GoogleTokenResponse; -import com.apps.pochak.login.dto.response.GoogleMemberResponse; +import com.apps.pochak.login.dto.google.GoogleTokenResponse; +import com.apps.pochak.login.dto.google.GoogleMemberResponse; import com.apps.pochak.login.dto.response.OAuthMemberResponse; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.member.domain.repository.MemberRepository; import jakarta.transaction.Transactional; @@ -23,7 +23,7 @@ public class GoogleOAuthService { private final MemberRepository memberRepository; private final WebClient webClient; - private final JwtService jwtService; + private final JwtProvider jwtProvider; @Value("${oauth2.google.client-id}") private String GOOGLE_CLIENT_ID; @@ -37,7 +37,7 @@ public class GoogleOAuthService { private String GOOGLE_USER_BASE_URL; @Transactional - public OAuthMemberResponse login(String accessToken) { + public OAuthMemberResponse login(final String accessToken) { GoogleMemberResponse memberResponse = getUserInfo(accessToken); Member member = memberRepository.findMemberBySocialId(memberResponse.getId()).orElse(null); @@ -52,8 +52,8 @@ public OAuthMemberResponse login(String accessToken) { .build(); } - String appRefreshToken = jwtService.createRefreshToken(); - String appAccessToken = jwtService.createAccessToken(member.getHandle()); + String appRefreshToken = jwtProvider.createRefreshToken(); + String appAccessToken = jwtProvider.createAccessToken(member.getId().toString()); member.updateRefreshToken(appRefreshToken); memberRepository.save(member); @@ -68,10 +68,28 @@ public OAuthMemberResponse login(String accessToken) { .build(); } + /** + * Get User Info Using Access Token + */ + public GoogleMemberResponse getUserInfo(final String accessToken) { + GoogleMemberResponse googleMemberResponse = webClient.get() + .uri(GOOGLE_USER_BASE_URL, uriBuilder -> uriBuilder.queryParam("access_token", accessToken).build()) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new RuntimeException("Social Access Token is unauthorized"))) + .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new RuntimeException("Internal Server Error"))) + .bodyToMono(GoogleMemberResponse.class) + .flux() + .toStream() + .findFirst() + .orElseThrow(() -> new GoogleOAuthException(INVALID_USER_INFO)); + + return googleMemberResponse; + } + /** * Get Access Token */ - public String getAccessToken(String code) { + public String getAccessToken(final String code) { GoogleTokenResponse googleTokenResponse = webClient.post() .uri(GOOGLE_BASE_URL, uriBuilder -> uriBuilder .queryParam("grant_type", "authorization_code") @@ -91,22 +109,4 @@ public String getAccessToken(String code) { return googleTokenResponse.getAccessToken(); } - - /** - * Get User Info Using Access Token - */ - public GoogleMemberResponse getUserInfo(String accessToken) { - GoogleMemberResponse googleMemberResponse = webClient.get() - .uri(GOOGLE_USER_BASE_URL, uriBuilder -> uriBuilder.queryParam("access_token", accessToken).build()) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new RuntimeException("Social Access Token is unauthorized"))) - .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new RuntimeException("Internal Server Error"))) - .bodyToMono(GoogleMemberResponse.class) - .flux() - .toStream() - .findFirst() - .orElseThrow(() -> new GoogleOAuthException(INVALID_USER_INFO)); - - return googleMemberResponse; - } } diff --git a/src/main/java/com/apps/pochak/login/oauth/OAuthService.java b/src/main/java/com/apps/pochak/login/service/OAuthService.java similarity index 83% rename from src/main/java/com/apps/pochak/login/oauth/OAuthService.java rename to src/main/java/com/apps/pochak/login/service/OAuthService.java index bf3ff2ff..4dff67ec 100644 --- a/src/main/java/com/apps/pochak/login/oauth/OAuthService.java +++ b/src/main/java/com/apps/pochak/login/service/OAuthService.java @@ -1,4 +1,4 @@ -package com.apps.pochak.login.oauth; +package com.apps.pochak.login.service; import com.apps.pochak.comment.domain.repository.CommentRepository; import com.apps.pochak.follow.domain.repository.FollowRepository; @@ -8,9 +8,9 @@ import com.apps.pochak.like.domain.repository.LikeRepository; import com.apps.pochak.login.dto.request.MemberInfoRequest; import com.apps.pochak.login.dto.response.OAuthMemberResponse; -import com.apps.pochak.login.dto.response.PostTokenResponse; -import com.apps.pochak.login.jwt.JwtHeaderUtil; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.dto.response.AccessTokenResponse; +import com.apps.pochak.login.util.JwtHeaderUtil; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.member.domain.SocialType; import com.apps.pochak.member.domain.repository.MemberRepository; @@ -30,7 +30,7 @@ @RequiredArgsConstructor @Transactional public class OAuthService { - private final JwtService jwtService; + private final JwtProvider jwtProvider; private final CommentRepository commentRepository; private final FollowRepository followRepository; private final LikeRepository likeRepository; @@ -47,7 +47,7 @@ public OAuthMemberResponse signup(MemberInfoRequest memberInfoRequest) { String profileImageUrl = awsS3Service.upload(memberInfoRequest.getProfileImage(), MEMBER); - String refreshToken = jwtService.createRefreshToken(); + String refreshToken = jwtProvider.createRefreshToken(); Member member = Member.signupMember() .name(memberInfoRequest.getName()) @@ -62,25 +62,25 @@ public OAuthMemberResponse signup(MemberInfoRequest memberInfoRequest) { .build(); memberRepository.save(member); - String accessToken = jwtService.createAccessToken(member.getId().toString()); + String accessToken = jwtProvider.createAccessToken(member.getId().toString()); return new OAuthMemberResponse(member, false, accessToken); } @Transactional(readOnly = true) - public PostTokenResponse reissueAccessToken() { + public AccessTokenResponse reissueAccessToken() { String accessToken = JwtHeaderUtil.getAccessToken(); String refreshToken = JwtHeaderUtil.getRefreshToken(); - if (jwtService.isValidRefreshAndInvalidAccess(refreshToken, accessToken)) { + if (jwtProvider.isValidRefreshAndInvalidAccess(refreshToken, accessToken)) { Member member = memberRepository.findMemberByRefreshToken(refreshToken) .orElseThrow(() -> new InvalidJwtException(INVALID_REFRESH_TOKEN)); - return PostTokenResponse.builder() - .accessToken(jwtService.createAccessToken(member.getId().toString())) + return AccessTokenResponse.builder() + .accessToken(jwtProvider.createAccessToken(member.getId().toString())) .build(); } - if (jwtService.isValidRefreshAndValidAccess(refreshToken, accessToken)) { - return PostTokenResponse.builder() - .accessToken(jwtService.createAccessToken(accessToken)) + if (jwtProvider.isValidRefreshAndValidAccess(refreshToken, accessToken)) { + return AccessTokenResponse.builder() + .accessToken(accessToken) .build(); } throw new InvalidJwtException(FAIL_VALIDATE_TOKEN); diff --git a/src/main/java/com/apps/pochak/login/util/ApplePublicKeyGenerator.java b/src/main/java/com/apps/pochak/login/util/ApplePublicKeyGenerator.java new file mode 100644 index 00000000..c49f40b3 --- /dev/null +++ b/src/main/java/com/apps/pochak/login/util/ApplePublicKeyGenerator.java @@ -0,0 +1,54 @@ +package com.apps.pochak.login.util; + +import com.apps.pochak.global.api_payload.exception.handler.AppleOAuthException; +import com.apps.pochak.login.dto.apple.key.ApplePublicKey; +import com.apps.pochak.login.dto.apple.key.ApplePublicKeyResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.Map; + +import static com.apps.pochak.global.api_payload.code.status.ErrorStatus.INVALID_KEY_SPEC; +import static com.apps.pochak.global.api_payload.code.status.ErrorStatus.NO_SUCH_ALGORITHM; + +@Component +@RequiredArgsConstructor +public class ApplePublicKeyGenerator { + public PublicKey generatePublicKey( + final Map tokenHeaders, + final ApplePublicKeyResponse response + ) { + ApplePublicKey publicKey = response.getMatchedKeyBy( + tokenHeaders.get("kid"), + tokenHeaders.get("alg") + ); + + return getPublicKey(publicKey); + } + + private PublicKey getPublicKey(final ApplePublicKey publicKey) { + byte[] nBytes = Base64.getUrlDecoder().decode(publicKey.getN()); + byte[] eBytes = Base64.getUrlDecoder().decode(publicKey.getE()); + + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec( + new BigInteger(1, nBytes), + new BigInteger(1, eBytes) + ); + + try { + KeyFactory keyFactory = KeyFactory.getInstance(publicKey.getKty()); + return keyFactory.generatePublic(publicKeySpec); + } catch (NoSuchAlgorithmException e) { + throw new AppleOAuthException(NO_SUCH_ALGORITHM); + } catch (InvalidKeySpecException e) { + throw new AppleOAuthException(INVALID_KEY_SPEC); + } + } +} diff --git a/src/main/java/com/apps/pochak/login/jwt/JwtHeaderUtil.java b/src/main/java/com/apps/pochak/login/util/JwtHeaderUtil.java similarity index 97% rename from src/main/java/com/apps/pochak/login/jwt/JwtHeaderUtil.java rename to src/main/java/com/apps/pochak/login/util/JwtHeaderUtil.java index 45386a64..8d6c3361 100644 --- a/src/main/java/com/apps/pochak/login/jwt/JwtHeaderUtil.java +++ b/src/main/java/com/apps/pochak/login/util/JwtHeaderUtil.java @@ -1,4 +1,4 @@ -package com.apps.pochak.login.jwt; +package com.apps.pochak.login.util; import com.apps.pochak.global.api_payload.exception.GeneralException; import com.apps.pochak.global.api_payload.exception.handler.InvalidJwtException; diff --git a/src/main/java/com/apps/pochak/login/util/JwtValidator.java b/src/main/java/com/apps/pochak/login/util/JwtValidator.java new file mode 100644 index 00000000..d31012f6 --- /dev/null +++ b/src/main/java/com/apps/pochak/login/util/JwtValidator.java @@ -0,0 +1,42 @@ +package com.apps.pochak.login.util; + +import com.apps.pochak.global.api_payload.exception.handler.AppleOAuthException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.util.Base64; +import java.util.Map; + +import static com.apps.pochak.global.api_payload.code.status.ErrorStatus.JSON_PROCESSING_EXCEPTION; + +@Component +@RequiredArgsConstructor +public class JwtValidator { + + public Map parseHeaders(String token) { + try { + String header = token.split("\\.")[0]; + return new ObjectMapper().readValue(decodeHeader(header), Map.class); + } catch (JsonProcessingException e) { + throw new AppleOAuthException(JSON_PROCESSING_EXCEPTION); + } + } + + public String decodeHeader(String token) { + return new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8); + } + + public Claims getTokenClaims(String token, PublicKey publicKey) { + return Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/src/main/java/com/apps/pochak/member/service/MemberService.java b/src/main/java/com/apps/pochak/member/service/MemberService.java index 6935e74a..391964ab 100644 --- a/src/main/java/com/apps/pochak/member/service/MemberService.java +++ b/src/main/java/com/apps/pochak/member/service/MemberService.java @@ -3,7 +3,7 @@ import com.apps.pochak.follow.domain.repository.FollowRepository; import com.apps.pochak.global.api_payload.exception.GeneralException; import com.apps.pochak.global.s3.S3Service; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.dto.request.ProfileUpdateRequest; import com.apps.pochak.member.dto.response.ProfileUpdateResponse; import com.apps.pochak.member.domain.Member; @@ -28,14 +28,14 @@ public class MemberService { private final MemberRepository memberRepository; private final FollowRepository followRepository; private final PostRepository postRepository; - private final JwtService jwtService; + private final JwtProvider jwtProvider; private final S3Service awsS3Service; @Transactional(readOnly = true) public ProfileResponse getProfileDetail(final String handle, final Pageable pageable ) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Member member = memberRepository.findByHandle(handle, loginMember); final long followerCount = followRepository.countActiveFollowByReceiver(member); final long followingCount = followRepository.countActiveFollowBySender(member); @@ -54,7 +54,7 @@ public ProfileResponse getProfileDetail(final String handle, public ProfileUpdateResponse updateProfileDetail(final String handle, final ProfileUpdateRequest profileUpdateRequest){ - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Member updateMember = memberRepository.findByHandleWithoutLogin(handle); if (!loginMember.equals(updateMember)) { throw new GeneralException(UNAUTHORIZED_MEMBER_REQUEST); @@ -79,7 +79,7 @@ public PostElements getTaggedPosts( final String handle, final Pageable pageable ) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Member member = memberRepository.findByHandle(handle, loginMember); final Page taggedPost = postRepository.findTaggedPost(member, loginMember, pageable); return PostElements.from(taggedPost); @@ -90,7 +90,7 @@ public PostElements getUploadPosts( final String handle, final Pageable pageable ) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Member owner = memberRepository.findByHandle(handle, loginMember); final Page taggedPost = postRepository.findUploadPost(owner, loginMember, pageable); return PostElements.from(taggedPost); @@ -101,7 +101,7 @@ public MemberElements search( final String keyword, final Pageable pageable ) { - Member loginMember = jwtService.getLoginMember(); + Member loginMember = jwtProvider.getLoginMember(); Page memberPage = memberRepository.searchByKeyword(keyword, loginMember, pageable); return MemberElements.from(memberPage); } diff --git a/src/main/java/com/apps/pochak/post/service/PostService.java b/src/main/java/com/apps/pochak/post/service/PostService.java index e619d1dd..b2412478 100644 --- a/src/main/java/com/apps/pochak/post/service/PostService.java +++ b/src/main/java/com/apps/pochak/post/service/PostService.java @@ -9,7 +9,7 @@ import com.apps.pochak.global.api_payload.exception.GeneralException; import com.apps.pochak.global.s3.S3Service; import com.apps.pochak.like.domain.repository.LikeRepository; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.member.domain.repository.MemberRepository; import com.apps.pochak.post.domain.Post; @@ -45,18 +45,18 @@ public class PostService { private final AlarmRepository alarmRepository; private final S3Service s3Service; - private final JwtService jwtService; + private final JwtProvider jwtProvider; @Transactional(readOnly = true) public PostElements getHomeTab(Pageable pageable) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Page taggedPost = postRepository.findTaggedPostsOfFollowing(loginMember, pageable); return PostElements.from(taggedPost); } @Transactional(readOnly = true) public PostDetailResponse getPostDetail(final Long postId) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Post post = postRepository.findPostById(postId, loginMember); final List tagList = tagRepository.findTagsByPost(post); if (post.isPrivate() && !isAccessAuthorized(post, tagList, loginMember)) { @@ -89,7 +89,7 @@ private boolean isAccessAuthorized(final Post post, } public void savePost(final PostUploadRequest request) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final String image = s3Service.upload(request.getPostImage(), POST); final Post post = request.toEntity(image, loginMember); postRepository.save(post); @@ -119,7 +119,7 @@ private void saveTagApprovalAlarms(List tagList) { } public void deletePost(final Long postId) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Post post = postRepository.findById(postId).orElseThrow(() -> new GeneralException(INVALID_POST_ID)); if (!post.getOwner().getId().equals(loginMember.getId())) { throw new GeneralException(NOT_YOUR_POST); @@ -136,7 +136,7 @@ public PostElements getSearchTab(Pageable pageable) { @Transactional(readOnly = true) public PostPreviewResponse getPreviewPost(final Long alarmId) { - Member loginMember = jwtService.getLoginMember(); + Member loginMember = jwtProvider.getLoginMember(); Alarm alarm = alarmRepository.findAlarmByIdAndReceiver(alarmId, loginMember) .orElseThrow(() -> new GeneralException(INVALID_ALARM_ID)); diff --git a/src/main/java/com/apps/pochak/report/service/ReportService.java b/src/main/java/com/apps/pochak/report/service/ReportService.java index b1928d72..9bb3c442 100644 --- a/src/main/java/com/apps/pochak/report/service/ReportService.java +++ b/src/main/java/com/apps/pochak/report/service/ReportService.java @@ -1,6 +1,6 @@ package com.apps.pochak.report.service; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.post.domain.Post; import com.apps.pochak.post.domain.repository.PostRepository; @@ -17,10 +17,10 @@ public class ReportService { private final ReportRepository reportRepository; private final PostRepository postRepository; - private final JwtService jwtService; + private final JwtProvider jwtProvider; public void saveReport(final ReportUploadRequest request) { - final Member reporter = jwtService.getLoginMember(); + final Member reporter = jwtProvider.getLoginMember(); final Post reportedPost = postRepository.findPostById(request.getPostId(), reporter); Report report = request.toEntity(reporter, reportedPost); diff --git a/src/main/java/com/apps/pochak/tag/service/TagService.java b/src/main/java/com/apps/pochak/tag/service/TagService.java index e8e21ce4..1e422e1c 100644 --- a/src/main/java/com/apps/pochak/tag/service/TagService.java +++ b/src/main/java/com/apps/pochak/tag/service/TagService.java @@ -2,7 +2,7 @@ import com.apps.pochak.alarm.domain.repository.AlarmRepository; import com.apps.pochak.global.api_payload.code.BaseCode; -import com.apps.pochak.login.jwt.JwtService; +import com.apps.pochak.login.provider.JwtProvider; import com.apps.pochak.member.domain.Member; import com.apps.pochak.post.domain.Post; import com.apps.pochak.post.domain.repository.PostRepository; @@ -25,10 +25,10 @@ public class TagService { private final AlarmRepository alarmRepository; private final PostRepository postRepository; - private final JwtService jwtService; + private final JwtProvider jwtProvider; public BaseCode approveOrRejectTagRequest(final Long tagId, final Boolean isAccept) { - final Member loginMember = jwtService.getLoginMember(); + final Member loginMember = jwtProvider.getLoginMember(); final Tag tag = tagRepository.findTagByIdAndMember(tagId, loginMember); if (isAccept) { return acceptPost(tag);