From 916c272cf68589f13d1b4ab4f673d62417a2dd00 Mon Sep 17 00:00:00 2001 From: dev-Crayon Date: Tue, 19 Dec 2023 11:43:45 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존의 AuthService 명칭 SocialService로 변경 토큰 재발급 API 구현 Related to: #13 --- .../auth/application/AuthService.java | 46 ++++++++++++++++--- .../auth/application/AuthServiceProvider.java | 27 ----------- ...thService.java => KakaoSocialService.java} | 16 ++----- .../auth/application/SocialService.java | 10 ++++ .../application/SocialServiceProvider.java | 27 +++++++++++ .../SobokSobok/auth/ui/AuthController.java | 36 +++++++++++---- .../auth/ui/dto/JwtTokenResponse.java | 11 +++++ .../auth/ui/dto/SocialLoginRequest.java | 6 +-- .../auth/ui/dto/SocialSignupRequest.java | 8 ++-- .../sobok/SobokSobok/exception/ErrorCode.java | 1 + .../SobokSobok/exception/SuccessCode.java | 3 +- .../SobokSobok/security/jwt/JwtProvider.java | 9 +++- 12 files changed, 134 insertions(+), 66 deletions(-) delete mode 100644 src/main/java/io/sobok/SobokSobok/auth/application/AuthServiceProvider.java rename src/main/java/io/sobok/SobokSobok/auth/application/{KakaoAuthService.java => KakaoSocialService.java} (81%) create mode 100644 src/main/java/io/sobok/SobokSobok/auth/application/SocialService.java create mode 100644 src/main/java/io/sobok/SobokSobok/auth/application/SocialServiceProvider.java create mode 100644 src/main/java/io/sobok/SobokSobok/auth/ui/dto/JwtTokenResponse.java diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/AuthService.java b/src/main/java/io/sobok/SobokSobok/auth/application/AuthService.java index 8d4497e..43fc3c6 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/application/AuthService.java +++ b/src/main/java/io/sobok/SobokSobok/auth/application/AuthService.java @@ -1,13 +1,45 @@ package io.sobok.SobokSobok.auth.application; -import io.sobok.SobokSobok.auth.ui.dto.SocialLoginRequest; -import io.sobok.SobokSobok.auth.ui.dto.SocialLoginResponse; -import io.sobok.SobokSobok.auth.ui.dto.SocialSignupRequest; -import io.sobok.SobokSobok.auth.ui.dto.SocialSignupResponse; +import io.sobok.SobokSobok.auth.infrastructure.UserRepository; +import io.sobok.SobokSobok.auth.ui.dto.JwtTokenResponse; +import io.sobok.SobokSobok.exception.ErrorCode; +import io.sobok.SobokSobok.exception.model.NotFoundException; +import io.sobok.SobokSobok.security.jwt.Jwt; +import io.sobok.SobokSobok.security.jwt.JwtProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -public abstract class AuthService { +@Service +@RequiredArgsConstructor +public class AuthService { - public abstract SocialSignupResponse signup(SocialSignupRequest request); + private final UserRepository userRepository; - public abstract SocialLoginResponse login(SocialLoginRequest request); + private final RedisTemplate redisTemplate; + private final JwtProvider jwtProvider; + + @Transactional + public JwtTokenResponse refresh(String refresh) { + + ValueOperations valueOperations = redisTemplate.opsForValue(); + String socialId = valueOperations.get(refresh); + + if (socialId == null) { + throw new NotFoundException(ErrorCode.UNREGISTERED_TOKEN); + } + + if(!userRepository.existsBySocialInfoSocialId(socialId)) { + throw new NotFoundException(ErrorCode.UNREGISTERED_USER); + } + + Jwt jwt = jwtProvider.getUserJwt(socialId); + + return JwtTokenResponse.builder() + .accessToken(jwt.accessToken()) + .refreshToken(jwt.refreshToken()) + .build(); + } } diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/AuthServiceProvider.java b/src/main/java/io/sobok/SobokSobok/auth/application/AuthServiceProvider.java deleted file mode 100644 index d79d452..0000000 --- a/src/main/java/io/sobok/SobokSobok/auth/application/AuthServiceProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.sobok.SobokSobok.auth.application; - -import io.sobok.SobokSobok.auth.domain.SocialType; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.HashMap; -import java.util.Map; - -@RequiredArgsConstructor -@Component -public class AuthServiceProvider { - - private static final Map authServiceMap = new HashMap<>(); - - private final KakaoAuthService kakaoAuthService; - - @PostConstruct - void initializeAuthServiceMap() { - authServiceMap.put(SocialType.KAKAO, kakaoAuthService); - } - - public AuthService getAuthService(SocialType socialType) { - return authServiceMap.get(socialType); - } -} diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/KakaoAuthService.java b/src/main/java/io/sobok/SobokSobok/auth/application/KakaoSocialService.java similarity index 81% rename from src/main/java/io/sobok/SobokSobok/auth/application/KakaoAuthService.java rename to src/main/java/io/sobok/SobokSobok/auth/application/KakaoSocialService.java index 5bbfb94..4018755 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/application/KakaoAuthService.java +++ b/src/main/java/io/sobok/SobokSobok/auth/application/KakaoSocialService.java @@ -17,24 +17,19 @@ import io.sobok.SobokSobok.security.jwt.Jwt; import io.sobok.SobokSobok.security.jwt.JwtProvider; import lombok.RequiredArgsConstructor; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @RequiredArgsConstructor -public class KakaoAuthService extends AuthService { +public class KakaoSocialService extends SocialService { private final KakaoService kakaoService; private final UserRepository userRepository; private final JwtProvider jwtProvider; - private final AuthenticationManagerBuilder authenticationManagerBuilder; @Override @Transactional @@ -60,7 +55,7 @@ public SocialSignupResponse signup(SocialSignupRequest request) { .roles(Role.USER.name()) .build()); - Jwt jwt = getUserJwt(signupUser.getSocialInfo().getSocialId()); + Jwt jwt = jwtProvider.getUserJwt(signupUser.getSocialInfo().getSocialId()); return SocialSignupResponse.builder() .id(signupUser.getId()) @@ -83,16 +78,11 @@ public SocialLoginResponse login(SocialLoginRequest request) { loginUser.updateDeviceToken(request.deviceToken()); } - Jwt jwt = getUserJwt(loginUser.getSocialInfo().getSocialId()); + Jwt jwt = jwtProvider.getUserJwt(loginUser.getSocialInfo().getSocialId()); return SocialLoginResponse.builder() .accessToken(jwt.accessToken()) .refreshToken(jwt.refreshToken()) .build(); } - - private Jwt getUserJwt(String principle) { - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principle, null, List.of(new SimpleGrantedAuthority(Role.USER.name()))); - return jwtProvider.createToken(authenticationToken); - } } diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/SocialService.java b/src/main/java/io/sobok/SobokSobok/auth/application/SocialService.java new file mode 100644 index 0000000..718393e --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/auth/application/SocialService.java @@ -0,0 +1,10 @@ +package io.sobok.SobokSobok.auth.application; + +import io.sobok.SobokSobok.auth.ui.dto.*; + +public abstract class SocialService { + + public abstract SocialSignupResponse signup(SocialSignupRequest request); + + public abstract SocialLoginResponse login(SocialLoginRequest request); +} diff --git a/src/main/java/io/sobok/SobokSobok/auth/application/SocialServiceProvider.java b/src/main/java/io/sobok/SobokSobok/auth/application/SocialServiceProvider.java new file mode 100644 index 0000000..81b5211 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/auth/application/SocialServiceProvider.java @@ -0,0 +1,27 @@ +package io.sobok.SobokSobok.auth.application; + +import io.sobok.SobokSobok.auth.domain.SocialType; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class SocialServiceProvider { + + private static final Map socialServiceMap = new HashMap<>(); + + private final KakaoSocialService kakaoSocialService; + + @PostConstruct + void initializeSocialServiceMap() { + socialServiceMap.put(SocialType.KAKAO, kakaoSocialService); + } + + public SocialService getSocialService(SocialType socialType) { + return socialServiceMap.get(socialType); + } +} diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/AuthController.java b/src/main/java/io/sobok/SobokSobok/auth/ui/AuthController.java index f8e1960..760e009 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/ui/AuthController.java +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/AuthController.java @@ -1,12 +1,10 @@ package io.sobok.SobokSobok.auth.ui; import io.sobok.SobokSobok.auth.application.AuthService; -import io.sobok.SobokSobok.auth.application.AuthServiceProvider; +import io.sobok.SobokSobok.auth.application.SocialService; +import io.sobok.SobokSobok.auth.application.SocialServiceProvider; import io.sobok.SobokSobok.auth.domain.SocialType; -import io.sobok.SobokSobok.auth.ui.dto.SocialLoginRequest; -import io.sobok.SobokSobok.auth.ui.dto.SocialLoginResponse; -import io.sobok.SobokSobok.auth.ui.dto.SocialSignupRequest; -import io.sobok.SobokSobok.auth.ui.dto.SocialSignupResponse; +import io.sobok.SobokSobok.auth.ui.dto.*; import io.sobok.SobokSobok.common.dto.ApiResponse; import io.sobok.SobokSobok.exception.SuccessCode; import io.swagger.v3.oas.annotations.Operation; @@ -23,7 +21,8 @@ @Tag(name = "Auth", description = "인증 관련 컨트롤러") public class AuthController { - private final AuthServiceProvider authServiceProvider; + private final AuthService authService; + private final SocialServiceProvider socialServiceProvider; @PostMapping("/signup") @Operation( @@ -32,12 +31,12 @@ public class AuthController { ) public ResponseEntity> signup(@RequestBody @Valid final SocialSignupRequest request) { - AuthService authService = authServiceProvider.getAuthService(request.socialType()); + SocialService socialService = socialServiceProvider.getSocialService(request.socialType()); return ResponseEntity .status(HttpStatus.CREATED) .body(ApiResponse.success( SuccessCode.SOCIAL_SIGNUP_SUCCESS, - authService.signup(request) + socialService.signup(request) )); } @@ -52,16 +51,33 @@ public ResponseEntity> login( @RequestParam final String deviceToken ) { - AuthService authService = authServiceProvider.getAuthService(socialType); + SocialService socialService = socialServiceProvider.getSocialService(socialType); return ResponseEntity .status(HttpStatus.OK) .body(ApiResponse.success( SuccessCode.SOCIAL_LOGIN_SUCCESS, - authService.login(SocialLoginRequest.builder() + socialService.login(SocialLoginRequest.builder() .code(code) .socialType(socialType) .deviceToken(deviceToken) .build()) )); } + + @GetMapping("/refresh") + @Operation( + summary = "JWT 토큰 재발급", + description = "Refresh token을 통해 JWT 토큰을 재발급받는 API 입니다." + ) + public ResponseEntity> refresh( + @RequestHeader("Refresh-Token") final String refreshToken + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.TOKEN_REFRESH_SUCCESS, + authService.refresh(refreshToken) + )); + } } diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/JwtTokenResponse.java b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/JwtTokenResponse.java new file mode 100644 index 0000000..b149368 --- /dev/null +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/JwtTokenResponse.java @@ -0,0 +1,11 @@ +package io.sobok.SobokSobok.auth.ui.dto; + +import lombok.Builder; + +@Builder +public record JwtTokenResponse( + + String accessToken, + + String refreshToken +) { } diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialLoginRequest.java b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialLoginRequest.java index d4f369d..f7860d7 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialLoginRequest.java +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialLoginRequest.java @@ -8,14 +8,14 @@ @Builder public record SocialLoginRequest( - @Schema(required = true) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String code, - @Schema(required = true) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotNull SocialType socialType, - @Schema(required = true) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String deviceToken ) { } diff --git a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupRequest.java b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupRequest.java index a4e58c6..e1f3ebb 100644 --- a/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupRequest.java +++ b/src/main/java/io/sobok/SobokSobok/auth/ui/dto/SocialSignupRequest.java @@ -7,19 +7,19 @@ public record SocialSignupRequest( - @Schema(required = true) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String code, - @Schema(required = true) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotNull SocialType socialType, - @Schema(required = true) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String username, - @Schema(required = true) + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank String deviceToken ) { diff --git a/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java b/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java index dacb02b..3642633 100644 --- a/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java +++ b/src/main/java/io/sobok/SobokSobok/exception/ErrorCode.java @@ -13,6 +13,7 @@ public enum ErrorCode { // auth UNREGISTERED_USER(HttpStatus.NOT_FOUND, "등록되지 않은 사용자입니다."), + UNREGISTERED_TOKEN(HttpStatus.NOT_FOUND, "등록되지 않은 토큰입니다."), ALREADY_EXISTS_USER(HttpStatus.CONFLICT, "이미 회원가입이 완료된 사용자입니다."), ALREADY_USING_USERNAME(HttpStatus.CONFLICT, "이미 사용중인 username입니다."), diff --git a/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java b/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java index ded55af..1b33f4e 100644 --- a/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java +++ b/src/main/java/io/sobok/SobokSobok/exception/SuccessCode.java @@ -10,7 +10,8 @@ public enum SuccessCode { // auth SOCIAL_SIGNUP_SUCCESS(HttpStatus.CREATED, "소셜 회원가입에 성공했습니다."), - SOCIAL_LOGIN_SUCCESS(HttpStatus.OK, "소셜 로그인에 성공했습니다.") + SOCIAL_LOGIN_SUCCESS(HttpStatus.OK, "소셜 로그인에 성공했습니다."), + TOKEN_REFRESH_SUCCESS(HttpStatus.OK, "토큰 재발급에 성공했습니다,"), ; private final HttpStatus code; diff --git a/src/main/java/io/sobok/SobokSobok/security/jwt/JwtProvider.java b/src/main/java/io/sobok/SobokSobok/security/jwt/JwtProvider.java index 424acf5..39d7094 100644 --- a/src/main/java/io/sobok/SobokSobok/security/jwt/JwtProvider.java +++ b/src/main/java/io/sobok/SobokSobok/security/jwt/JwtProvider.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import io.sobok.SobokSobok.auth.domain.Role; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; @@ -19,6 +20,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -57,6 +59,11 @@ public void afterPropertiesSet() { this.key = Keys.hmacShaKeyFor(keyBytes); } + public Jwt getUserJwt(String principle) { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principle, null, List.of(new SimpleGrantedAuthority(Role.USER.name()))); + return createToken(authenticationToken); + } + public Jwt createToken(Authentication authentication) { String authorities = authentication.getAuthorities().stream() @@ -67,7 +74,7 @@ public Jwt createToken(Authentication authentication) { String refreshToken = generateToken(authentication.getName(), "", REFRESH_TOKEN_TYPE, refreshTokenExpiration); ValueOperations valueOperations = redisTemplate.opsForValue(); - valueOperations.set(authentication.getName(), refreshToken, refreshTokenExpiration, TimeUnit.SECONDS); + valueOperations.set(refreshToken, authentication.getName(), refreshTokenExpiration, TimeUnit.SECONDS); return Jwt.builder() .accessToken(accessToken)