From 008d7210f6946dc688eaabaca47a2042962515cc Mon Sep 17 00:00:00 2001 From: SIWON990327 Date: Fri, 6 Dec 2024 15:45:31 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../com/groom/orbit/auth/app/AuthService.java | 48 +++++++++++++++++-- .../orbit/auth/controller/AuthController.java | 7 +++ .../groom/orbit/config/redis/RedisConfig.java | 32 +++++++++++++ .../groom/orbit/config/redis/RedisUtil.java | 12 +++++ .../config/security/JwtTokenProvider.java | 28 ++++++++++- .../config/security/kakao/KakaoApiClient.java | 15 ++++++ .../security/kakao/KakaoReissueParams.java | 20 ++++++++ .../config/security/oAuth/OAuthApiClient.java | 4 ++ .../oAuth/RequestOAuthInfoService.java | 7 +++ src/main/resources/application.yml | 8 +++- 11 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/groom/orbit/config/redis/RedisConfig.java create mode 100644 src/main/java/com/groom/orbit/config/redis/RedisUtil.java create mode 100644 src/main/java/com/groom/orbit/config/security/kakao/KakaoReissueParams.java diff --git a/build.gradle b/build.gradle index 3c4e7fc..8d24b54 100644 --- a/build.gradle +++ b/build.gradle @@ -120,6 +120,9 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.16.1' implementation 'com.fasterxml.jackson.core:jackson-core:2.16.1' + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/main/java/com/groom/orbit/auth/app/AuthService.java b/src/main/java/com/groom/orbit/auth/app/AuthService.java index f50796e..4c5d516 100644 --- a/src/main/java/com/groom/orbit/auth/app/AuthService.java +++ b/src/main/java/com/groom/orbit/auth/app/AuthService.java @@ -1,5 +1,9 @@ package com.groom.orbit.auth.app; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -8,10 +12,11 @@ import com.groom.orbit.auth.app.dto.LoginResponseDto; import com.groom.orbit.auth.dao.AuthMemberRepository; import com.groom.orbit.auth.dao.entity.AuthMember; -import com.groom.orbit.config.security.oAuth.AuthTokenGenerator; -import com.groom.orbit.config.security.oAuth.OAuthInfoResponse; -import com.groom.orbit.config.security.oAuth.OAuthLoginParams; -import com.groom.orbit.config.security.oAuth.RequestOAuthInfoService; +import com.groom.orbit.common.exception.CommonException; +import com.groom.orbit.common.exception.ErrorCode; +import com.groom.orbit.config.security.JwtTokenProvider; +import com.groom.orbit.config.security.kakao.KakaoReissueParams; +import com.groom.orbit.config.security.oAuth.*; import lombok.RequiredArgsConstructor; @@ -24,6 +29,11 @@ public class AuthService { private final AuthTokenGenerator authTokensGenerator; private final RequestOAuthInfoService requestOAuthInfoService; private final VectorService vectorService; + private final JwtTokenProvider jwtTokenProvider; + private final RedisTemplate redisTemplate; + + @Value("${jwt.refresh-token-validity}") + private Long refreshTokenValidityMilliseconds; public LoginResponseDto login(OAuthLoginParams params) { OAuthInfoResponse oAuthInfoResponse = requestOAuthInfoService.request(params); @@ -55,4 +65,34 @@ private void saveVector(AuthMember member) { CreateVectorDto vectorDto = CreateVectorDto.builder().memberId(member.getId()).build(); vectorService.save(vectorDto); } + + public AuthToken reissue(KakaoReissueParams params) { + + String refreshToken = params.getRefreshToken(); + + Long memberId = jwtTokenProvider.parseRefreshToken(refreshToken); + + if (!refreshToken.equals(redisTemplate.opsForValue().get(memberId.toString()))) { + throw new CommonException(ErrorCode.INVALID_TOKEN_ERROR); + } + + AuthMember member = + memberRepository + .findById(memberId) + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_MEMBER)); + + String newAccessToken = jwtTokenProvider.generateAccessToken(member.getId()); + + String newRefreshToken = jwtTokenProvider.generateRefreshToken(member.getId()); + + redisTemplate + .opsForValue() + .set( + member.getId().toString(), + newRefreshToken, + refreshTokenValidityMilliseconds, + TimeUnit.MILLISECONDS); + + return AuthToken.of(newAccessToken, newRefreshToken); + } } diff --git a/src/main/java/com/groom/orbit/auth/controller/AuthController.java b/src/main/java/com/groom/orbit/auth/controller/AuthController.java index 4781474..67a4c3d 100644 --- a/src/main/java/com/groom/orbit/auth/controller/AuthController.java +++ b/src/main/java/com/groom/orbit/auth/controller/AuthController.java @@ -9,6 +9,8 @@ import com.groom.orbit.auth.app.dto.LoginResponseDto; import com.groom.orbit.common.exception.BaseResponse; import com.groom.orbit.config.security.kakao.KakaoLoginParams; +import com.groom.orbit.config.security.kakao.KakaoReissueParams; +import com.groom.orbit.config.security.oAuth.AuthToken; import lombok.RequiredArgsConstructor; @@ -23,4 +25,9 @@ public class AuthController { public BaseResponse loginKakao(@RequestBody KakaoLoginParams params) { return BaseResponse.onSuccess(authService.login(params)); } + + @PostMapping("/reissue") + public BaseResponse reissue(@RequestBody KakaoReissueParams params) { + return BaseResponse.onSuccess(authService.reissue(params)); + } } diff --git a/src/main/java/com/groom/orbit/config/redis/RedisConfig.java b/src/main/java/com/groom/orbit/config/redis/RedisConfig.java new file mode 100644 index 0000000..59a251a --- /dev/null +++ b/src/main/java/com/groom/orbit/config/redis/RedisConfig.java @@ -0,0 +1,32 @@ +package com.groom.orbit.config.redis; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Value("${spring.data.redis.port}") + private int port; + + @Value("${spring.data.redis.host}") + private String host; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } +} diff --git a/src/main/java/com/groom/orbit/config/redis/RedisUtil.java b/src/main/java/com/groom/orbit/config/redis/RedisUtil.java new file mode 100644 index 0000000..f728a22 --- /dev/null +++ b/src/main/java/com/groom/orbit/config/redis/RedisUtil.java @@ -0,0 +1,12 @@ +package com.groom.orbit.config.redis; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Repository +public class RedisUtil { + private final StringRedisTemplate stringRedisTemplate; +} diff --git a/src/main/java/com/groom/orbit/config/security/JwtTokenProvider.java b/src/main/java/com/groom/orbit/config/security/JwtTokenProvider.java index 60ad408..e4398ae 100644 --- a/src/main/java/com/groom/orbit/config/security/JwtTokenProvider.java +++ b/src/main/java/com/groom/orbit/config/security/JwtTokenProvider.java @@ -3,10 +3,15 @@ import java.security.Key; import java.time.ZonedDateTime; import java.util.Date; +import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; +import com.groom.orbit.common.exception.CommonException; +import com.groom.orbit.common.exception.ErrorCode; + import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; @@ -23,15 +28,18 @@ public class JwtTokenProvider { private final Key key; private final long accessTokenValidityMilliseconds; private final long refreshTokenValidityMilliseconds; + private final RedisTemplate redisTemplate; public JwtTokenProvider( @Value("${jwt.secret}") String secretKey, @Value("${jwt.access-token-validity}") final long accessTokenValidityMilliseconds, - @Value("${jwt.refresh-token-validity}") final long refreshTokenValidityMilliseconds) { + @Value("${jwt.refresh-token-validity}") final long refreshTokenValidityMilliseconds, + RedisTemplate redisTemplate) { byte[] keyBytes = Decoders.BASE64.decode(secretKey); this.key = Keys.hmacShaKeyFor(keyBytes); this.accessTokenValidityMilliseconds = accessTokenValidityMilliseconds; this.refreshTokenValidityMilliseconds = refreshTokenValidityMilliseconds; + this.redisTemplate = redisTemplate; } public String generate(Long userId, long validityMilliseconds) { @@ -55,7 +63,15 @@ public String generateAccessToken(Long userId) { } public String generateRefreshToken(Long userId) { - return generate(userId, refreshTokenValidityMilliseconds); + String refreshToken = generate(userId, refreshTokenValidityMilliseconds); + redisTemplate + .opsForValue() + .set( + userId.toString(), + refreshToken, + refreshTokenValidityMilliseconds, + TimeUnit.MILLISECONDS); + return refreshToken; } public String extractSubject(String accessToken) { @@ -93,4 +109,12 @@ public boolean isTokenValid(String token) { public Long getSubject(String token) { return Long.valueOf(getClaims(token).getBody().getSubject()); } + + public Long parseRefreshToken(String token) { + if (isTokenValid(token)) { + Claims claims = getClaims(token).getBody(); + return Long.parseLong(claims.getSubject()); + } + throw new CommonException(ErrorCode.NOT_FOUND_MEMBER); + } } diff --git a/src/main/java/com/groom/orbit/config/security/kakao/KakaoApiClient.java b/src/main/java/com/groom/orbit/config/security/kakao/KakaoApiClient.java index 55bc929..a21ee76 100644 --- a/src/main/java/com/groom/orbit/config/security/kakao/KakaoApiClient.java +++ b/src/main/java/com/groom/orbit/config/security/kakao/KakaoApiClient.java @@ -81,4 +81,19 @@ public OAuthInfoResponse requestOauthInfo(String accessToken) { return restTemplate.postForObject(url, request, KakaoInfoResponse.class); } + + @Override + public String reissueAccessToken(KakaoReissueParams params) { + String url = authUrl + "/oauth/token"; + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap body = params.makeBody(); + body.add("grant_type", GRANT_TYPE); + body.add("client_id", clientId); + body.add("client_secret", clientSecret); + HttpEntity request = new HttpEntity<>(body, httpHeaders); + KakaoToken response = restTemplate.postForObject(url, request, KakaoToken.class); + assert response != null; + return response.getAccessToken(); + } } diff --git a/src/main/java/com/groom/orbit/config/security/kakao/KakaoReissueParams.java b/src/main/java/com/groom/orbit/config/security/kakao/KakaoReissueParams.java new file mode 100644 index 0000000..117c5fc --- /dev/null +++ b/src/main/java/com/groom/orbit/config/security/kakao/KakaoReissueParams.java @@ -0,0 +1,20 @@ +package com.groom.orbit.config.security.kakao; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class KakaoReissueParams { + + private String refreshToken; + + public MultiValueMap makeBody() { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("refresh_token", refreshToken); + return body; + } +} diff --git a/src/main/java/com/groom/orbit/config/security/oAuth/OAuthApiClient.java b/src/main/java/com/groom/orbit/config/security/oAuth/OAuthApiClient.java index 7b786b0..df0f2be 100644 --- a/src/main/java/com/groom/orbit/config/security/oAuth/OAuthApiClient.java +++ b/src/main/java/com/groom/orbit/config/security/oAuth/OAuthApiClient.java @@ -1,8 +1,12 @@ package com.groom.orbit.config.security.oAuth; +import com.groom.orbit.config.security.kakao.KakaoReissueParams; + public interface OAuthApiClient { String requestAccessToken(OAuthLoginParams params); OAuthInfoResponse requestOauthInfo(String accessToken); + + String reissueAccessToken(KakaoReissueParams params); } diff --git a/src/main/java/com/groom/orbit/config/security/oAuth/RequestOAuthInfoService.java b/src/main/java/com/groom/orbit/config/security/oAuth/RequestOAuthInfoService.java index af67c96..02aa23a 100644 --- a/src/main/java/com/groom/orbit/config/security/oAuth/RequestOAuthInfoService.java +++ b/src/main/java/com/groom/orbit/config/security/oAuth/RequestOAuthInfoService.java @@ -2,6 +2,8 @@ import org.springframework.stereotype.Component; +import com.groom.orbit.config.security.kakao.KakaoReissueParams; + @Component public class RequestOAuthInfoService { @@ -15,4 +17,9 @@ public OAuthInfoResponse request(OAuthLoginParams params) { String accessToken = client.requestAccessToken(params); return client.requestOauthInfo(accessToken); } + + public OAuthInfoResponse reissue(KakaoReissueParams params) { + String accessToken = client.reissueAccessToken(params); + return client.requestOauthInfo(accessToken); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c141c79..8666381 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -54,4 +54,10 @@ cloud: auto: false --- fcm: - fcm-url: ${FCM_URL} \ No newline at end of file + fcm-url: ${FCM_URL} +--- +spring: + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} \ No newline at end of file