From 6c4195a96feece7e7fe557a0eeaf54e684a4fa75 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 18:10:14 +0900 Subject: [PATCH 01/24] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit security, jwt, feign, redis related to: #11 --- build.gradle | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/build.gradle b/build.gradle index 1fb633e..999582c 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,22 @@ dependencies { // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // security + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // feign + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation platform("org.springframework.cloud:spring-cloud-dependencies:2023.0.2") + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { From 4b2f2c77b269e0b600b10c06f421d6dfa1a9278c Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 18:12:37 +0900 Subject: [PATCH 02/24] =?UTF-8?q?fix:=20BaseEntity=20nullable=20false=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nullable false로 하면 createdAt과 updatedAt이 자동 삽입 안 되는 문제 발생 --- src/main/java/com/_119/wepro/global/BaseEntity.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/_119/wepro/global/BaseEntity.java b/src/main/java/com/_119/wepro/global/BaseEntity.java index c382578..b3696ac 100644 --- a/src/main/java/com/_119/wepro/global/BaseEntity.java +++ b/src/main/java/com/_119/wepro/global/BaseEntity.java @@ -3,12 +3,11 @@ import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; - -import java.time.LocalDateTime; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Getter @@ -18,10 +17,9 @@ public abstract class BaseEntity { @CreatedDate - @Column(nullable = false, updatable = false) + @Column(updatable = false) private LocalDateTime createdAt; @LastModifiedDate - @Column(nullable = false) private LocalDateTime updatedAt; } From 51f1553a960dc3efc093b45cad270992950cb7a7 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 18:14:21 +0900 Subject: [PATCH 03/24] =?UTF-8?q?feat:=20Role,=20Provider,=20Status=20enum?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../com/_119/wepro/global/enums/Provider.java | 17 +++++++++++++++++ .../java/com/_119/wepro/global/enums/Role.java | 5 +++++ .../com/_119/wepro/global/enums/Status.java | 5 +++++ 3 files changed, 27 insertions(+) create mode 100644 src/main/java/com/_119/wepro/global/enums/Provider.java create mode 100644 src/main/java/com/_119/wepro/global/enums/Role.java create mode 100644 src/main/java/com/_119/wepro/global/enums/Status.java diff --git a/src/main/java/com/_119/wepro/global/enums/Provider.java b/src/main/java/com/_119/wepro/global/enums/Provider.java new file mode 100644 index 0000000..c7b508b --- /dev/null +++ b/src/main/java/com/_119/wepro/global/enums/Provider.java @@ -0,0 +1,17 @@ +package com._119.wepro.global.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Provider { + KAKAO("https://kauth.kakao.com/.well-known/jwks.json"), + //TODO apple jwk set uri 세팅하기 + APPLE("https://kauth.kakao.com/.well-known/jwks.json"), + ; + + private final String jwkSetUrl; + +// public static IdentityProvider of(){} +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/global/enums/Role.java b/src/main/java/com/_119/wepro/global/enums/Role.java new file mode 100644 index 0000000..954a8b6 --- /dev/null +++ b/src/main/java/com/_119/wepro/global/enums/Role.java @@ -0,0 +1,5 @@ +package com._119.wepro.global.enums; + +public enum Role { + ADMIN, USER, GUEST +} diff --git a/src/main/java/com/_119/wepro/global/enums/Status.java b/src/main/java/com/_119/wepro/global/enums/Status.java new file mode 100644 index 0000000..645cbbc --- /dev/null +++ b/src/main/java/com/_119/wepro/global/enums/Status.java @@ -0,0 +1,5 @@ +package com._119.wepro.global.enums; + +public enum Status { + ACTIVE, INACTIVE +} From ed65275b5ee902e8cd054a53da295bb00b48b5d3 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 18:14:52 +0900 Subject: [PATCH 04/24] =?UTF-8?q?feat:=20Member=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../com/_119/wepro/member/domain/Member.java | 46 +++++++++++++++++++ .../domain/repository/MemberRepository.java | 4 +- .../member/presentation/MemberController.java | 12 +++++ .../wepro/member/service/MemberService.java | 10 ++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/_119/wepro/member/presentation/MemberController.java create mode 100644 src/main/java/com/_119/wepro/member/service/MemberService.java diff --git a/src/main/java/com/_119/wepro/member/domain/Member.java b/src/main/java/com/_119/wepro/member/domain/Member.java index c020c99..ea3bbac 100644 --- a/src/main/java/com/_119/wepro/member/domain/Member.java +++ b/src/main/java/com/_119/wepro/member/domain/Member.java @@ -1,15 +1,25 @@ package com._119.wepro.member.domain; +import com._119.wepro.auth.dto.response.OIDCDecodePayload; import com._119.wepro.global.BaseEntity; +import com._119.wepro.global.enums.Provider; +import com._119.wepro.global.enums.Role; +import com._119.wepro.global.enums.Status; +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; @Entity @Getter @@ -22,5 +32,41 @@ public class Member extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String profile; + private String name; + + @Enumerated(value = EnumType.STRING) + @Column(length = 10, nullable = false) + private Provider provider; + + @Column(length = 20, nullable = false) + private String providerId; + + @Enumerated(value = EnumType.STRING) + @Column(length = 10, nullable = false) + private Status status; + + @Enumerated(value = EnumType.STRING) + @Column(length = 10, nullable = false) + private Role role; + + private String position; + + private String tag; + + private LocalDateTime inactivatedAt; + + public static Member of(SignInRequest request, OidcUser oidcDecodePayload) { + return Member.builder() + .profile(oidcDecodePayload.getPicture()) + .name(oidcDecodePayload.getNickName()) + .provider(request.getProvider()) + .role(Role.GUEST) + .providerId(oidcDecodePayload.getName()) + .status(Status.ACTIVE) + //Todo 태그 생성하기 +// .tag() + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java b/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java index 412cbac..a3b4973 100644 --- a/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java +++ b/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java @@ -1,10 +1,12 @@ package com._119.wepro.member.domain.repository; +import com._119.wepro.global.enums.Provider; import com._119.wepro.member.domain.Member; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface MemberRepository extends JpaRepository { - + Optional findByProviderAndProviderId(Provider provider, String providerId); } diff --git a/src/main/java/com/_119/wepro/member/presentation/MemberController.java b/src/main/java/com/_119/wepro/member/presentation/MemberController.java new file mode 100644 index 0000000..7958b5c --- /dev/null +++ b/src/main/java/com/_119/wepro/member/presentation/MemberController.java @@ -0,0 +1,12 @@ +package com._119.wepro.member.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/members") +public class MemberController { + +} diff --git a/src/main/java/com/_119/wepro/member/service/MemberService.java b/src/main/java/com/_119/wepro/member/service/MemberService.java new file mode 100644 index 0000000..ab8c31b --- /dev/null +++ b/src/main/java/com/_119/wepro/member/service/MemberService.java @@ -0,0 +1,10 @@ +package com._119.wepro.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberService { + +} From 911f3b0a47b33026e84b2a85ca212d231e6eebb8 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 18:16:14 +0900 Subject: [PATCH 05/24] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20exception=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 토큰, provder 관련 error code 추가 feign exception handler 추가 related to: #11 --- .../exception/GlobalExceptionHandler.java | 19 +++++++++++++++++++ .../exception/errorcode/CommonErrorCode.java | 3 +++ .../exception/errorcode/UserErrorCode.java | 1 + 3 files changed, 23 insertions(+) diff --git a/src/main/java/com/_119/wepro/global/exception/GlobalExceptionHandler.java b/src/main/java/com/_119/wepro/global/exception/GlobalExceptionHandler.java index 6178f2f..344e555 100644 --- a/src/main/java/com/_119/wepro/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/_119/wepro/global/exception/GlobalExceptionHandler.java @@ -3,9 +3,14 @@ import com._119.wepro.global.dto.ErrorResponseDto; import com._119.wepro.global.exception.errorcode.CommonErrorCode; import com._119.wepro.global.exception.errorcode.ErrorCode; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import feign.FeignException; import java.util.Collections; +import java.util.Map; import java.util.Objects; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; @@ -20,10 +25,13 @@ import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_PARAMETER; +@RequiredArgsConstructor @RestControllerAdvice @Slf4j public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + private final ObjectMapper objectMapper; + @ExceptionHandler(RestApiException.class) public ResponseEntity handleCustomException(RestApiException e) { ErrorCode errorCode = e.getErrorCode(); @@ -44,6 +52,17 @@ public ResponseEntity handleAllException(Exception ex) { return handleExceptionInternal(errorCode); } + @ExceptionHandler(FeignException.class) + public ResponseEntity feignExceptionHandler(FeignException feignException) throws JsonProcessingException { + + String responseJson = feignException.contentUTF8(); + Map responseMap = objectMapper.readValue(responseJson, Map.class); + + return ResponseEntity + .status(feignException.status()) + .body(responseMap); + } + private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { return ResponseEntity.status(errorCode.getHttpStatus()).body(makeErrorResponseDto(errorCode)); } diff --git a/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java b/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java index 1e5c1cf..5030011 100644 --- a/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java +++ b/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java @@ -10,6 +10,9 @@ public enum CommonErrorCode implements ErrorCode { INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter included"), RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not exists"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid token"), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Expired token"), + NOT_EXIST_BEARER_SUFFIX(HttpStatus.UNAUTHORIZED, "Bearer prefix is missing."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/_119/wepro/global/exception/errorcode/UserErrorCode.java b/src/main/java/com/_119/wepro/global/exception/errorcode/UserErrorCode.java index 922ca47..8a6335d 100644 --- a/src/main/java/com/_119/wepro/global/exception/errorcode/UserErrorCode.java +++ b/src/main/java/com/_119/wepro/global/exception/errorcode/UserErrorCode.java @@ -10,6 +10,7 @@ public enum UserErrorCode implements ErrorCode { INACTIVE_USER(HttpStatus.FORBIDDEN, "User is inactive"), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "User not found"), + UNSUPPORTED_PROVIDER(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported provider"), ; private final HttpStatus httpStatus; From 16aa78cda9e2019b1beb29c52c4ab76df53acec3 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 18:19:58 +0900 Subject: [PATCH 06/24] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20config=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feign, redis, security 설정 related to: #11 --- .../global/config/FeignClientConfig.java | 10 ++++ .../_119/wepro/global/config/RedisConfig.java | 60 +++++++++++++++++++ .../wepro/global/config/SecurityConfig.java | 48 +++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 src/main/java/com/_119/wepro/global/config/FeignClientConfig.java create mode 100644 src/main/java/com/_119/wepro/global/config/RedisConfig.java create mode 100644 src/main/java/com/_119/wepro/global/config/SecurityConfig.java diff --git a/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java b/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java new file mode 100644 index 0000000..e546da3 --- /dev/null +++ b/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java @@ -0,0 +1,10 @@ +package com._119.wepro.global.config; + +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients(basePackages = "com._119.wepro.global.security.client") +public class FeignClientConfig { + +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/global/config/RedisConfig.java b/src/main/java/com/_119/wepro/global/config/RedisConfig.java new file mode 100644 index 0000000..e34aa5c --- /dev/null +++ b/src/main/java/com/_119/wepro/global/config/RedisConfig.java @@ -0,0 +1,60 @@ +package com._119.wepro.global.config; + +import java.time.Duration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +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.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public CacheManager oidcCacheManager(RedisConnectionFactory cf) { + RedisCacheConfiguration redisCacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofDays(7L)); + + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf) + .cacheDefaults(redisCacheConfiguration) + .build(); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} diff --git a/src/main/java/com/_119/wepro/global/config/SecurityConfig.java b/src/main/java/com/_119/wepro/global/config/SecurityConfig.java new file mode 100644 index 0000000..cf0533e --- /dev/null +++ b/src/main/java/com/_119/wepro/global/config/SecurityConfig.java @@ -0,0 +1,48 @@ +package com._119.wepro.global.config; + +import com._119.wepro.global.security.jwt.JwtTokenProvider; +import com._119.wepro.global.security.jwt.JwtTokenFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { // security를 적용하지 않을 리소스 + return web -> web.ignoring() + .requestMatchers("/css/**", "/images/**", "/js/**", "/lib/**") + .requestMatchers("/", "/swagger-ui-custom.html", "/api-docs/**", "/swagger-ui/**", "swagger-ui.html", "/v3/api-docs/**") + .requestMatchers("/error", "/favicon.ico") + .requestMatchers("/auth/**", "/login/oauth2/code/**", "/login"); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(request -> request + .requestMatchers("/", "/auth/**", "/login/oauth2/code/**", "/login").permitAll() + .requestMatchers(HttpMethod.OPTIONS).permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } +} From e90225577d3894b79d3a853ae1d73288d5a5c634 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 21:20:00 +0900 Subject: [PATCH 07/24] =?UTF-8?q?refactor:=20security=20->=20auth=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../java/com/_119/wepro/global/config/FeignClientConfig.java | 2 +- .../java/com/_119/wepro/global/config/SecurityConfig.java | 4 ++-- src/main/java/com/_119/wepro/member/domain/Member.java | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java b/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java index e546da3..bf04cb7 100644 --- a/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java +++ b/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java @@ -4,7 +4,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -@EnableFeignClients(basePackages = "com._119.wepro.global.security.client") +@EnableFeignClients(basePackages = "com._119.wepro.auth.client") public class FeignClientConfig { } \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/global/config/SecurityConfig.java b/src/main/java/com/_119/wepro/global/config/SecurityConfig.java index cf0533e..b01fa6b 100644 --- a/src/main/java/com/_119/wepro/global/config/SecurityConfig.java +++ b/src/main/java/com/_119/wepro/global/config/SecurityConfig.java @@ -1,7 +1,7 @@ package com._119.wepro.global.config; -import com._119.wepro.global.security.jwt.JwtTokenProvider; -import com._119.wepro.global.security.jwt.JwtTokenFilter; +import com._119.wepro.auth.jwt.JwtTokenProvider; +import com._119.wepro.auth.jwt.JwtTokenFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/_119/wepro/member/domain/Member.java b/src/main/java/com/_119/wepro/member/domain/Member.java index ea3bbac..d40bba5 100644 --- a/src/main/java/com/_119/wepro/member/domain/Member.java +++ b/src/main/java/com/_119/wepro/member/domain/Member.java @@ -1,6 +1,5 @@ package com._119.wepro.member.domain; -import com._119.wepro.auth.dto.response.OIDCDecodePayload; import com._119.wepro.global.BaseEntity; import com._119.wepro.global.enums.Provider; import com._119.wepro.global.enums.Role; From 856e7e7e472c6001a18d4717a27fa0e03fb602b7 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 21:21:38 +0900 Subject: [PATCH 08/24] =?UTF-8?q?feat:=20kakao=20feign=20client=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../wepro/auth/client/KakaoOauthClient.java | 30 +++++++++++++++++++ .../auth/dto/response/KakaoTokenResponse.java | 15 ++++++++++ .../dto/response/OIDCPublicKeyResponse.java | 23 ++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java create mode 100644 src/main/java/com/_119/wepro/auth/dto/response/KakaoTokenResponse.java create mode 100644 src/main/java/com/_119/wepro/auth/dto/response/OIDCPublicKeyResponse.java diff --git a/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java b/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java new file mode 100644 index 0000000..f673bc9 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java @@ -0,0 +1,30 @@ +package com._119.wepro.auth.client; + +import com._119.wepro.auth.dto.response.KakaoTokenResponse; +import com._119.wepro.auth.dto.response.OIDCPublicKeyResponse; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; + +@FeignClient( + name = "KakaoOauthClient", + url = "https://kauth.kakao.com" +) +public interface KakaoOauthClient { + + // 만약 클라이언트로부터 code 받을 경우, + @PostMapping( + "/oauth/token?grant_type=authorization_code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&code={CODE}&client_secret={CLIENT_SECRET}") + KakaoTokenResponse kakaoAuth( + @PathVariable("CLIENT_ID") String clientId, + @PathVariable("REDIRECT_URI") String redirectUri, + @PathVariable("CODE") String code, + @PathVariable("CLIENT_SECRET") String client_secret); + + // oidc 공개 키 받아 오기 + @Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager") // 공개키 자주 요청할 거 같으면, 캐싱하기 + @GetMapping("/.well-known/jwks.json") + OIDCPublicKeyResponse getOIDCPublicKey(); +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/auth/dto/response/KakaoTokenResponse.java b/src/main/java/com/_119/wepro/auth/dto/response/KakaoTokenResponse.java new file mode 100644 index 0000000..f339f5f --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/response/KakaoTokenResponse.java @@ -0,0 +1,15 @@ +package com._119.wepro.auth.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonNaming(SnakeCaseStrategy.class) +public class KakaoTokenResponse { + private String accessToken; + private String refreshToken; + private String idToken; +} diff --git a/src/main/java/com/_119/wepro/auth/dto/response/OIDCPublicKeyResponse.java b/src/main/java/com/_119/wepro/auth/dto/response/OIDCPublicKeyResponse.java new file mode 100644 index 0000000..5f27996 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/response/OIDCPublicKeyResponse.java @@ -0,0 +1,23 @@ +package com._119.wepro.auth.dto.response; + +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OIDCPublicKeyResponse { + + private List keys; + + @Getter + @NoArgsConstructor + public static class OIDCPublicKey { + + private String kid; + private String alg; + private String use; + private String n; + private String e; + } +} From 0b92373fc4bc3783153487e5f84b01bb07f91a23 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 21:22:52 +0900 Subject: [PATCH 09/24] =?UTF-8?q?feat:=20jwt=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../_119/wepro/auth/jwt/JwtTokenFilter.java | 62 ++++++++ .../_119/wepro/auth/jwt/JwtTokenProvider.java | 138 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java create mode 100644 src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java new file mode 100644 index 0000000..aa7a07d --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java @@ -0,0 +1,62 @@ +package com._119.wepro.auth.jwt; + +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.NOT_EXIST_BEARER_SUFFIX; + +import com._119.wepro.global.exception.RestApiException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +@RequiredArgsConstructor +public class JwtTokenFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private final String accessHeader = "Authorization"; + private final String grantType = "Bearer"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + Optional token = getTokensFromHeader(request, accessHeader); + + token.ifPresent(t -> { + String accessToken = getAccessToken(t); + + Authentication authentication = jwtTokenProvider.getAuthentication(accessToken); + + SecurityContextHolder.getContext().setAuthentication(authentication); + }); + + String clientIp = request.getHeader("X-Forwarded-For"); + if (clientIp == null) { + clientIp = request.getRemoteAddr(); + } + log.error("Invalid token for requestURI: {}, Access from IP: {}", request.getRequestURI(), clientIp); + + filterChain.doFilter(request, response); + } + + private Optional getTokensFromHeader(HttpServletRequest request, String header) { + return Optional.ofNullable(request.getHeader(header)); + } + + private String getAccessToken(String token) { + String suffix = grantType + " "; + + if (!token.startsWith(suffix)) { + throw new RestApiException(NOT_EXIST_BEARER_SUFFIX); + } + + return token.replace(suffix, ""); + } +} diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..6687d4d --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java @@ -0,0 +1,138 @@ +package com._119.wepro.auth.jwt; + +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.EXPIRED_TOKEN; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN; + +import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.global.enums.Role; +import com._119.wepro.global.exception.RestApiException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class JwtTokenProvider { + + private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24; // 1일 + private static final long REFRESH_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 7일 + + private SecretKey secretKey; + + public JwtTokenProvider(@Value("${jwt.secret}") String key) { + byte[] keyBytes = key.getBytes(); + this.secretKey = Keys.hmacShaKeyFor(keyBytes); + } + + public TokenInfo generateToken(Long memberId, Role memberRole) { + String accessToken = generateAccessToken(memberId, memberRole); + // TODO: refresh redis에 저장 + String refreshToken = generateRefreshToken(); + + return new TokenInfo("Bearer", accessToken, refreshToken); + } + + public boolean validateToken(String token) { + if (!StringUtils.hasText(token)) { + return false; + } + + Claims claims = parseClaims(token); + return claims.getExpiration().after(new Date()); + } + + + public Authentication getAuthentication(String accessToken) { + Claims claims = parseClaims(accessToken); + + if (claims.get("auth") == null) { + throw new RestApiException(INVALID_TOKEN); + } + List authority = getAuthorities(claims); + validateAuthorityValue(authority); + + UserDetails principal = new User(claims.getSubject(), "", authority); + return new UsernamePasswordAuthenticationToken(principal, "", authority); + } + + private void validateAuthorityValue(List authority) { + if (authority.size() != 1 + || !isValidAuthority(authority.get(0))) { + throw new RestApiException(INVALID_TOKEN); + } + } + + private boolean isValidAuthority(SimpleGrantedAuthority authority) { + for (Role role : Role.values()) { + if (role.name().equals(authority.getAuthority())) { + return true; + } + } + return false; + } + + private Claims parseClaims(String accessToken) { + try { + // TODO: 블랙 리스트 여부 추가 + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(accessToken) + .getBody(); + } catch (SecurityException | MalformedJwtException e) { + throw new RestApiException(INVALID_TOKEN); + } catch (ExpiredJwtException e) { + throw new RestApiException(EXPIRED_TOKEN); + } catch (UnsupportedJwtException e) { + throw new RestApiException(INVALID_TOKEN); + } catch (IllegalArgumentException e) { + throw new RestApiException(INVALID_TOKEN); + } catch (JwtException e) { + throw new RestApiException(INVALID_TOKEN); + } + } + + private String generateAccessToken(Long memberId, Role memberRole) { + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + ACCESS_TOKEN_DURATION); + + return Jwts.builder() + .setSubject(memberId.toString()) + .claim("auth", memberRole.name()) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + private String generateRefreshToken() { + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + REFRESH_TOKEN_DURATION); + + return Jwts.builder() + .setExpiration(expiredDate) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + private List getAuthorities(Claims claims) { + return Collections.singletonList(new SimpleGrantedAuthority( + claims.get("auth").toString())); + } +} From 1883a81a045533b2fdd04ae966fbd256f640f9f6 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 21:23:31 +0900 Subject: [PATCH 10/24] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20req?= =?UTF-8?q?uest,=20response=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../wepro/auth/dto/request/AuthRequest.java | 37 +++++++++++++++++++ .../wepro/auth/dto/response/AuthResponse.java | 18 +++++++++ .../wepro/auth/dto/response/TokenInfo.java | 12 ++++++ 3 files changed, 67 insertions(+) create mode 100644 src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java create mode 100644 src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java create mode 100644 src/main/java/com/_119/wepro/auth/dto/response/TokenInfo.java diff --git a/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java b/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java new file mode 100644 index 0000000..10fc7c0 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java @@ -0,0 +1,37 @@ +package com._119.wepro.auth.dto.request; + +import com._119.wepro.global.enums.Provider; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class AuthRequest { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SignInRequest { + + @NotNull + @Enumerated(EnumType.STRING) + private Provider provider; + + @NotNull + private String idToken; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SignUpRequest { + + @NotNull + private String position; + } +} diff --git a/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java b/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java new file mode 100644 index 0000000..c1741cb --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java @@ -0,0 +1,18 @@ +package com._119.wepro.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class AuthResponse { + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class SignInResponse { + + private boolean newMember; + private TokenInfo tokenInfo; + } + +} diff --git a/src/main/java/com/_119/wepro/auth/dto/response/TokenInfo.java b/src/main/java/com/_119/wepro/auth/dto/response/TokenInfo.java new file mode 100644 index 0000000..8912728 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/response/TokenInfo.java @@ -0,0 +1,12 @@ +package com._119.wepro.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TokenInfo { + private String type; + private String accessToken; + private String refreshToken; +} From 26faf60d21538b08b4a33fee2d898183cd663e3e Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 21:24:45 +0900 Subject: [PATCH 11/24] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20con?= =?UTF-8?q?troller,=20service=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../auth/presentation/AuthController.java | 58 ++++++++++++++ .../wepro/auth/service/SignInService.java | 75 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/main/java/com/_119/wepro/auth/presentation/AuthController.java create mode 100644 src/main/java/com/_119/wepro/auth/service/SignInService.java diff --git a/src/main/java/com/_119/wepro/auth/presentation/AuthController.java b/src/main/java/com/_119/wepro/auth/presentation/AuthController.java new file mode 100644 index 0000000..0e71987 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/presentation/AuthController.java @@ -0,0 +1,58 @@ +package com._119.wepro.auth.presentation; + +import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; +import com._119.wepro.auth.service.KakaoService; +import com._119.wepro.auth.service.SignInService; +import com._119.wepro.auth.dto.request.AuthRequest.*; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.view.RedirectView; + +@Slf4j +@RequiredArgsConstructor +@RestController +public class AuthController { + + private final KakaoService kakaoService; + private final SignInService signInService; + + @PostMapping("/login") + @Operation(summary = "Kakao idToken 받아 소셜 로그인") + public ResponseEntity signIn( + @RequestBody @Valid SignInRequest request) { + return ResponseEntity.ok(signInService.signIn(request)); + } + + @PostMapping("/signup") + @Operation(summary = "직군 정보 받아 최종 회원가입") + public ResponseEntity register( + @RequestBody @Valid SignUpRequest request) { + return ResponseEntity.ok().build(); + } + + @GetMapping("/auth/kakao") + @Operation(summary = "Kakao Web 소셜 로그인 용 api, 백엔드용") + public RedirectView kakaoLogin() { + return new RedirectView(kakaoService.generateKakaoRedirectUrl()); + } + + @GetMapping("/login/oauth2/code/kakao") + @Operation(summary = "카카오 code 받는 api, 백엔드용") + public SignInResponse handleKakaoCallback(@RequestParam String code) { + return kakaoService.handleKakaoCallback(code); + } + + @GetMapping("/test") + public void test(Authentication authentication) { + log.info(authentication.getName()); + } +} diff --git a/src/main/java/com/_119/wepro/auth/service/SignInService.java b/src/main/java/com/_119/wepro/auth/service/SignInService.java new file mode 100644 index 0000000..38c114d --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/service/SignInService.java @@ -0,0 +1,75 @@ +package com._119.wepro.auth.service; + +import static com._119.wepro.global.enums.Provider.APPLE; +import static com._119.wepro.global.enums.Provider.KAKAO; + +import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; +import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.global.enums.Provider; +import com._119.wepro.global.enums.Role; +import com._119.wepro.auth.jwt.JwtTokenProvider; +import com._119.wepro.member.domain.Member; +import com._119.wepro.member.domain.repository.MemberRepository; +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; +import jakarta.transaction.Transactional; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Service; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + + +@Slf4j +@Service +@RequiredArgsConstructor +public class SignInService { + + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + private final Map decoders = Map.of( + Provider.KAKAO, buildDecoder(KAKAO.getJwkSetUrl()), + Provider.APPLE, buildDecoder(APPLE.getJwkSetUrl())); + + private JwtDecoder buildDecoder(String jwkUrl) { + return NimbusJwtDecoder.withJwkSetUri(jwkUrl).build(); + } + + @Transactional + public SignInResponse signIn(SignInRequest request) { + OidcUser oidcDecodePayload = socialLogin(request); + + Member member = getOrSaveUser(request, oidcDecodePayload); + TokenInfo tokenInfo = jwtTokenProvider.generateToken(member.getId(), member.getRole()); + boolean isNewMember = Role.GUEST == member.getRole(); + + return new SignInResponse(isNewMember, tokenInfo); + } + + + private Member getOrSaveUser(SignInRequest request, OidcUser oidcDecodePayload) { + Optional member = memberRepository.findByProviderAndProviderId( + request.getProvider(), oidcDecodePayload.getName()); + + return member.orElseGet( + () -> memberRepository.save(Member.of(request, oidcDecodePayload))); + + } + + private OidcUser socialLogin(SignInRequest request) { + Provider provider = request.getProvider(); + + Jwt jwt = decoders.get(provider).decode(request.getIdToken()); + OidcIdToken oidcIdToken = new OidcIdToken( + jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); + + return new DefaultOidcUser(null, oidcIdToken); +// throw new RestApiException(UNSUPPORTED_PROVIDER); + } +} From 643d8be48cd7f0d56166b0578649a533e9fbadd1 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Tue, 3 Sep 2024 21:25:24 +0900 Subject: [PATCH 12/24] =?UTF-8?q?feat:=20kakaoService=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추후에 삭제 예정 related to: #11 --- .../_119/wepro/auth/service/KakaoService.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/main/java/com/_119/wepro/auth/service/KakaoService.java diff --git a/src/main/java/com/_119/wepro/auth/service/KakaoService.java b/src/main/java/com/_119/wepro/auth/service/KakaoService.java new file mode 100644 index 0000000..2b47d87 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/service/KakaoService.java @@ -0,0 +1,73 @@ +package com._119.wepro.auth.service; + +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; +import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; +import com._119.wepro.global.enums.Provider; +import com._119.wepro.auth.client.KakaoOauthClient; +import com._119.wepro.auth.dto.response.KakaoTokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@RequiredArgsConstructor +public class KakaoService { + + private final KakaoOauthClient kakaoOauthClient; + + // 임시 + private static final String LOGIN_URI = "http://localhost:8080/login"; + + @Value("${kakao.client-id}") + private String CLIENT_ID; + + @Value("${kakao.redirect-uri}") + private String REDIRECT_URI; + + @Value("${kakao.client-secret}") + private String CLIENT_SECRET; + + @Value("${kakao.authorization-uri}") + private String KAKAO_AUTH_URL; + + // 백엔드용 + public String generateKakaoRedirectUrl() { + return UriComponentsBuilder.fromUriString(KAKAO_AUTH_URL) + .queryParam("client_id", CLIENT_ID) + .queryParam("redirect_uri", REDIRECT_URI) + .queryParam("response_type", "code") + .build() + .toUriString(); + } + + // 백엔드용 + public SignInResponse handleKakaoCallback(String code) { + KakaoTokenResponse tokenResponse = kakaoOauthClient.kakaoAuth(CLIENT_ID, REDIRECT_URI, code, + CLIENT_SECRET); + String idToken = tokenResponse.getIdToken(); + ResponseEntity response = callLoginApiWithIdToken(idToken); + + return response.getBody(); + } + + // 백엔드용 + private ResponseEntity callLoginApiWithIdToken(String idToken) { + RestTemplate restTemplate = new RestTemplate(); + SignInRequest signInRequest = new SignInRequest(Provider.KAKAO, idToken); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(signInRequest, headers); + + ResponseEntity response = restTemplate.postForEntity(LOGIN_URI, requestEntity, + SignInResponse.class); + + return response; + } +} From c81399c3cf286f4fa9ca89f854f32c601a42ca6e Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Wed, 4 Sep 2024 15:07:03 +0900 Subject: [PATCH 13/24] =?UTF-8?q?chore:=20yaml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_119/wepro/auth/service/KakaoService.java | 4 ++-- src/main/resources/application.yaml | 22 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/_119/wepro/auth/service/KakaoService.java b/src/main/java/com/_119/wepro/auth/service/KakaoService.java index 2b47d87..03b76bd 100644 --- a/src/main/java/com/_119/wepro/auth/service/KakaoService.java +++ b/src/main/java/com/_119/wepro/auth/service/KakaoService.java @@ -21,8 +21,8 @@ public class KakaoService { private final KakaoOauthClient kakaoOauthClient; - // 임시 - private static final String LOGIN_URI = "http://localhost:8080/login"; + @Value("${login.uri}") + private String LOGIN_URI; @Value("${kakao.client-id}") private String CLIENT_ID; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b624804..d51f2aa 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -14,6 +14,22 @@ spring: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect - - - +--- +spring: + data: + redis: + host: localhost + port: 6379 +--- +jwt: + secret: ${jwt.secret} +--- +login: + uri: ${login.uri} +--- +kakao: + client-id: ${kakao.client-id} + client-secret: ${kakao.client-secret} + redirect-uri: ${kakao.redirect-uri} + iss: https://kauth.kakao.com + authorization-uri: https://kauth.kakao.com/oauth/authorize \ No newline at end of file From 68f663089408d716641cdcf073ac25c6dbfbf537 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Wed, 4 Sep 2024 15:17:12 +0900 Subject: [PATCH 14/24] =?UTF-8?q?chore:=20deploy.yaml=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e3d9c76..50bba31 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -33,6 +33,11 @@ jobs: spring.datasource.url: ${{ secrets.SPRING_DATASOURCE_URL }} spring.datasource.username: ${{ secrets.SPRING_DATASOURCE_USERNAME }} spring.datasource.password: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} + jwt.secret: ${{ secrets.JWT_SECRET }} + login.uri: ${{ secrets.LOGIN_URI }} + kakao.client-id: ${{ secrets.KAKAO_CLIENT_ID }} + kakao.client-secret: ${{ secrets.KAKAO_CLIENT_SECRET }} + kakao.redirect-uri: ${{ secrets.KAKAO_REDIRECT_URI }} - name: Grant execute permission for gradlew run: chmod +x ./gradlew From af0b209894cb7168db6153e3d1b0929c14c64338 Mon Sep 17 00:00:00 2001 From: minje0204 Date: Wed, 4 Sep 2024 15:24:58 +0900 Subject: [PATCH 15/24] feature: add generate member tag --- .github/workflows/deploy.yaml | 1 + src/main/java/com/_119/wepro/member/domain/Member.java | 10 ++++++++-- src/main/resources/application.yaml | 9 +++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e3d9c76..2e9a2d0 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -33,6 +33,7 @@ jobs: spring.datasource.url: ${{ secrets.SPRING_DATASOURCE_URL }} spring.datasource.username: ${{ secrets.SPRING_DATASOURCE_USERNAME }} spring.datasource.password: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} + kakao.client-id: ${{secrets.KAKAO_CLIENT_ID}} - name: Grant execute permission for gradlew run: chmod +x ./gradlew diff --git a/src/main/java/com/_119/wepro/member/domain/Member.java b/src/main/java/com/_119/wepro/member/domain/Member.java index d40bba5..82c4c43 100644 --- a/src/main/java/com/_119/wepro/member/domain/Member.java +++ b/src/main/java/com/_119/wepro/member/domain/Member.java @@ -12,6 +12,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.PostPersist; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -56,6 +57,12 @@ public class Member extends BaseEntity { private LocalDateTime inactivatedAt; + // 엔티티가 저장된 후 id로 태그를 생성합니다. + @PostPersist + public void generateTag() { + this.tag = this.id.toString(); + } + public static Member of(SignInRequest request, OidcUser oidcDecodePayload) { return Member.builder() .profile(oidcDecodePayload.getPicture()) @@ -64,8 +71,7 @@ public static Member of(SignInRequest request, OidcUser oidcDecodePayload) { .role(Role.GUEST) .providerId(oidcDecodePayload.getName()) .status(Status.ACTIVE) - //Todo 태그 생성하기 -// .tag() + // 태그는 나중에 설정됩니다. .build(); } } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b624804..4087994 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,5 +15,10 @@ spring: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect - - +--- +kakao: + client-id: ${kakao.client-id} + client-secret: EN0ZGfmtay9f5WQzqaFtwAa6vh3YJ5F3 + redirect-uri: http://localhost:8080/login/oauth2/code/kakao + iss: https://kauth.kakao.com + authorization-uri: https://kauth.kakao.com/oauth/authorize From c4eaa6be9f8556a99ce5e2ef92b649b002127df3 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Wed, 4 Sep 2024 15:27:16 +0900 Subject: [PATCH 16/24] =?UTF-8?q?fix:=20application.yaml=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d51f2aa..2a79187 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -14,22 +14,20 @@ spring: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect ---- -spring: data: redis: host: localhost port: 6379 ---- + jwt: secret: ${jwt.secret} ---- + login: uri: ${login.uri} ---- + kakao: client-id: ${kakao.client-id} client-secret: ${kakao.client-secret} redirect-uri: ${kakao.redirect-uri} iss: https://kauth.kakao.com - authorization-uri: https://kauth.kakao.com/oauth/authorize \ No newline at end of file + authorization-uri: https://kauth.kakao.com/oauth/authorize From 8ccc6ac14f599f325333e3c3eb9a1db7f483dcde Mon Sep 17 00:00:00 2001 From: Minje Cho Date: Wed, 4 Sep 2024 15:53:05 +0900 Subject: [PATCH 17/24] Update deploy.yaml --- .github/workflows/deploy.yaml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 50bba31..c6a33bb 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - S3_BUCKET_NAME: wepro + S3_BUCKET_NAME: wepro1 RESOURCE_PATH: ./src/main/resources/application.yaml CODE_DEPLOY_APPLICATION_NAME: wepro-code-deploy CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: wepro-server @@ -16,6 +16,20 @@ jobs: build: runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + ports: + - 3306:3306 + env: + MYSQL_ROOT_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} + MYSQL_DATABASE: wepro + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + steps: - name: Checkout uses: actions/checkout@v2 @@ -43,6 +57,13 @@ jobs: run: chmod +x ./gradlew shell: bash + - name: Wait for MySQL + run: | + while ! mysqladmin ping -h"127.0.0.1" --silent; do + echo "Waiting for MySQL to be ready..." + sleep 1 + done + - name: Build with Gradle run: ./gradlew build shell: bash @@ -67,4 +88,4 @@ jobs: --deployment-config-name CodeDeployDefault.AllAtOnce \ --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \ --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \ - --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip \ No newline at end of file + --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip From 285103e87d1d9afc6fc9991a5a05c2ebb690d6ca Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Sat, 7 Sep 2024 12:28:08 +0900 Subject: [PATCH 18/24] =?UTF-8?q?feat:=20jwt=20exception=20filter=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../auth/jwt/JwtTokenExceptionFilter.java | 50 +++++++++++++++++++ .../_119/wepro/auth/jwt/JwtTokenFilter.java | 7 --- .../wepro/global/config/SecurityConfig.java | 13 +++-- 3 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/_119/wepro/auth/jwt/JwtTokenExceptionFilter.java diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenExceptionFilter.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenExceptionFilter.java new file mode 100644 index 0000000..57a9415 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenExceptionFilter.java @@ -0,0 +1,50 @@ +package com._119.wepro.auth.jwt; + +import com._119.wepro.global.dto.ErrorResponseDto; +import com._119.wepro.global.exception.RestApiException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +public class JwtTokenExceptionFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (RestApiException e) { + logClientIpAndRequestUri(request); + sendErrorResponse(response, e); + } + } + + private void logClientIpAndRequestUri(HttpServletRequest request) { + String clientIp = request.getHeader("X-Forwarded-For"); + if (clientIp == null) { + clientIp = request.getRemoteAddr(); + } + log.error("Invalid token for requestURI: {}, Access from IP: {}", request.getRequestURI(), + clientIp); + } + + private void sendErrorResponse(HttpServletResponse response, RestApiException e) + throws IOException { + ErrorResponseDto errorResponseDto = ErrorResponseDto.builder() + .code(e.getErrorCode().name()) + .message(e.getErrorCode().getMessage()) + .build(); + + response.setStatus(e.getErrorCode().getHttpStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponseDto)); + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java index aa7a07d..341c3d3 100644 --- a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java @@ -36,13 +36,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse SecurityContextHolder.getContext().setAuthentication(authentication); }); - - String clientIp = request.getHeader("X-Forwarded-For"); - if (clientIp == null) { - clientIp = request.getRemoteAddr(); - } - log.error("Invalid token for requestURI: {}, Access from IP: {}", request.getRequestURI(), clientIp); - filterChain.doFilter(request, response); } diff --git a/src/main/java/com/_119/wepro/global/config/SecurityConfig.java b/src/main/java/com/_119/wepro/global/config/SecurityConfig.java index b01fa6b..f19e5da 100644 --- a/src/main/java/com/_119/wepro/global/config/SecurityConfig.java +++ b/src/main/java/com/_119/wepro/global/config/SecurityConfig.java @@ -1,7 +1,8 @@ package com._119.wepro.global.config; -import com._119.wepro.auth.jwt.JwtTokenProvider; +import com._119.wepro.auth.jwt.JwtTokenExceptionFilter; import com._119.wepro.auth.jwt.JwtTokenFilter; +import com._119.wepro.auth.jwt.JwtTokenProvider; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -26,9 +27,10 @@ public class SecurityConfig { public WebSecurityCustomizer webSecurityCustomizer() { // security를 적용하지 않을 리소스 return web -> web.ignoring() .requestMatchers("/css/**", "/images/**", "/js/**", "/lib/**") - .requestMatchers("/", "/swagger-ui-custom.html", "/api-docs/**", "/swagger-ui/**", "swagger-ui.html", "/v3/api-docs/**") + .requestMatchers("/", "/swagger-ui-custom.html", "/api-docs/**", "/swagger-ui/**", + "swagger-ui.html", "/v3/api-docs/**") .requestMatchers("/error", "/favicon.ico") - .requestMatchers("/auth/**", "/login/oauth2/code/**", "/login"); + .requestMatchers("/auth/**", "/login/oauth2/code/**", "/login", "/refresh"); } @Bean @@ -42,7 +44,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated() ) - .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + .logout(logout -> logout.disable()) + .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), + UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new JwtTokenExceptionFilter(), JwtTokenFilter.class); return http.build(); } } From e2c84cad44f74f585f83bdc21aa2bc9cb5672aa1 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Sat, 7 Sep 2024 12:29:34 +0900 Subject: [PATCH 19/24] =?UTF-8?q?refactor:=20oidcCacheManager=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추후에 쓰지 않을 코드 관련이라 삭제 related to: #11 --- .../wepro/auth/client/KakaoOauthClient.java | 5 ++-- .../_119/wepro/global/config/RedisConfig.java | 23 ------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java b/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java index f673bc9..02e779d 100644 --- a/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java +++ b/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java @@ -2,7 +2,6 @@ import com._119.wepro.auth.dto.response.KakaoTokenResponse; import com._119.wepro.auth.dto.response.OIDCPublicKeyResponse; -import org.springframework.cache.annotation.Cacheable; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -23,8 +22,8 @@ KakaoTokenResponse kakaoAuth( @PathVariable("CODE") String code, @PathVariable("CLIENT_SECRET") String client_secret); - // oidc 공개 키 받아 오기 - @Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager") // 공개키 자주 요청할 거 같으면, 캐싱하기 + // oidc 공개 키 받아 오기 - 안 쓸 예정 +// @Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager") // 공개키 자주 요청할 거 같으면, 캐싱하기 @GetMapping("/.well-known/jwks.json") OIDCPublicKeyResponse getOIDCPublicKey(); } \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/global/config/RedisConfig.java b/src/main/java/com/_119/wepro/global/config/RedisConfig.java index e34aa5c..a5f48ea 100644 --- a/src/main/java/com/_119/wepro/global/config/RedisConfig.java +++ b/src/main/java/com/_119/wepro/global/config/RedisConfig.java @@ -1,18 +1,12 @@ package com._119.wepro.global.config; -import java.time.Duration; import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; 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.repository.configuration.EnableRedisRepositories; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @@ -30,23 +24,6 @@ public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(host, port); } - @Bean - public CacheManager oidcCacheManager(RedisConnectionFactory cf) { - RedisCacheConfiguration redisCacheConfiguration = - RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith( - RedisSerializationContext.SerializationPair.fromSerializer( - new StringRedisSerializer())) - .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer( - new GenericJackson2JsonRedisSerializer())) - .entryTtl(Duration.ofDays(7L)); - - return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf) - .cacheDefaults(redisCacheConfiguration) - .build(); - } - @Bean public RedisTemplate redisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); From 6693930bbbe3c7bcb55ada14b2f5f0b88aae656d Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Sat, 7 Sep 2024 12:31:06 +0900 Subject: [PATCH 20/24] =?UTF-8?q?feat:=20RedisUtil=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- .../com/_119/wepro/global/util/RedisUtil.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/java/com/_119/wepro/global/util/RedisUtil.java diff --git a/src/main/java/com/_119/wepro/global/util/RedisUtil.java b/src/main/java/com/_119/wepro/global/util/RedisUtil.java new file mode 100644 index 0000000..872d4a4 --- /dev/null +++ b/src/main/java/com/_119/wepro/global/util/RedisUtil.java @@ -0,0 +1,42 @@ +package com._119.wepro.global.util; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class RedisUtil { + + private final RedisTemplate redisTemplate; + + public String getData(String key) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + return valueOperations.get(key); + } + + public void setData(String key, String value) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value); + } + + public void setDataExpire(String key, String value, long duration) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value, Duration.ofMillis(duration)); + } + + public void deleteData(String key) { + redisTemplate.delete(key); + } + + public void expireValues(String key, int timeout) { + redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS); + } + + public boolean existsData(String key) { + return redisTemplate.hasKey(key); + } +} From 5825446acef96231d19ac4d58e47bf3d6f2c3eb2 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Sat, 7 Sep 2024 12:32:50 +0900 Subject: [PATCH 21/24] =?UTF-8?q?feat:=20token=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89,=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로그인 시, refresh token 저장 로직도 추가 related to: #11 --- .../wepro/auth/dto/request/AuthRequest.java | 12 ++++ .../wepro/auth/dto/response/AuthResponse.java | 2 - .../_119/wepro/auth/jwt/JwtTokenProvider.java | 42 ++++++++++--- .../auth/presentation/AuthController.java | 23 ++++++- .../{SignInService.java => AuthService.java} | 15 +++-- .../wepro/auth/service/RefreshService.java | 60 +++++++++++++++++++ .../exception/errorcode/CommonErrorCode.java | 1 + 7 files changed, 137 insertions(+), 18 deletions(-) rename src/main/java/com/_119/wepro/auth/service/{SignInService.java => AuthService.java} (94%) create mode 100644 src/main/java/com/_119/wepro/auth/service/RefreshService.java diff --git a/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java b/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java index 10fc7c0..f3c6cee 100644 --- a/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java +++ b/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java @@ -25,6 +25,18 @@ public static class SignInRequest { private String idToken; } + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RefreshRequest { + @NotNull + private String accessToken; + + @NotNull + private String refreshToken; + } + @Getter @Builder @NoArgsConstructor diff --git a/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java b/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java index c1741cb..e6b4bab 100644 --- a/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java +++ b/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java @@ -2,13 +2,11 @@ import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; public class AuthResponse { @Getter @AllArgsConstructor - @NoArgsConstructor public static class SignInResponse { private boolean newMember; diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java index 6687d4d..7da32e1 100644 --- a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java @@ -4,6 +4,7 @@ import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN; import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.global.util.RedisUtil; import com._119.wepro.global.enums.Role; import com._119.wepro.global.exception.RestApiException; import io.jsonwebtoken.Claims; @@ -30,21 +31,25 @@ @Component public class JwtTokenProvider { - private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24; // 1일 + private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 1일 private static final long REFRESH_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 7일 - + private static final String AUTHORITIES_KEY = "auth"; + private final RedisUtil redisUtil; private SecretKey secretKey; - public JwtTokenProvider(@Value("${jwt.secret}") String key) { + public JwtTokenProvider(@Value("${jwt.secret}") String key, RedisUtil redisUtil) { + this.redisUtil = redisUtil; byte[] keyBytes = key.getBytes(); this.secretKey = Keys.hmacShaKeyFor(keyBytes); } public TokenInfo generateToken(Long memberId, Role memberRole) { String accessToken = generateAccessToken(memberId, memberRole); - // TODO: refresh redis에 저장 String refreshToken = generateRefreshToken(); + deleteInvalidRefreshToken(memberId.toString()); + redisUtil.setData(memberId.toString(), refreshToken); + return new TokenInfo("Bearer", accessToken, refreshToken); } @@ -61,7 +66,7 @@ public boolean validateToken(String token) { public Authentication getAuthentication(String accessToken) { Claims claims = parseClaims(accessToken); - if (claims.get("auth") == null) { + if (claims.get(AUTHORITIES_KEY) == null) { throw new RestApiException(INVALID_TOKEN); } List authority = getAuthorities(claims); @@ -89,7 +94,6 @@ private boolean isValidAuthority(SimpleGrantedAuthority authority) { private Claims parseClaims(String accessToken) { try { - // TODO: 블랙 리스트 여부 추가 return Jwts.parserBuilder() .setSigningKey(secretKey) .build() @@ -114,7 +118,7 @@ private String generateAccessToken(Long memberId, Role memberRole) { return Jwts.builder() .setSubject(memberId.toString()) - .claim("auth", memberRole.name()) + .claim(AUTHORITIES_KEY, memberRole.name()) .setIssuedAt(now) .setExpiration(expiredDate) .signWith(secretKey, SignatureAlgorithm.HS256) @@ -133,6 +137,28 @@ private String generateRefreshToken() { private List getAuthorities(Claims claims) { return Collections.singletonList(new SimpleGrantedAuthority( - claims.get("auth").toString())); + claims.get(AUTHORITIES_KEY).toString())); + } + + public String getRefreshToken(String memberId){ + return redisUtil.getData(memberId); + } + + public void deleteInvalidRefreshToken(String memberId) { + redisUtil.deleteData(memberId); + } + + public Claims parseExpiredToken(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } catch (JwtException e) { + throw new RestApiException(INVALID_TOKEN); + } } } diff --git a/src/main/java/com/_119/wepro/auth/presentation/AuthController.java b/src/main/java/com/_119/wepro/auth/presentation/AuthController.java index 0e71987..f9f8c8f 100644 --- a/src/main/java/com/_119/wepro/auth/presentation/AuthController.java +++ b/src/main/java/com/_119/wepro/auth/presentation/AuthController.java @@ -1,8 +1,10 @@ package com._119.wepro.auth.presentation; import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; +import com._119.wepro.auth.dto.response.TokenInfo; import com._119.wepro.auth.service.KakaoService; -import com._119.wepro.auth.service.SignInService; +import com._119.wepro.auth.service.RefreshService; +import com._119.wepro.auth.service.AuthService; import com._119.wepro.auth.dto.request.AuthRequest.*; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -23,13 +25,28 @@ public class AuthController { private final KakaoService kakaoService; - private final SignInService signInService; + private final AuthService authService; + private final RefreshService refreshService; @PostMapping("/login") @Operation(summary = "Kakao idToken 받아 소셜 로그인") public ResponseEntity signIn( @RequestBody @Valid SignInRequest request) { - return ResponseEntity.ok(signInService.signIn(request)); + return ResponseEntity.ok(authService.signIn(request)); + } + + @PostMapping("/refresh") + @Operation(summary = "access token 재발급") + public ResponseEntity refresh( + @RequestBody @Valid RefreshRequest request) { + return ResponseEntity.ok(refreshService.refresh(request)); + } + + @PostMapping("/logout") + @Operation(summary = "로그아웃") + public ResponseEntity logout(Authentication authentication){ + authService.logOut(authentication.getName()); + return ResponseEntity.ok().build(); } @PostMapping("/signup") diff --git a/src/main/java/com/_119/wepro/auth/service/SignInService.java b/src/main/java/com/_119/wepro/auth/service/AuthService.java similarity index 94% rename from src/main/java/com/_119/wepro/auth/service/SignInService.java rename to src/main/java/com/_119/wepro/auth/service/AuthService.java index 38c114d..09903f8 100644 --- a/src/main/java/com/_119/wepro/auth/service/SignInService.java +++ b/src/main/java/com/_119/wepro/auth/service/AuthService.java @@ -3,32 +3,32 @@ import static com._119.wepro.global.enums.Provider.APPLE; import static com._119.wepro.global.enums.Provider.KAKAO; +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.auth.jwt.JwtTokenProvider; import com._119.wepro.global.enums.Provider; import com._119.wepro.global.enums.Role; -import com._119.wepro.auth.jwt.JwtTokenProvider; import com._119.wepro.member.domain.Member; import com._119.wepro.member.domain.repository.MemberRepository; -import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; import jakarta.transaction.Transactional; import java.util.Map; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.stereotype.Service; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor -public class SignInService { +public class AuthService { private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; @@ -52,6 +52,11 @@ public SignInResponse signIn(SignInRequest request) { return new SignInResponse(isNewMember, tokenInfo); } + @Transactional + public void logOut(String memberId) { + jwtTokenProvider.deleteInvalidRefreshToken(memberId); + } + private Member getOrSaveUser(SignInRequest request, OidcUser oidcDecodePayload) { Optional member = memberRepository.findByProviderAndProviderId( diff --git a/src/main/java/com/_119/wepro/auth/service/RefreshService.java b/src/main/java/com/_119/wepro/auth/service/RefreshService.java new file mode 100644 index 0000000..a569541 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/service/RefreshService.java @@ -0,0 +1,60 @@ +package com._119.wepro.auth.service; + +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.EXPIRED_TOKEN; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.REFRESH_DENIED; + +import com._119.wepro.auth.dto.request.AuthRequest.RefreshRequest; +import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.auth.jwt.JwtTokenProvider; +import com._119.wepro.global.exception.RestApiException; +import com._119.wepro.global.exception.errorcode.UserErrorCode; +import com._119.wepro.member.domain.Member; +import com._119.wepro.member.domain.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RefreshService { + + private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + + public TokenInfo refresh(RefreshRequest request) { + String accessToken = request.getAccessToken(); + String refreshToken = request.getRefreshToken(); + + if (!isTokenExpired(accessToken)) { + throw new RestApiException(REFRESH_DENIED); + } + String memberId = jwtTokenProvider.parseExpiredToken(accessToken) + .getSubject(); + Member member = memberRepository.findById(Long.parseLong(memberId)) + .orElseThrow(() -> new RestApiException(UserErrorCode.USER_NOT_FOUND)); + + validateRefreshToken(refreshToken, memberId); + return jwtTokenProvider.generateToken(Long.parseLong(memberId), member.getRole()); + } + + private boolean isTokenExpired(String accessToken) { + try { + jwtTokenProvider.validateToken(accessToken); + throw new RestApiException(REFRESH_DENIED); + } catch (RestApiException e) { + if (e.getErrorCode() == EXPIRED_TOKEN) { + return true; + } + throw e; + } + } + + private void validateRefreshToken(String refreshToken, String memberId) { + String savedRefreshToken = jwtTokenProvider.getRefreshToken(memberId); + if (!refreshToken.equals(savedRefreshToken)) { + throw new RestApiException(INVALID_TOKEN); + } + } +} diff --git a/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java b/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java index 5030011..f3cf9cb 100644 --- a/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java +++ b/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java @@ -13,6 +13,7 @@ public enum CommonErrorCode implements ErrorCode { INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid token"), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Expired token"), NOT_EXIST_BEARER_SUFFIX(HttpStatus.UNAUTHORIZED, "Bearer prefix is missing."), + REFRESH_DENIED(HttpStatus.FORBIDDEN, "Refresh denied"), ; private final HttpStatus httpStatus; From 647493e435c6e30b7925db518acfefad088ec105 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Sat, 7 Sep 2024 12:34:34 +0900 Subject: [PATCH 22/24] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java index 7da32e1..e830ba2 100644 --- a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java @@ -31,7 +31,7 @@ @Component public class JwtTokenProvider { - private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 1일 + private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24; // 1일 private static final long REFRESH_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 7일 private static final String AUTHORITIES_KEY = "auth"; private final RedisUtil redisUtil; From ef7c73a97403b293dbe2bf5f5431306c3218e0be Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Sun, 8 Sep 2024 00:05:28 +0900 Subject: [PATCH 23/24] =?UTF-8?q?chore:=20redis=20container=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit related to: #11 --- db/docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/db/docker-compose.yml b/db/docker-compose.yml index c1fea26..8aa9795 100644 --- a/db/docker-compose.yml +++ b/db/docker-compose.yml @@ -18,3 +18,12 @@ services: - ${DEFAULT_PATH}/mysql/data:/var/lib/mysql - ${DEFAULT_PATH}/mysql/initdb.d:/docker-entrypoint-initdb.d restart: always + redis: + container_name: "redis" + image: redis:latest + command: redis-server --port 6379 + ports: + - "6379:6379" + volumes: + - ${DEFAULT_PATH}/redis/data:/data + restart: always \ No newline at end of file From 8811e4ec32822532e67867567d033d184cbf9216 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Sun, 8 Sep 2024 02:23:46 +0900 Subject: [PATCH 24/24] =?UTF-8?q?fix:=20subject=20=EA=B0=92=20providerId?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit providerId 인덱스도 추가 related to: #11 --- .../_119/wepro/auth/jwt/JwtTokenProvider.java | 22 +++++++++---------- .../_119/wepro/auth/service/AuthService.java | 6 ++--- .../wepro/auth/service/RefreshService.java | 9 ++++---- .../com/_119/wepro/member/domain/Member.java | 9 +++++++- .../domain/repository/MemberRepository.java | 3 +++ 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java index e830ba2..c95870c 100644 --- a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java @@ -4,9 +4,9 @@ import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN; import com._119.wepro.auth.dto.response.TokenInfo; -import com._119.wepro.global.util.RedisUtil; import com._119.wepro.global.enums.Role; import com._119.wepro.global.exception.RestApiException; +import com._119.wepro.global.util.RedisUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; @@ -43,12 +43,12 @@ public JwtTokenProvider(@Value("${jwt.secret}") String key, RedisUtil redisUtil) this.secretKey = Keys.hmacShaKeyFor(keyBytes); } - public TokenInfo generateToken(Long memberId, Role memberRole) { - String accessToken = generateAccessToken(memberId, memberRole); + public TokenInfo generateToken(String providerId, Role memberRole) { + String accessToken = generateAccessToken(providerId, memberRole); String refreshToken = generateRefreshToken(); - deleteInvalidRefreshToken(memberId.toString()); - redisUtil.setData(memberId.toString(), refreshToken); + deleteInvalidRefreshToken(providerId); + redisUtil.setData(providerId, refreshToken); return new TokenInfo("Bearer", accessToken, refreshToken); } @@ -112,12 +112,12 @@ private Claims parseClaims(String accessToken) { } } - private String generateAccessToken(Long memberId, Role memberRole) { + private String generateAccessToken(String providerId, Role memberRole) { Date now = new Date(); Date expiredDate = new Date(now.getTime() + ACCESS_TOKEN_DURATION); return Jwts.builder() - .setSubject(memberId.toString()) + .setSubject(providerId) .claim(AUTHORITIES_KEY, memberRole.name()) .setIssuedAt(now) .setExpiration(expiredDate) @@ -140,12 +140,12 @@ private List getAuthorities(Claims claims) { claims.get(AUTHORITIES_KEY).toString())); } - public String getRefreshToken(String memberId){ - return redisUtil.getData(memberId); + public String getRefreshToken(String provierId) { + return redisUtil.getData(provierId); } - public void deleteInvalidRefreshToken(String memberId) { - redisUtil.deleteData(memberId); + public void deleteInvalidRefreshToken(String provierId) { + redisUtil.deleteData(provierId); } public Claims parseExpiredToken(String token) { diff --git a/src/main/java/com/_119/wepro/auth/service/AuthService.java b/src/main/java/com/_119/wepro/auth/service/AuthService.java index 09903f8..865bde6 100644 --- a/src/main/java/com/_119/wepro/auth/service/AuthService.java +++ b/src/main/java/com/_119/wepro/auth/service/AuthService.java @@ -46,15 +46,15 @@ public SignInResponse signIn(SignInRequest request) { OidcUser oidcDecodePayload = socialLogin(request); Member member = getOrSaveUser(request, oidcDecodePayload); - TokenInfo tokenInfo = jwtTokenProvider.generateToken(member.getId(), member.getRole()); + TokenInfo tokenInfo = jwtTokenProvider.generateToken(member.getProviderId(), member.getRole()); boolean isNewMember = Role.GUEST == member.getRole(); return new SignInResponse(isNewMember, tokenInfo); } @Transactional - public void logOut(String memberId) { - jwtTokenProvider.deleteInvalidRefreshToken(memberId); + public void logOut(String providerId) { + jwtTokenProvider.deleteInvalidRefreshToken(providerId); } diff --git a/src/main/java/com/_119/wepro/auth/service/RefreshService.java b/src/main/java/com/_119/wepro/auth/service/RefreshService.java index a569541..ed0d865 100644 --- a/src/main/java/com/_119/wepro/auth/service/RefreshService.java +++ b/src/main/java/com/_119/wepro/auth/service/RefreshService.java @@ -30,13 +30,14 @@ public TokenInfo refresh(RefreshRequest request) { if (!isTokenExpired(accessToken)) { throw new RestApiException(REFRESH_DENIED); } - String memberId = jwtTokenProvider.parseExpiredToken(accessToken) + String providerId = jwtTokenProvider.parseExpiredToken(accessToken) .getSubject(); - Member member = memberRepository.findById(Long.parseLong(memberId)) + validateRefreshToken(refreshToken, providerId); + + Member member = memberRepository.findByProviderId(providerId) .orElseThrow(() -> new RestApiException(UserErrorCode.USER_NOT_FOUND)); - validateRefreshToken(refreshToken, memberId); - return jwtTokenProvider.generateToken(Long.parseLong(memberId), member.getRole()); + return jwtTokenProvider.generateToken(providerId, member.getRole()); } private boolean isTokenExpired(String accessToken) { diff --git a/src/main/java/com/_119/wepro/member/domain/Member.java b/src/main/java/com/_119/wepro/member/domain/Member.java index 82c4c43..f7125c1 100644 --- a/src/main/java/com/_119/wepro/member/domain/Member.java +++ b/src/main/java/com/_119/wepro/member/domain/Member.java @@ -1,10 +1,10 @@ package com._119.wepro.member.domain; +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; import com._119.wepro.global.BaseEntity; import com._119.wepro.global.enums.Provider; import com._119.wepro.global.enums.Role; import com._119.wepro.global.enums.Status; -import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -12,7 +12,9 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Index; import jakarta.persistence.PostPersist; +import jakarta.persistence.Table; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -26,6 +28,11 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder +@Table( + indexes = { + @Index(name = "idx_provider_id", columnList = "providerId") + } +) public class Member extends BaseEntity { @Id diff --git a/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java b/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java index a3b4973..bd3d0cd 100644 --- a/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java +++ b/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java @@ -8,5 +8,8 @@ @Repository public interface MemberRepository extends JpaRepository { + Optional findByProviderAndProviderId(Provider provider, String providerId); + + Optional findByProviderId(String providerId); }