From c8c2a828b66f47757d2dee06ea6f9efea569905a Mon Sep 17 00:00:00 2001 From: Minjoon Park Date: Sat, 11 Jan 2025 03:59:00 +0900 Subject: [PATCH] =?UTF-8?q?feature/#64=20rtr=20=EA=B8=B0=EB=B0=98=20refres?= =?UTF-8?q?h=20token=20=EB=B0=9C=ED=96=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/auth/application/AuthFacade.java | 44 ++++++++++++++ .../api/auth/application/AuthService.java | 58 ------------------- .../auth/presentation/AuthApiController.java | 29 +++++++--- .../request/jwt/RefreshTokenRequest.java | 15 +++++ .../request/oauth}/OidcLoginRequest.java | 2 +- .../response/NoisePersistResponse.java | 2 +- .../src/main/resources/application.yml | 6 ++ soridam-auth/build.gradle | 3 + .../soridam/auth/config/SecurityConfig.java | 9 +-- .../auth/jwt/JwtAuthenticationFilter.java | 1 + .../jwt/{ => application}/JwtProvider.java | 20 +++++-- .../auth/jwt/application/JwtService.java | 46 +++++++++++++++ .../auth/jwt/response}/JwtResponse.java | 2 +- .../soridam/auth/oauth/OidcService.java | 7 +-- soridam-domain/build.gradle | 1 + .../application/NoiseCommandService.java | 4 +- .../soridam/domain/refresh/RefreshToken.java | 28 +++++++++ .../refresh/RefreshTokenRepository.java | 11 ++++ .../application/RefreshTokenService.java | 24 ++++++++ .../exception/RefreshTokenExceptionCode.java | 24 ++++++++ .../RefreshTokenNotFoundException.java | 11 ++++ .../user/application/UserCommandService.java | 12 +++- .../user/application/UserQueryService.java | 4 +- .../soridam/domain/user/domain/User.java | 4 +- .../globalutil/logging/LoggingUtils.java | 4 ++ soridam-infra/build.gradle | 1 + .../infra/config/redis/RedisConfig.java | 56 ++++++++++++++++++ 27 files changed, 337 insertions(+), 91 deletions(-) create mode 100644 soridam-api/src/main/java/sorisoop/soridam/api/auth/application/AuthFacade.java delete mode 100644 soridam-api/src/main/java/sorisoop/soridam/api/auth/application/AuthService.java create mode 100644 soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/request/jwt/RefreshTokenRequest.java rename {soridam-auth/src/main/java/sorisoop/soridam/auth/oauth/request => soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/request/oauth}/OidcLoginRequest.java (84%) rename soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/{ => application}/JwtProvider.java (82%) create mode 100644 soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/application/JwtService.java rename {soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/response/jwt => soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/response}/JwtResponse.java (82%) create mode 100644 soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/RefreshToken.java create mode 100644 soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/RefreshTokenRepository.java create mode 100644 soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/application/RefreshTokenService.java create mode 100644 soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/exception/RefreshTokenExceptionCode.java create mode 100644 soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/exception/RefreshTokenNotFoundException.java create mode 100644 soridam-infra/src/main/java/sorisoop/soridam/infra/config/redis/RedisConfig.java diff --git a/soridam-api/src/main/java/sorisoop/soridam/api/auth/application/AuthFacade.java b/soridam-api/src/main/java/sorisoop/soridam/api/auth/application/AuthFacade.java new file mode 100644 index 0000000..3fc206b --- /dev/null +++ b/soridam-api/src/main/java/sorisoop/soridam/api/auth/application/AuthFacade.java @@ -0,0 +1,44 @@ +package sorisoop.soridam.api.auth.application; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import sorisoop.soridam.api.auth.presentation.request.jwt.JwtLoginRequest; +import sorisoop.soridam.api.auth.presentation.request.jwt.RefreshTokenRequest; +import sorisoop.soridam.api.auth.presentation.request.oauth.OidcLoginRequest; +import sorisoop.soridam.auth.jwt.application.JwtService; +import sorisoop.soridam.auth.jwt.response.JwtResponse; +import sorisoop.soridam.auth.oauth.google.GoogleOidcService; +import sorisoop.soridam.auth.oauth.kakao.KakaoOidcService; +import sorisoop.soridam.domain.user.domain.User; + +@Service +@RequiredArgsConstructor +public class AuthFacade { + private final KakaoOidcService kakaoOidcService; + private final GoogleOidcService googleOidcService; + private final JwtService jwtService; + + public JwtResponse jwtLogin(JwtLoginRequest request) { + return jwtService.jwtLogin(request.email(), request.password()); + } + + @Transactional + public JwtResponse kakaoLogin(OidcLoginRequest idToken) { + User user = kakaoOidcService.processLogin(idToken.idToken()); + user.updateLastLoginTime(); + return jwtService.getToken(user); + } + + @Transactional + public JwtResponse googleLogin(OidcLoginRequest idToken) { + User user = googleOidcService.processLogin(idToken.idToken()); + user.updateLastLoginTime(); + return jwtService.getToken(user); + } + + public JwtResponse reissue(RefreshTokenRequest request) { + return jwtService.reissue(request.refreshToken()); + } +} diff --git a/soridam-api/src/main/java/sorisoop/soridam/api/auth/application/AuthService.java b/soridam-api/src/main/java/sorisoop/soridam/api/auth/application/AuthService.java deleted file mode 100644 index df4d098..0000000 --- a/soridam-api/src/main/java/sorisoop/soridam/api/auth/application/AuthService.java +++ /dev/null @@ -1,58 +0,0 @@ -package sorisoop.soridam.api.auth.application; - -import java.time.Duration; - -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import lombok.RequiredArgsConstructor; -import sorisoop.soridam.api.auth.presentation.request.jwt.JwtLoginRequest; -import sorisoop.soridam.api.auth.presentation.response.jwt.JwtResponse; -import sorisoop.soridam.auth.jwt.JwtProvider; -import sorisoop.soridam.auth.oauth.google.GoogleOidcService; -import sorisoop.soridam.auth.oauth.kakao.KakaoOidcService; -import sorisoop.soridam.auth.oauth.request.OidcLoginRequest; -import sorisoop.soridam.domain.user.application.UserQueryService; -import sorisoop.soridam.domain.user.domain.User; - -@Service -@Transactional -@RequiredArgsConstructor -public class AuthService { - private final UserQueryService userQueryService; - private final KakaoOidcService kakaoOidcService; - private final GoogleOidcService googleOidcService; - private final PasswordEncoder passwordEncoder; - private final JwtProvider jwtProvider; - - public JwtResponse jwtLogin(JwtLoginRequest request) { - String email = request.email(); - String password = request.password(); - - User user = userQueryService.getByEmail(email); - user.isPasswordMatching(password, passwordEncoder); - user.updateLastLoginTime(); - - return getToken(user); - } - - public JwtResponse kakaoLogin(OidcLoginRequest idToken) { - User user = kakaoOidcService.processLogin(idToken); - user.updateLastLoginTime(); - return getToken(user); - } - - public JwtResponse googleLogin(OidcLoginRequest idToken) { - User user = googleOidcService.processLogin(idToken); - user.updateLastLoginTime(); - return getToken(user); - } - - private JwtResponse getToken(User user) { - String refreshToken = jwtProvider.generateToken(user.getId(), user.getRole(), Duration.ofDays(7)); - String accessToken = jwtProvider.generateToken(user.getId(), user.getRole(), Duration.ofHours(2)); - - return JwtResponse.of(accessToken, refreshToken); - } -} diff --git a/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/AuthApiController.java b/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/AuthApiController.java index 9c01db0..4b1cc66 100644 --- a/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/AuthApiController.java +++ b/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/AuthApiController.java @@ -11,17 +11,18 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import sorisoop.soridam.api.auth.application.AuthService; +import sorisoop.soridam.api.auth.application.AuthFacade; import sorisoop.soridam.api.auth.presentation.request.jwt.JwtLoginRequest; -import sorisoop.soridam.api.auth.presentation.response.jwt.JwtResponse; -import sorisoop.soridam.auth.oauth.request.OidcLoginRequest; +import sorisoop.soridam.api.auth.presentation.request.jwt.RefreshTokenRequest; +import sorisoop.soridam.auth.jwt.response.JwtResponse; +import sorisoop.soridam.api.auth.presentation.request.oauth.OidcLoginRequest; @RestController @RequiredArgsConstructor @Tag(name = "Auth", description = "로그인 API") @RequestMapping("/api/auth") public class AuthApiController { - private final AuthService authService; + private final AuthFacade authFacade; @Operation(summary = "JWT 로그인 API", description = """ - Description : 이 API는 로그인 시 JWT를 발급합니다. @@ -33,7 +34,7 @@ public ResponseEntity login( @RequestBody JwtLoginRequest request ) { - JwtResponse response = authService.jwtLogin(request); + JwtResponse response = authFacade.jwtLogin(request); return ResponseEntity.ok(response); } @@ -41,9 +42,9 @@ public ResponseEntity login( public ResponseEntity kakaoSocialLogin( @Valid @RequestBody - OidcLoginRequest idToken + OidcLoginRequest request ){ - JwtResponse response = authService.kakaoLogin(idToken); + JwtResponse response = authFacade.kakaoLogin(request); return ResponseEntity.ok(response); } @@ -51,9 +52,19 @@ public ResponseEntity kakaoSocialLogin( public ResponseEntity googleSocialLogin( @Valid @RequestBody - OidcLoginRequest idToken + OidcLoginRequest request ){ - JwtResponse response = authService.googleLogin(idToken); + JwtResponse response = authFacade.googleLogin(request); + return ResponseEntity.ok(response); + } + + @PostMapping("/reissue") + public ResponseEntity reissue( + @Valid + @RequestBody + RefreshTokenRequest request + ){ + JwtResponse response = authFacade.reissue(request); return ResponseEntity.ok(response); } } diff --git a/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/request/jwt/RefreshTokenRequest.java b/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/request/jwt/RefreshTokenRequest.java new file mode 100644 index 0000000..16f57fe --- /dev/null +++ b/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/request/jwt/RefreshTokenRequest.java @@ -0,0 +1,15 @@ +package sorisoop.soridam.api.auth.presentation.request.jwt; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record RefreshTokenRequest( + @Schema(description = "refresh token", + example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsILJdhDYTYzNzQwNjQwMH0.7J", + requiredMode = REQUIRED) + @NotNull + String refreshToken +) { +} diff --git a/soridam-auth/src/main/java/sorisoop/soridam/auth/oauth/request/OidcLoginRequest.java b/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/request/oauth/OidcLoginRequest.java similarity index 84% rename from soridam-auth/src/main/java/sorisoop/soridam/auth/oauth/request/OidcLoginRequest.java rename to soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/request/oauth/OidcLoginRequest.java index d352ce5..4f4469e 100644 --- a/soridam-auth/src/main/java/sorisoop/soridam/auth/oauth/request/OidcLoginRequest.java +++ b/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/request/oauth/OidcLoginRequest.java @@ -1,4 +1,4 @@ -package sorisoop.soridam.auth.oauth.request; +package sorisoop.soridam.api.auth.presentation.request.oauth; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; diff --git a/soridam-api/src/main/java/sorisoop/soridam/api/noise/presentation/response/NoisePersistResponse.java b/soridam-api/src/main/java/sorisoop/soridam/api/noise/presentation/response/NoisePersistResponse.java index 6b50e54..51c0399 100644 --- a/soridam-api/src/main/java/sorisoop/soridam/api/noise/presentation/response/NoisePersistResponse.java +++ b/soridam-api/src/main/java/sorisoop/soridam/api/noise/presentation/response/NoisePersistResponse.java @@ -13,7 +13,7 @@ public record NoisePersistResponse( ) { public static NoisePersistResponse from(Noise noise){ return builder() - .id(noise.getId()) + .id(noise.extractUuid()) .build(); } } diff --git a/soridam-api/src/main/resources/application.yml b/soridam-api/src/main/resources/application.yml index 9e8e3ee..863316b 100644 --- a/soridam-api/src/main/resources/application.yml +++ b/soridam-api/src/main/resources/application.yml @@ -24,6 +24,12 @@ spring: init: mode: always + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD} + springdoc: default-consumes-media-type: application/json default-produces-media-type: application/json diff --git a/soridam-auth/build.gradle b/soridam-auth/build.gradle index ea85f4e..00e5776 100644 --- a/soridam-auth/build.gradle +++ b/soridam-auth/build.gradle @@ -15,6 +15,7 @@ dependencies { // 도메인 및 공통 모듈 implementation project(':soridam-common') implementation project(':soridam-domain') + implementation project(':soridam-global-util') implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' @@ -22,4 +23,6 @@ dependencies { //swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1' + + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } diff --git a/soridam-auth/src/main/java/sorisoop/soridam/auth/config/SecurityConfig.java b/soridam-auth/src/main/java/sorisoop/soridam/auth/config/SecurityConfig.java index ea9b509..534cf8e 100644 --- a/soridam-auth/src/main/java/sorisoop/soridam/auth/config/SecurityConfig.java +++ b/soridam-auth/src/main/java/sorisoop/soridam/auth/config/SecurityConfig.java @@ -23,7 +23,7 @@ import sorisoop.soridam.auth.common.CustomAccessDeniedHandler; import sorisoop.soridam.auth.jwt.JwtAuthenticationEntryPoint; import sorisoop.soridam.auth.jwt.JwtAuthenticationFilter; -import sorisoop.soridam.auth.jwt.JwtProvider; +import sorisoop.soridam.auth.jwt.application.JwtProvider; @Configuration @EnableWebSecurity @@ -51,7 +51,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(STATIC_RESOURCES_PATTERNS).permitAll() .requestMatchers(PERMIT_ALL_PATTERNS).permitAll() .requestMatchers(PUBLIC_ENDPOINTS).permitAll() - .requestMatchers(OAUTH2_PATTERNS).permitAll() .anyRequest().authenticated() ) .exceptionHandling(exceptions -> exceptions @@ -84,12 +83,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/api/auth/**", }; - private static final String[] OAUTH2_PATTERNS = { - "/oauth2/**", // Spring Security OAuth2 기본 경로 - "/login/oauth2/**", // 로그인 리다이렉트 처리 경로 - "/oauth2/authorization/**" // 인증 요청 트리거 경로 - }; - CorsConfigurationSource corsConfigurationSource() { return request -> { diff --git a/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/JwtAuthenticationFilter.java b/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/JwtAuthenticationFilter.java index 02b853e..024fcfe 100644 --- a/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/JwtAuthenticationFilter.java +++ b/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/JwtAuthenticationFilter.java @@ -11,6 +11,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import sorisoop.soridam.auth.jwt.application.JwtProvider; import sorisoop.soridam.common.exception.CustomException; import sorisoop.soridam.common.exception.ExceptionResponse; diff --git a/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/JwtProvider.java b/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/application/JwtProvider.java similarity index 82% rename from soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/JwtProvider.java rename to soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/application/JwtProvider.java index 3a6822b..e58ec8b 100644 --- a/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/JwtProvider.java +++ b/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/application/JwtProvider.java @@ -1,4 +1,4 @@ -package sorisoop.soridam.auth.jwt; +package sorisoop.soridam.auth.jwt.application; import java.time.Duration; import java.util.Collections; @@ -19,12 +19,14 @@ import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; import lombok.RequiredArgsConstructor; +import sorisoop.soridam.auth.jwt.JwtProperties; import sorisoop.soridam.auth.jwt.exception.JwtExpiredException; import sorisoop.soridam.auth.jwt.exception.JwtInvalidException; import sorisoop.soridam.auth.jwt.exception.JwtMalformedException; import sorisoop.soridam.auth.jwt.exception.JwtSignatureInvalidException; import sorisoop.soridam.auth.jwt.exception.JwtUnsupportedException; import sorisoop.soridam.domain.user.domain.Role; +import sorisoop.soridam.domain.user.exception.UnauthorizedException; @Service @RequiredArgsConstructor @@ -33,9 +35,19 @@ public class JwtProvider { private final Header header = Jwts.header().type("JWT").build(); - public String generateToken(String userId, Role role, Duration expiredAt) { + public String generateAccessToken(String userId, Role role, Duration duration) { Date now = new Date(); - return makeToken(new Date(now.getTime() + expiredAt.toMillis()), userId, role); + return makeToken(new Date(now.getTime() + duration.toMillis()), userId, role); + } + + public String generateAccessToken(String userId, Role role) { + Date now = new Date(); + return makeToken(new Date(now.getTime() + Duration.ofMinutes(30).toMillis()), userId, role); + } + + public String generateRefreshToken(String userId, Role role) { + Date now = new Date(); + return makeToken(new Date(now.getTime() + Duration.ofDays(1).toMillis()), userId, role); } private String makeToken(Date expiry, String userId, Role role) { @@ -96,7 +108,7 @@ public Set getRoles(Claims claims) { case "ADMIN" -> Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN")); case "PAID_USER" -> Collections.singleton(new SimpleGrantedAuthority("ROLE_PAID_USER")); case "USER" -> Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")); - default -> Collections.emptySet(); + default -> throw new UnauthorizedException(); }; } diff --git a/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/application/JwtService.java b/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/application/JwtService.java new file mode 100644 index 0000000..277fa51 --- /dev/null +++ b/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/application/JwtService.java @@ -0,0 +1,46 @@ +package sorisoop.soridam.auth.jwt.application; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import sorisoop.soridam.auth.jwt.response.JwtResponse; +import sorisoop.soridam.domain.refresh.RefreshToken; +import sorisoop.soridam.domain.refresh.application.RefreshTokenService; +import sorisoop.soridam.domain.user.application.UserCommandService; +import sorisoop.soridam.domain.user.application.UserQueryService; +import sorisoop.soridam.domain.user.domain.User; + +@Service +@RequiredArgsConstructor +public class JwtService { + private final UserQueryService userQueryService; + private final UserCommandService userCommandService; + private final RefreshTokenService refreshTokenService; + private final JwtProvider jwtProvider; + + public JwtResponse jwtLogin(String email, String password) { + User user = userCommandService.login(email, password); + JwtResponse response = getToken(user); + refreshTokenService.save(user.getId(), response.refreshToken()); + return response; + } + + public JwtResponse reissue(String token) { + RefreshToken refreshToken = refreshTokenService.getToken(token); + String userId = refreshToken.getUserId(); + User user = userQueryService.getById(userId); + JwtResponse response = getToken(user); + + refreshTokenService.save(userId, response.refreshToken()); + + return response; + } + + public JwtResponse getToken(User user) { + String refreshToken = jwtProvider.generateRefreshToken(user.extractUuid(), user.getRole()); + String accessToken = jwtProvider.generateAccessToken(user.extractUuid(), user.getRole()); + + return JwtResponse.of(accessToken, refreshToken); + } + +} diff --git a/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/response/jwt/JwtResponse.java b/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/response/JwtResponse.java similarity index 82% rename from soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/response/jwt/JwtResponse.java rename to soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/response/JwtResponse.java index c82a060..81d69a3 100644 --- a/soridam-api/src/main/java/sorisoop/soridam/api/auth/presentation/response/jwt/JwtResponse.java +++ b/soridam-auth/src/main/java/sorisoop/soridam/auth/jwt/response/JwtResponse.java @@ -1,4 +1,4 @@ -package sorisoop.soridam.api.auth.presentation.response.jwt; +package sorisoop.soridam.auth.jwt.response; import lombok.Builder; diff --git a/soridam-auth/src/main/java/sorisoop/soridam/auth/oauth/OidcService.java b/soridam-auth/src/main/java/sorisoop/soridam/auth/oauth/OidcService.java index 5ecafc8..491174a 100644 --- a/soridam-auth/src/main/java/sorisoop/soridam/auth/oauth/OidcService.java +++ b/soridam-auth/src/main/java/sorisoop/soridam/auth/oauth/OidcService.java @@ -13,7 +13,6 @@ import sorisoop.soridam.auth.oauth.exception.OidcExpiredException; import sorisoop.soridam.auth.oauth.exception.OidcInvalidAudienceException; import sorisoop.soridam.auth.oauth.exception.OidcInvalidIssuerException; -import sorisoop.soridam.auth.oauth.request.OidcLoginRequest; import sorisoop.soridam.common.domain.Provider; import sorisoop.soridam.domain.user.domain.User; import sorisoop.soridam.domain.user.domain.UserRepository; @@ -44,9 +43,9 @@ private OidcIdToken validateAndDecodeIdToken(String idToken) { return oidcIdToken; } - public User processLogin(OidcLoginRequest request) { - OidcIdToken idToken = validateAndDecodeIdToken(request.idToken()); - return findOrCreateUser(idToken); + public User processLogin(String idToken) { + OidcIdToken oidcIdToken = validateAndDecodeIdToken(idToken); + return findOrCreateUser(oidcIdToken); } private void validateIssuer(String issuer) { diff --git a/soridam-domain/build.gradle b/soridam-domain/build.gradle index 5cf9743..cf47fdf 100644 --- a/soridam-domain/build.gradle +++ b/soridam-domain/build.gradle @@ -28,4 +28,5 @@ dependencies { // 데이터베이스와 상호작용 (JPA 사용) implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/noise/application/NoiseCommandService.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/noise/application/NoiseCommandService.java index a6f229e..e9f788c 100644 --- a/soridam-domain/src/main/java/sorisoop/soridam/domain/noise/application/NoiseCommandService.java +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/noise/application/NoiseCommandService.java @@ -1,5 +1,7 @@ package sorisoop.soridam.domain.noise.application; +import static sorisoop.soridam.globalutil.uuid.UuidPrefix.NOISE; + import org.locationtech.jts.geom.Point; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,7 +30,7 @@ public Noise createNoise(User user, double x, double y, int maxDecibel, int avgD } public void deleteNoise(User user, String id) { - Noise noise = noiseRepository.findById(id) + Noise noise = noiseRepository.findById(NOISE.getPrefix() + id) .orElseThrow(NoiseNotFoundException::new); validateUser(user.getId(), noise.getUser().getId()); diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/RefreshToken.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/RefreshToken.java new file mode 100644 index 0000000..8707a30 --- /dev/null +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/RefreshToken.java @@ -0,0 +1,28 @@ +package sorisoop.soridam.domain.refresh; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24) +@Builder +@AllArgsConstructor +public class RefreshToken { + @Id + private String userId; + + @Indexed + private String refreshToken; + + public static RefreshToken of(String userId, String refreshToken) { + return RefreshToken.builder() + .userId(userId) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/RefreshTokenRepository.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/RefreshTokenRepository.java new file mode 100644 index 0000000..d407392 --- /dev/null +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/RefreshTokenRepository.java @@ -0,0 +1,11 @@ +package sorisoop.soridam.domain.refresh; + +import java.util.Optional; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RefreshTokenRepository extends CrudRepository { + Optional findByRefreshToken(String refreshToken); +} diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/application/RefreshTokenService.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/application/RefreshTokenService.java new file mode 100644 index 0000000..b9d3872 --- /dev/null +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/application/RefreshTokenService.java @@ -0,0 +1,24 @@ +package sorisoop.soridam.domain.refresh.application; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import sorisoop.soridam.domain.refresh.RefreshToken; +import sorisoop.soridam.domain.refresh.RefreshTokenRepository; +import sorisoop.soridam.domain.refresh.exception.RefreshTokenNotFoundException; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + private final RefreshTokenRepository refreshTokenRepository; + + public RefreshToken getToken(String token) { + return refreshTokenRepository.findByRefreshToken(token) + .orElseThrow(RefreshTokenNotFoundException::new); + } + + public void save(String userId, String token) { + refreshTokenRepository.findById(userId) + .ifPresent(refreshTokenRepository::delete); + refreshTokenRepository.save(RefreshToken.of(userId, token)); + }} diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/exception/RefreshTokenExceptionCode.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/exception/RefreshTokenExceptionCode.java new file mode 100644 index 0000000..9f7c6fc --- /dev/null +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/exception/RefreshTokenExceptionCode.java @@ -0,0 +1,24 @@ +package sorisoop.soridam.domain.refresh.exception; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import sorisoop.soridam.common.exception.ExceptionCode; + +@Getter +@AllArgsConstructor +public enum RefreshTokenExceptionCode implements ExceptionCode { + REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "해당 토큰을 찾을 수 없습니다."), + ; + + private final HttpStatus status; + private final String message; + + @Override + public String getCode() { + return this.name(); + } +} diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/exception/RefreshTokenNotFoundException.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/exception/RefreshTokenNotFoundException.java new file mode 100644 index 0000000..5ee7815 --- /dev/null +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/refresh/exception/RefreshTokenNotFoundException.java @@ -0,0 +1,11 @@ +package sorisoop.soridam.domain.refresh.exception; + +import static sorisoop.soridam.domain.refresh.exception.RefreshTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND; + +import sorisoop.soridam.common.exception.CustomException; + +public class RefreshTokenNotFoundException extends CustomException { + public RefreshTokenNotFoundException() { + super(REFRESH_TOKEN_NOT_FOUND); + } +} diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/user/application/UserCommandService.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/user/application/UserCommandService.java index e5c210a..b216e27 100644 --- a/soridam-domain/src/main/java/sorisoop/soridam/domain/user/application/UserCommandService.java +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/user/application/UserCommandService.java @@ -9,14 +9,15 @@ import lombok.RequiredArgsConstructor; import sorisoop.soridam.domain.user.domain.User; import sorisoop.soridam.domain.user.domain.UserRepository; +import sorisoop.soridam.domain.user.exception.UserNotFoundException; @Service @RequiredArgsConstructor +@Transactional public class UserCommandService { private final UserRepository userRepository; private final BCryptPasswordEncoder bCryptPasswordEncoder; - @Transactional public User signUp(String email, String password, String name, String nickname, LocalDate birthDate, String phoneNumber, String profileImageUrl){ User user = User.create( @@ -31,4 +32,13 @@ public User signUp(String email, String password, String name, String nickname, return userRepository.save(user); } + + public User login(String email, String password){ + User user = userRepository.findByEmail(email) + .orElseThrow(UserNotFoundException::new); + user.isPasswordMatching(password, bCryptPasswordEncoder); + user.updateLastLoginTime(); + + return user; + } } diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/user/application/UserQueryService.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/user/application/UserQueryService.java index 6d8e7a6..1c3217e 100644 --- a/soridam-domain/src/main/java/sorisoop/soridam/domain/user/application/UserQueryService.java +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/user/application/UserQueryService.java @@ -28,7 +28,7 @@ public List getUserNoises(String id) { } public User getById(String id) { - return userRepository.findById(USER.getPrefix() + id) + return userRepository.findById(id) .orElseThrow(UserNotFoundException::new); } @@ -41,7 +41,7 @@ public User me() { try{ Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String userId = ((UserDetails)principal).getUsername(); - return getById(userId); + return getById(USER.getPrefix() + userId); } catch (Exception e){ throw new UnauthorizedException(); } diff --git a/soridam-domain/src/main/java/sorisoop/soridam/domain/user/domain/User.java b/soridam-domain/src/main/java/sorisoop/soridam/domain/user/domain/User.java index 2aa359d..fa46f47 100644 --- a/soridam-domain/src/main/java/sorisoop/soridam/domain/user/domain/User.java +++ b/soridam-domain/src/main/java/sorisoop/soridam/domain/user/domain/User.java @@ -2,6 +2,7 @@ import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; import static lombok.AccessLevel.PROTECTED; import static sorisoop.soridam.globalutil.uuid.UuidPrefix.USER; @@ -69,7 +70,7 @@ public class User extends BaseTimeEntity implements UuidExtractable { private LocalDateTime lastLoginAt; - @OneToMany(mappedBy = "user", cascade = ALL, orphanRemoval = true) + @OneToMany(mappedBy = "user", cascade = ALL, orphanRemoval = true, fetch = LAZY) @Builder.Default private List noises = new ArrayList<>(); @@ -90,6 +91,7 @@ public static User create(String email, String password, String name, String nic .phoneNumber(phoneNumber) .profileImageUrl(profileImageUrl) .point(0) + .role(Role.USER) .build(); } diff --git a/soridam-global-util/src/main/java/sorisoop/soridam/globalutil/logging/LoggingUtils.java b/soridam-global-util/src/main/java/sorisoop/soridam/globalutil/logging/LoggingUtils.java index ba183c4..97b2b72 100644 --- a/soridam-global-util/src/main/java/sorisoop/soridam/globalutil/logging/LoggingUtils.java +++ b/soridam-global-util/src/main/java/sorisoop/soridam/globalutil/logging/LoggingUtils.java @@ -25,6 +25,10 @@ public static List getArguments(JoinPoint joinPoint) { } public static String getObjectFields(Object obj) { + if (Objects.isNull(obj)) { + return "null"; + } + StringBuilder result = new StringBuilder(); Class objClass = obj.getClass(); result.append(objClass.getSimpleName()).append(" {"); diff --git a/soridam-infra/build.gradle b/soridam-infra/build.gradle index 8f3ce94..d9a62ff 100644 --- a/soridam-infra/build.gradle +++ b/soridam-infra/build.gradle @@ -20,4 +20,5 @@ dependencies { // 데이터베이스와 상호작용 (JPA 사용) implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } \ No newline at end of file diff --git a/soridam-infra/src/main/java/sorisoop/soridam/infra/config/redis/RedisConfig.java b/soridam-infra/src/main/java/sorisoop/soridam/infra/config/redis/RedisConfig.java new file mode 100644 index 0000000..767adcf --- /dev/null +++ b/soridam-infra/src/main/java/sorisoop/soridam/infra/config/redis/RedisConfig.java @@ -0,0 +1,56 @@ +package sorisoop.soridam.infra.config.redis; + +import java.time.Duration; + +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.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +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.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories(basePackages = "sorisoop.soridam") +public class RedisConfig { + @Value("${spring.data.redis.host}") + String redisHost; + + @Value("${spring.data.redis.port}") + int redisPort; + + @Value("${spring.data.redis.password}") + String redisPassword; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); + configuration.setHostName(redisHost); + configuration.setPort(redisPort); + configuration.setPassword(redisPassword); + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .commandTimeout(Duration.ofSeconds(5)) + .shutdownTimeout(Duration.ofSeconds(2)) + .build(); + + LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration, clientConfig); + factory.afterPropertiesSet(); + return factory; + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + template.setEnableTransactionSupport(true); + template.afterPropertiesSet(); + + return template; + } +}