From c70064e0d03b33300159c85b4a26cbcf9b23bb93 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 16 Feb 2024 19:24:45 +0900 Subject: [PATCH 01/20] =?UTF-8?q?=E2=9A=A1=EF=B8=8F:=20fix=20Swagger,=20Ap?= =?UTF-8?q?iResponseDto=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plu-api/build.gradle.kts | 29 ++++++++++--------- .../plu/api/config/swagger/SwaggerConfig.kt | 16 +++++----- .../notification/NotificationController.kt | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) rename {src => plu-api/src}/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt (65%) diff --git a/plu-api/build.gradle.kts b/plu-api/build.gradle.kts index fc399e4..dc9a94c 100644 --- a/plu-api/build.gradle.kts +++ b/plu-api/build.gradle.kts @@ -1,26 +1,29 @@ plugins { - kotlin("jvm") + kotlin("jvm") } tasks.jar { - enabled = false + enabled = false } dependencies { - implementation(project(":plu-domain")) - implementation(project(":plu-external")) - implementation(project(":plu-common")) + implementation(project(":plu-domain")) + implementation(project(":plu-external")) + implementation(project(":plu-common")) - // web - implementation("org.springframework.boot:spring-boot-starter-web") + // web + implementation("org.springframework.boot:spring-boot-starter-web") - // Redis - implementation("org.springframework.boot:spring-boot-starter-data-redis") - implementation("org.springframework.session:spring-session-data-redis") - implementation(kotlin("stdlib-jdk8")) + // swagger + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0") + + // Redis + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.session:spring-session-data-redis") + implementation(kotlin("stdlib-jdk8")) } repositories { - mavenCentral() + mavenCentral() } kotlin { - jvmToolchain(17) + jvmToolchain(17) } diff --git a/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt similarity index 65% rename from src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt rename to plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt index 3879d4e..edb34cf 100644 --- a/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt @@ -19,19 +19,19 @@ class SwaggerConfig { @Bean fun openAPI(): OpenAPI { val info = Info() - .title(TITLE) - .description(DESCRIPTION) - .version(VERSION) + .title(TITLE) + .description(DESCRIPTION) + .version(VERSION) val securityScheme = SecurityScheme() - .type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT") - .`in`(SecurityScheme.In.HEADER).name("Authorization") + .type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT") + .`in`(SecurityScheme.In.HEADER).name("Authorization") val securityRequirement = SecurityRequirement().addList("Bearer Token") return OpenAPI() - .components(Components().addSecuritySchemes("Bearer Token", securityScheme)) - .security(listOf(securityRequirement)) - .info(info) + .components(Components().addSecuritySchemes("Bearer Token", securityScheme)) + .security(listOf(securityRequirement)) + .info(info) } } \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/notification/NotificationController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/notification/NotificationController.kt index e3d8e6e..bd2dd49 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/controller/notification/NotificationController.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/notification/NotificationController.kt @@ -1,8 +1,8 @@ package com.th.plu.api.controller.notification import com.th.plu.api.controller.notification.dto.request.MessageSendRequest -import com.th.plu.api.dto.ApiResponse import com.th.plu.api.service.notification.NotificationService +import com.th.plu.common.dto.response.ApiResponse import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping From 5a7144969074d84c8eb0949cbec3090c33961680 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 16 Feb 2024 20:01:47 +0900 Subject: [PATCH 02/20] =?UTF-8?q?=E2=9A=A1=EF=B8=8F:=20fix=20PluApiApplica?= =?UTF-8?q?tion=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=88=98=EC=A0=95=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/com/th/plu/{ => api}/PluApiApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename plu-api/src/main/kotlin/com/th/plu/{ => api}/PluApiApplication.kt (95%) diff --git a/plu-api/src/main/kotlin/com/th/plu/PluApiApplication.kt b/plu-api/src/main/kotlin/com/th/plu/api/PluApiApplication.kt similarity index 95% rename from plu-api/src/main/kotlin/com/th/plu/PluApiApplication.kt rename to plu-api/src/main/kotlin/com/th/plu/api/PluApiApplication.kt index 20f4274..4ed30ce 100644 --- a/plu-api/src/main/kotlin/com/th/plu/PluApiApplication.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/PluApiApplication.kt @@ -1,4 +1,4 @@ -package com.th.plu +package com.th.plu.api import com.th.plu.common.PluCommonRoot import com.th.plu.domain.PluDomainRoot From ac90cd27ce5f5e7bcf8f787bc81e7ba9b571684e Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 16 Feb 2024 20:31:51 +0900 Subject: [PATCH 03/20] =?UTF-8?q?=E2=9A=A1=EF=B8=8F:=20fix=20Jpa,=20Queryd?= =?UTF-8?q?sl=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt | 8 ++++++-- .../com/th/plu/domain/config/querydsl/QueryDslConfig.kt | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt index 6bb639b..1bf7984 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/config/jpa/JpaConfig.kt @@ -1,9 +1,13 @@ package com.th.plu.domain.config.jpa +import com.th.plu.domain.PluDomainRoot +import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.context.annotation.Configuration import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.data.jpa.repository.config.EnableJpaRepositories @Configuration +@EntityScan(basePackageClasses = [PluDomainRoot::class]) +@EnableJpaRepositories(basePackageClasses = [PluDomainRoot::class]) @EnableJpaAuditing -class JpaConfig { -} +class JpaConfig diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt index abb3e22..d3cfc3d 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt @@ -7,11 +7,11 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration -class QueryDslConfig( +class QueryDslConfig { @PersistenceContext - private val entityManager: EntityManager -) { + private lateinit var entityManager: EntityManager + @Bean fun queryFactory(): JPAQueryFactory { return JPAQueryFactory(entityManager) From 0e9002afe859a7fbaf47b83b77666c6c7885b958 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 16 Feb 2024 20:37:35 +0900 Subject: [PATCH 04/20] =?UTF-8?q?=F0=9F=94=A5:=20remove=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20ControllerAdvice=20=EC=A0=9C=EA=B1=B0=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plu/api/controller/ExceptionController.kt | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 plu-api/src/main/kotlin/com/th/plu/api/controller/ExceptionController.kt diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/ExceptionController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/ExceptionController.kt deleted file mode 100644 index ca5de9a..0000000 --- a/plu-api/src/main/kotlin/com/th/plu/api/controller/ExceptionController.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.th.plu.api.controller - -import com.fasterxml.jackson.databind.exc.InvalidFormatException -import com.th.plu.common.dto.response.ApiResponse -import com.th.plu.common.exception.code.ErrorCode -import com.th.plu.common.exception.model.BadGatewayException -import com.th.plu.common.exception.model.NotFoundException -import com.th.plu.common.exception.model.ValidationException -import org.slf4j.LoggerFactory -import org.springframework.http.HttpStatus -import org.springframework.http.converter.HttpMessageNotReadableException -import org.springframework.web.HttpMediaTypeException -import org.springframework.web.HttpRequestMethodNotSupportedException -import org.springframework.web.bind.MethodArgumentNotValidException -import org.springframework.web.bind.ServletRequestBindingException -import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestControllerAdvice -import org.springframework.web.servlet.NoHandlerFoundException -import org.springframework.web.servlet.resource.NoResourceFoundException -import java.net.BindException - -@RestControllerAdvice -class ExceptionController { - private val log = LoggerFactory.getLogger(this.javaClass) - - /** - * 400 Bad Request - */ - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(ValidationException::class) - fun handleValidationException(exception: ValidationException): ApiResponse { - log.error(exception.message) - return ApiResponse.error(exception.errorCode) - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(MethodArgumentNotValidException::class) - fun handleMethodArgumentNotValidException(exception: MethodArgumentNotValidException): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.METHOD_ARGUMENT_NOT_VALID_EXCEPTION) - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(BindException::class) - fun handleBindException(exception: BindException): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.BIND_EXCEPTION) - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler( - value = [ - HttpMessageNotReadableException::class, - InvalidFormatException::class, - ServletRequestBindingException::class - ] - ) - fun handleInvalidFormatException(exception: Exception): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.INVALID_FORMAT_EXCEPTION) - } - - /** - * 404 Not Found - */ - @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(NotFoundException::class) - fun handleNotFoundException(exception: NotFoundException): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(exception.errorCode) - } - - @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler( - value = [ - NoHandlerFoundException::class, - NoResourceFoundException::class] - ) - fun handleNotFoundEndpointException(exception: Exception): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.NOT_FOUND_ENDPOINT_EXCEPTION) - } - - /** - * 405 Method Not Supported - */ - @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) - @ExceptionHandler(HttpRequestMethodNotSupportedException::class) - fun handleHttpRequestMethodNotSupportedException(exception: HttpRequestMethodNotSupportedException): ApiResponse { - return ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED_EXCEPTION) - } - - /** - * 415 Unsupported Media Type - */ - @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) - @ExceptionHandler(HttpMediaTypeException::class) - fun handleHttpMediaTypeException(exception: HttpMediaTypeException): ApiResponse { - return ApiResponse.error(ErrorCode.UNSUPPORTED_MEDIA_TYPE) - } - - /** - * 500 Internal Server Error - */ - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - @ExceptionHandler(Exception::class) - fun handleInternalServerException(exception: Exception): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(ErrorCode.INTERNAL_SERVER_EXCEPTION) - } - - /** - * 502 Bad Gateway - */ - @ResponseStatus(HttpStatus.BAD_GATEWAY) - @ExceptionHandler(BadGatewayException::class) - fun handleBadGatewayException(exception: BadGatewayException): ApiResponse { - log.error(exception.message, exception) - return ApiResponse.error(exception.errorCode) - } -} From 6c5f558bb0766c30c1c7d894200ad624140f5b6a Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 00:27:11 +0900 Subject: [PATCH 05/20] =?UTF-8?q?=E2=9C=A8:=20feat=20JWT=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plu-common/build.gradle.kts | 12 +- .../com/th/plu/common/constant/JwtKey.kt | 5 + .../com/th/plu/common/constant/RedisKey.kt | 5 + .../kotlin/com/th/plu/common/util/JwtUtils.kt | 105 ++++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 plu-common/src/main/kotlin/com/th/plu/common/constant/JwtKey.kt create mode 100644 plu-common/src/main/kotlin/com/th/plu/common/constant/RedisKey.kt create mode 100644 plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt diff --git a/plu-common/build.gradle.kts b/plu-common/build.gradle.kts index d991fb4..72bdcf9 100644 --- a/plu-common/build.gradle.kts +++ b/plu-common/build.gradle.kts @@ -2,11 +2,11 @@ plugins { kotlin("jvm") } tasks.jar { - enabled = true + enabled = true } tasks.bootJar { - enabled = false + enabled = false } @@ -15,6 +15,14 @@ dependencies { // web implementation("org.springframework.boot:spring-boot-starter-web") + + // Redis + implementation("org.springframework.boot:spring-boot-starter-data-redis") + + //jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") } repositories { mavenCentral() diff --git a/plu-common/src/main/kotlin/com/th/plu/common/constant/JwtKey.kt b/plu-common/src/main/kotlin/com/th/plu/common/constant/JwtKey.kt new file mode 100644 index 0000000..364c430 --- /dev/null +++ b/plu-common/src/main/kotlin/com/th/plu/common/constant/JwtKey.kt @@ -0,0 +1,5 @@ +package com.th.plu.common.constant + +object JwtKey { + const val MEMBER_ID = "MEMBER_ID" +} diff --git a/plu-common/src/main/kotlin/com/th/plu/common/constant/RedisKey.kt b/plu-common/src/main/kotlin/com/th/plu/common/constant/RedisKey.kt new file mode 100644 index 0000000..c0bbb9c --- /dev/null +++ b/plu-common/src/main/kotlin/com/th/plu/common/constant/RedisKey.kt @@ -0,0 +1,5 @@ +package com.th.plu.common.constant + +object RedisKey { + const val REFRESH_TOKEN = "RT:" +} diff --git a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt b/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt new file mode 100644 index 0000000..93646a4 --- /dev/null +++ b/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt @@ -0,0 +1,105 @@ +package com.th.plu.common.util + +import com.th.plu.common.constant.JwtKey +import com.th.plu.common.constant.RedisKey +import io.jsonwebtoken.* +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.io.DecodingException +import io.jsonwebtoken.security.Keys +import jakarta.annotation.PostConstruct +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Component +import java.security.Key +import java.util.* +import java.util.concurrent.TimeUnit + +@Component +class JwtUtils( + private val redisTemplate: RedisTemplate +) { + + @Value("\${jwt.secret}") + private var jwtSecret: String? = null + + private var secretKey: Key? = null + private val log = LoggerFactory.getLogger(this.javaClass) + + companion object { +// private const val ACCESS_TOKEN_EXPIRE_TIME = 10 * 60 * 1000L // 10분 +// private const val REFRESH_TOKEN_EXPIRE_TIME = 6 * 30 * 24 * 60 * 60 * 1000L // 180일 + + private const val ACCESS_TOKEN_EXPIRE_TIME = 365 * 24 * 60 * 60 * 1000L; // 1년 + private const val REFRESH_TOKEN_EXPIRE_TIME = 365 * 24 * 60 * 60 * 1000L; // 1년 + private const val EXPIRED_TIME = 1L + } + + @PostConstruct + fun init() { + val keyBytes: ByteArray = Decoders.BASE64.decode(jwtSecret) + this.secretKey = Keys.hmacShaKeyFor(keyBytes) + } + + fun createTokenInfo(memberId: Long): List { + val now = Date().time + val accessTokenExpiresIn = Date(now + ACCESS_TOKEN_EXPIRE_TIME) + val refreshTokenExpiresIn = Date(now + REFRESH_TOKEN_EXPIRE_TIME) + + // Access Token 생성 + val accessToken: String = Jwts.builder() + .claim(JwtKey.MEMBER_ID, memberId) + .setExpiration(accessTokenExpiresIn) + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact() + + // Refresh Token 생성 + val refreshToken: String = Jwts.builder() + .setExpiration(refreshTokenExpiresIn) + .signWith(secretKey, SignatureAlgorithm.HS512) + .compact() + + redisTemplate.opsForValue() + .set(RedisKey.REFRESH_TOKEN + memberId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS) + + return listOf(accessToken, refreshToken) + } + + fun expireRefreshToken(memberId: Long) { + redisTemplate.opsForValue().set(RedisKey.REFRESH_TOKEN + memberId, "", EXPIRED_TIME, TimeUnit.MILLISECONDS) + } + + fun validateToken(token: String?): Boolean { + try { + Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token) + return true + } catch (e: SecurityException) { + log.warn("Invalid JWT Token", e) + } catch (e: MalformedJwtException) { + log.warn("Invalid JWT Token", e) + } catch (e: DecodingException) { + log.warn("Invalid JWT Token", e) + } catch (e: ExpiredJwtException) { + log.warn("Expired JWT Token", e) + } catch (e: UnsupportedJwtException) { + log.warn("Unsupported JWT Token", e) + } catch (e: IllegalArgumentException) { + log.warn("JWT claims string is empty.", e) + } catch (e: Exception) { + log.error("Unhandled JWT exception", e) + } + return false + } + + fun getMemberIdFromJwt(accessToken: String): Long { + return parseClaims(accessToken).get(JwtKey.MEMBER_ID, Long::class.java) + } + + private fun parseClaims(accessToken: String): Claims { + return try { + Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(accessToken).body + } catch (e: ExpiredJwtException) { + e.claims + } + } +} From ec7e20249edbd64fc1c79dcc56ebd4d34f984626 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 00:28:43 +0900 Subject: [PATCH 06/20] =?UTF-8?q?=E2=9C=A8:=20feat=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=83=9D=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/response/TokenResponseDto.kt | 12 ++++++++++++ .../com/th/plu/api/service/auth/TokenService.kt | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/response/TokenResponseDto.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/response/TokenResponseDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/response/TokenResponseDto.kt new file mode 100644 index 0000000..f844702 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/response/TokenResponseDto.kt @@ -0,0 +1,12 @@ +package com.th.plu.api.controller.auth.dto.response + +import io.swagger.v3.oas.annotations.media.Schema + +data class TokenResponseDto( + + @field:Schema(description = "PLU JWT accessToken") + val accessToken: String, + + @field:Schema(description = "PLU JWT refreshToken") + val refreshToken: String +) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt new file mode 100644 index 0000000..a6a8ac5 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt @@ -0,0 +1,17 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.controller.auth.dto.response.TokenResponseDto +import com.th.plu.common.util.JwtUtils +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TokenService( + private val jwtUtils: JwtUtils +) { + @Transactional + fun createTokenInfo(memberId: Long): TokenResponseDto { + val tokens: List = jwtUtils.createTokenInfo(memberId) + return TokenResponseDto(accessToken = tokens[0], refreshToken = tokens[1]) + } +} From 342d8dd95b8a6c2afec2356144bf0c4be4e00f23 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 00:30:05 +0900 Subject: [PATCH 07/20] =?UTF-8?q?=E2=9C=A8:=20feat=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20API=20=EC=97=B0=EB=8F=99=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../external/client/kakao/KakaoApiCaller.kt | 8 +++++ .../client/kakao/WebClientKakaoCaller.kt | 33 +++++++++++++++++++ .../dto/response/KakaoProfileResponseDto.kt | 8 +++++ 3 files changed, 49 insertions(+) create mode 100644 plu-external/src/main/kotlin/com/th/plu/external/client/kakao/KakaoApiCaller.kt create mode 100644 plu-external/src/main/kotlin/com/th/plu/external/client/kakao/WebClientKakaoCaller.kt create mode 100644 plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/KakaoApiCaller.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/KakaoApiCaller.kt new file mode 100644 index 0000000..15b8040 --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/KakaoApiCaller.kt @@ -0,0 +1,8 @@ +package com.th.plu.external.client.kakao + +import com.th.plu.external.client.kakao.dto.response.KakaoProfileResponseDto + +interface KakaoApiCaller { + + fun getProfileInfo(accessToken: String): KakaoProfileResponseDto +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/WebClientKakaoCaller.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/WebClientKakaoCaller.kt new file mode 100644 index 0000000..449822f --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/WebClientKakaoCaller.kt @@ -0,0 +1,33 @@ +package com.th.plu.external.client.kakao + +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.BadGatewayException +import com.th.plu.common.exception.model.ValidationException +import com.th.plu.external.client.kakao.dto.response.KakaoProfileResponseDto +import org.springframework.http.HttpStatusCode +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient + +@Component +class WebClientKakaoCaller( + private val webClient: WebClient +) : KakaoApiCaller { + + override fun getProfileInfo(accessToken: String): KakaoProfileResponseDto { + return webClient.get() + .uri("https://kapi.kakao.com/v2/user/me") + .headers { it.setBearerAuth(accessToken) } + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError) { + throw ValidationException( + ErrorCode.VALIDATION_INVALID_TOKEN_EXCEPTION, + "잘못된 카카오 액세스 토큰 $accessToken 입니다." + ) + } + .onStatus(HttpStatusCode::is5xxServerError) { + throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "카카오 로그인 연동 중 에러가 발생하였습니다.") + } + .bodyToMono(KakaoProfileResponseDto::class.java) + .block()!! + } +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt new file mode 100644 index 0000000..d17e3c4 --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt @@ -0,0 +1,8 @@ +package com.th.plu.external.client.kakao.dto.response + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class KakaoProfileResponseDto( + val id: String +) From aba0d2f36094097e679bd54a698fc44eb3258a50 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 00:32:22 +0900 Subject: [PATCH 08/20] =?UTF-8?q?=E2=9C=A8:=20feat=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 -- plu-api/build.gradle.kts | 4 ++ .../plu/api/controller/auth/AuthController.kt | 34 ++++++++++++++ .../auth/dto/request/LoginRequestDto.kt | 22 +++++++++ .../auth/dto/request/SignupRequestDto.kt | 38 +++++++++++++++ .../dto/request/CreateUserRequestDto.kt | 10 ++++ .../th/plu/api/service/auth/AuthService.kt | 11 +++++ .../api/service/auth/AuthServiceProvider.kt | 25 ++++++++++ .../plu/api/service/auth/KakaoAuthService.kt | 31 +++++++++++++ .../plu/api/service/member/MemberService.kt | 42 +++++++++++++++++ .../api/service/member/MemberServiceUtils.kt | 20 ++++++++ .../src/main/resources/application-dev.yml | 4 +- .../src/main/resources/application-local.yml | 4 +- plu-api/src/main/resources/sql/schema.sql | 2 +- .../aop/advice/ExceptionControllerAdvice.kt | 46 ++++++++++++++++--- .../th/plu/common/dto/response/ApiResponse.kt | 4 ++ .../exception/model/UnauthorizedException.kt | 5 ++ .../th/plu/domain/domain/common/BaseEntity.kt | 7 ++- .../com/th/plu/domain/domain/member/Member.kt | 45 ++++++++++++++---- .../th/plu/domain/domain/member/Onboarding.kt | 19 +++++++- .../th/plu/domain/domain/member/Setting.kt | 20 +++++--- .../th/plu/domain/domain/member/SocialInfo.kt | 23 ---------- .../member/repository/MemberRepositoryImpl.kt | 8 ++-- 23 files changed, 364 insertions(+), 63 deletions(-) create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/LoginRequestDto.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/SignupRequestDto.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/controller/member/dto/request/CreateUserRequestDto.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthService.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberService.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt create mode 100644 plu-common/src/main/kotlin/com/th/plu/common/exception/model/UnauthorizedException.kt delete mode 100644 plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/SocialInfo.kt diff --git a/build.gradle.kts b/build.gradle.kts index b4613f2..3402080 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,7 +41,6 @@ subprojects { implementation("org.springframework.boot:spring-boot-starter-actuator") // tools - implementation("org.springframework.boot:spring-boot-starter-validation") compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") @@ -55,6 +54,4 @@ subprojects { useJUnitPlatform() } } -// -//version = "0.0.1-SNAPSHOT" diff --git a/plu-api/build.gradle.kts b/plu-api/build.gradle.kts index dc9a94c..0867965 100644 --- a/plu-api/build.gradle.kts +++ b/plu-api/build.gradle.kts @@ -13,6 +13,10 @@ dependencies { // web implementation("org.springframework.boot:spring-boot-starter-web") + // validation + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + // swagger implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0") diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt new file mode 100644 index 0000000..866f8e9 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt @@ -0,0 +1,34 @@ +package com.th.plu.api.controller.auth + +import com.th.plu.api.controller.auth.dto.request.SignupRequestDto +import com.th.plu.api.controller.auth.dto.response.TokenResponseDto +import com.th.plu.api.service.auth.AuthServiceProvider +import com.th.plu.api.service.auth.TokenService +import com.th.plu.api.service.member.MemberService +import com.th.plu.common.dto.response.ApiResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "Auth") +@RestController +@RequestMapping("/api") +class AuthController( + private val authServiceProvider: AuthServiceProvider, + private val memberService: MemberService, + private val tokenService: TokenService +) { + + @Operation(summary = "카카오 소셜 회원가입") + @PostMapping("/v1/auth/signup") + fun signup(@Valid @RequestBody request: SignupRequestDto): ApiResponse { + val authService = authServiceProvider.getAuthService(request.socialType) + val memberId = authService.signup(request) + val tokenInfo = tokenService.createTokenInfo(memberId) + return ApiResponse.success(tokenInfo) + } +} \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/LoginRequestDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/LoginRequestDto.kt new file mode 100644 index 0000000..0e69aa8 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/LoginRequestDto.kt @@ -0,0 +1,22 @@ +package com.th.plu.api.controller.auth.dto.request + +import com.th.plu.domain.domain.member.MemberSocialType +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull + + +data class LoginRequestDto( + + @field:Schema(description = "소셜 로그인 타입", example = "KAKAO") + @field:NotNull(message = "socialType 을 입력해주세요.") + val socialType: MemberSocialType, + + @field:Schema(description = "소셜 토큰", example = "eyJhbGciOiJIUzUxdfadfadsMiJ9.udnKnDSK08EuX56E5k-") + @field:NotBlank(message = "token 을 입력해주세요.") + val token: String, + + @field:Schema(description = "FCM 토큰", example = "adfaffaffdfsfewvasdvasvdsvffsddauaiviajvasvavisavja") + @field:NotBlank(message = "fcmToken 을 입력해주세요.") + val fcmToken: String +) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/SignupRequestDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/SignupRequestDto.kt new file mode 100644 index 0000000..c344edf --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/SignupRequestDto.kt @@ -0,0 +1,38 @@ +package com.th.plu.api.controller.auth.dto.request + +import com.th.plu.api.controller.member.dto.request.CreateUserRequestDto +import com.th.plu.domain.domain.member.MemberSocialType +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size + + +data class SignupRequestDto( + + @field:Schema(description = "소셜 로그인 타입", example = "KAKAO") + @field:NotNull(message = "socialType 을 입력해주세요.") + val socialType: MemberSocialType, + + @field:Schema(description = "소셜 토큰", example = "eyJhbGciOiJIUzUxdfadfadsMiJ9.udnKnDSK08EuX56E5k-") + @field:NotBlank(message = "token 을 입력해주세요.") + val token: String, + + @field:Schema(description = "FCM 토큰", example = "adfaffaffdfsfewvasdvasvdsvffsddauaiviajvasvavisavja") + @field:NotBlank(message = "fcmToken 을 입력해주세요.") + val fcmToken: String, + + @field:Schema(description = "닉네임", example = "둘리") + @field:NotBlank(message = "nickname 을 입력해주세요.") + @field:Size(max = 8, message = "nickname 은 8자 이내로 입력해주세요.") + val nickname: String +) { + fun toCreateUserDto(socialId: String): CreateUserRequestDto { + return CreateUserRequestDto( + socialId = socialId, + socialType = socialType, + fcmToken = fcmToken, + nickname = nickname + ) + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/member/dto/request/CreateUserRequestDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/member/dto/request/CreateUserRequestDto.kt new file mode 100644 index 0000000..b08f08a --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/member/dto/request/CreateUserRequestDto.kt @@ -0,0 +1,10 @@ +package com.th.plu.api.controller.member.dto.request + +import com.th.plu.domain.domain.member.MemberSocialType + +data class CreateUserRequestDto( + val socialId: String, + val socialType: MemberSocialType, + val fcmToken: String, + val nickname: String +) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthService.kt new file mode 100644 index 0000000..c600534 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthService.kt @@ -0,0 +1,11 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.controller.auth.dto.request.LoginRequestDto +import com.th.plu.api.controller.auth.dto.request.SignupRequestDto + +interface AuthService { + + fun signup(request: SignupRequestDto): Long + fun login(request: LoginRequestDto): Long + +} \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt new file mode 100644 index 0000000..bd23590 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt @@ -0,0 +1,25 @@ +package com.th.plu.api.service.auth + +import com.th.plu.domain.domain.member.MemberSocialType +import jakarta.annotation.PostConstruct +import org.springframework.stereotype.Component + +@Component +class AuthServiceProvider( +// private val appleAuthService: AppleAuthService, + private val kakaoAuthService: KakaoAuthService +) { + companion object { + val authServiceMap = mutableMapOf() + } + + @PostConstruct + fun initAuthServiceMap() { + authServiceMap[MemberSocialType.KAKAO] = kakaoAuthService +// authServiceMap[MemberSocialType.APPLE] = appleAuthService + } + + fun getAuthService(socialType: MemberSocialType): AuthService { + return authServiceMap[socialType]!! + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt new file mode 100644 index 0000000..ce07a55 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt @@ -0,0 +1,31 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.controller.auth.dto.request.LoginRequestDto +import com.th.plu.api.controller.auth.dto.request.SignupRequestDto +import com.th.plu.api.service.member.MemberService +import com.th.plu.domain.domain.member.MemberSocialType +import com.th.plu.external.client.kakao.KakaoApiCaller +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class KakaoAuthService( + private val kakaoApiCaller: KakaoApiCaller, + private val memberService: MemberService, +) : AuthService { + + companion object { + private val socialType: MemberSocialType = MemberSocialType.KAKAO + } + + @Transactional + override fun signup(request: SignupRequestDto): Long { + val response = kakaoApiCaller.getProfileInfo(request.token) + return memberService.registerUser(request.toCreateUserDto(response.id)) + } + + override fun login(request: LoginRequestDto): Long { + TODO("Not yet implemented") + } + +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberService.kt new file mode 100644 index 0000000..cb812a0 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberService.kt @@ -0,0 +1,42 @@ +package com.th.plu.api.service.member + +import com.th.plu.api.controller.member.dto.request.CreateUserRequestDto +import com.th.plu.domain.domain.member.Member +import com.th.plu.domain.domain.member.Onboarding +import com.th.plu.domain.domain.member.Setting +import com.th.plu.domain.domain.member.repository.MemberRepository +import com.th.plu.domain.domain.member.repository.OnboardingRepository +import com.th.plu.domain.domain.member.repository.SettingRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class MemberService( + private val memberRepository: MemberRepository, + private val onboardingRepository: OnboardingRepository, + private val settingRepository: SettingRepository +) { + + @Transactional + fun registerUser(request: CreateUserRequestDto): Long { + MemberServiceUtils.validateNotExistsMember(memberRepository, request.socialId, request.socialType) + // TODO: 닉네임 중복 체크 추가해야합니다. + val member = memberRepository.save( + Member.newInstance( + socialId = request.socialId, + socialType = request.socialType, + fcmToken = request.fcmToken, + setting = settingRepository.save(Setting.newInstance()) + ) + ) + val onboarding = onboardingRepository.save( + Onboarding.newInstance( + member = member, + nickname = request.nickname + ) + ) + member.initOnboarding(onboarding) + return member.id!! + } + +} \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt new file mode 100644 index 0000000..8dd409f --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt @@ -0,0 +1,20 @@ +package com.th.plu.api.service.member + +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.ConflictException +import com.th.plu.domain.domain.member.MemberSocialType +import com.th.plu.domain.domain.member.repository.MemberRepository + +class MemberServiceUtils { + companion object { + fun validateNotExistsMember( + memberRepository: MemberRepository, + socialId: String, + socialType: MemberSocialType + ) { + if (memberRepository.existBySocialIdAndSocialType(socialId, socialType)) { + throw ConflictException(ErrorCode.CONFLICT_MEMBER_EXCEPTION, "이미 존재하는 유저 $socialId - $socialType 입니다") + } + } + } +} \ No newline at end of file diff --git a/plu-api/src/main/resources/application-dev.yml b/plu-api/src/main/resources/application-dev.yml index 38bbdbd..2c91d1d 100644 --- a/plu-api/src/main/resources/application-dev.yml +++ b/plu-api/src/main/resources/application-dev.yml @@ -49,5 +49,5 @@ spring: config: path: ${FIREBASE_PATH} -#jwt: -# secret: ${JWT_SECRET_DEV} +jwt: + secret: ${JWT_SECRET_DEV} diff --git a/plu-api/src/main/resources/application-local.yml b/plu-api/src/main/resources/application-local.yml index 376a74c..577ae0c 100644 --- a/plu-api/src/main/resources/application-local.yml +++ b/plu-api/src/main/resources/application-local.yml @@ -48,5 +48,5 @@ spring: config: path: ${FIREBASE_PATH} -#jwt: -# secret: secretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKey +jwt: + secret: secretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKeysecretKey diff --git a/plu-api/src/main/resources/sql/schema.sql b/plu-api/src/main/resources/sql/schema.sql index c747aea..bc80224 100644 --- a/plu-api/src/main/resources/sql/schema.sql +++ b/plu-api/src/main/resources/sql/schema.sql @@ -12,6 +12,7 @@ CREATE TABLE `members` `social_type` varchar(30) NOT NULL, `member_role` varchar(30) NOT NULL, `fcm_token` varchar(300) NULL, + `setting_id` bigint NOT NULL, `created_at` datetime NOT NULL, `modified_at` datetime NOT NULL ); @@ -19,7 +20,6 @@ CREATE TABLE `members` CREATE TABLE `settings` ( `setting_id` bigint auto_increment primary key, - `member_id` bigint NOT NULL, `notification_status` boolean NOT NULL, `created_at` datetime NOT NULL, `modified_at` datetime NOT NULL diff --git a/plu-common/src/main/kotlin/com/th/plu/common/aop/advice/ExceptionControllerAdvice.kt b/plu-common/src/main/kotlin/com/th/plu/common/aop/advice/ExceptionControllerAdvice.kt index 3effb00..9746400 100644 --- a/plu-common/src/main/kotlin/com/th/plu/common/aop/advice/ExceptionControllerAdvice.kt +++ b/plu-common/src/main/kotlin/com/th/plu/common/aop/advice/ExceptionControllerAdvice.kt @@ -3,12 +3,11 @@ package com.th.plu.common.aop.advice import com.fasterxml.jackson.databind.exc.InvalidFormatException import com.th.plu.common.dto.response.ApiResponse import com.th.plu.common.exception.code.ErrorCode -import com.th.plu.common.exception.model.BadGatewayException -import com.th.plu.common.exception.model.NotFoundException -import com.th.plu.common.exception.model.ValidationException +import com.th.plu.common.exception.model.* import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.validation.BindException import org.springframework.web.HttpMediaTypeException import org.springframework.web.HttpRequestMethodNotSupportedException import org.springframework.web.bind.MethodArgumentNotValidException @@ -18,7 +17,6 @@ import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.servlet.NoHandlerFoundException import org.springframework.web.servlet.resource.NoResourceFoundException -import java.net.BindException @RestControllerAdvice class ExceptionControllerAdvice { @@ -38,14 +36,20 @@ class ExceptionControllerAdvice { @ExceptionHandler(MethodArgumentNotValidException::class) fun handleMethodArgumentNotValidException(exception: MethodArgumentNotValidException): ApiResponse { log.error(exception.message, exception); - return ApiResponse.error(ErrorCode.METHOD_ARGUMENT_NOT_VALID_EXCEPTION); + return ApiResponse.error( + ErrorCode.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, + exception.bindingResult.fieldError?.defaultMessage.toString() + ) } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException::class) fun handleBindException(exception: BindException): ApiResponse { log.error(exception.message, exception); - return ApiResponse.error(ErrorCode.BIND_EXCEPTION); + return ApiResponse.error( + ErrorCode.BIND_EXCEPTION, + exception.bindingResult.fieldError?.defaultMessage.toString() + ) } @ResponseStatus(HttpStatus.BAD_REQUEST) @@ -61,6 +65,26 @@ class ExceptionControllerAdvice { return ApiResponse.error(ErrorCode.INVALID_FORMAT_EXCEPTION); } + /** + * 401 Unauthorized + */ + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(UnauthorizedException::class) + fun handleUnauthorizedException(exception: UnauthorizedException): ApiResponse { + log.error(exception.message, exception) + return ApiResponse.error(exception.errorCode) + } + + /** + * 403 Forbidden + */ + @ResponseStatus(HttpStatus.FORBIDDEN) + @ExceptionHandler(ForbiddenException::class) + fun handleForbiddenException(exception: ForbiddenException): ApiResponse { + log.error(exception.message, exception) + return ApiResponse.error(exception.errorCode) + } + /** * 404 Not Found */ @@ -91,6 +115,16 @@ class ExceptionControllerAdvice { return ApiResponse.error(ErrorCode.METHOD_NOT_ALLOWED_EXCEPTION) } + /** + * 409 Conflict + */ + @ResponseStatus(HttpStatus.CONFLICT) + @ExceptionHandler(ConflictException::class) + fun handleConflictException(exception: ConflictException): ApiResponse { + log.error(exception.message, exception) + return ApiResponse.error(exception.errorCode) + } + /** * 415 Unsupported Media Type */ diff --git a/plu-common/src/main/kotlin/com/th/plu/common/dto/response/ApiResponse.kt b/plu-common/src/main/kotlin/com/th/plu/common/dto/response/ApiResponse.kt index 57060f2..0fa671e 100644 --- a/plu-common/src/main/kotlin/com/th/plu/common/dto/response/ApiResponse.kt +++ b/plu-common/src/main/kotlin/com/th/plu/common/dto/response/ApiResponse.kt @@ -17,5 +17,9 @@ data class ApiResponse(val code: String, val message: String, var data: T?) { fun error(errorCode: ErrorCode): ApiResponse { return ApiResponse(errorCode.code, errorCode.message, null) } + + fun error(errorCode: ErrorCode, message: String): ApiResponse { + return ApiResponse(errorCode.code, message, null) + } } } diff --git a/plu-common/src/main/kotlin/com/th/plu/common/exception/model/UnauthorizedException.kt b/plu-common/src/main/kotlin/com/th/plu/common/exception/model/UnauthorizedException.kt new file mode 100644 index 0000000..5a8801e --- /dev/null +++ b/plu-common/src/main/kotlin/com/th/plu/common/exception/model/UnauthorizedException.kt @@ -0,0 +1,5 @@ +package com.th.plu.common.exception.model + +import com.th.plu.common.exception.code.ErrorCode + +class UnauthorizedException(errorCode: ErrorCode, message: String) : PluException(errorCode, message) diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/common/BaseEntity.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/common/BaseEntity.kt index 8368a85..d3667fb 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/common/BaseEntity.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/common/BaseEntity.kt @@ -16,11 +16,10 @@ open class BaseEntity( @CreatedDate @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") @Column(name = "created_at", nullable = false) - val createdAt: LocalDateTime = LocalDateTime.now(), + var createdAt: LocalDateTime = LocalDateTime.now(), @LastModifiedDate @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") @Column(name = "modified_at", nullable = false) - val modifiedAt: LocalDateTime = LocalDateTime.now(), -) { -} + var modifiedAt: LocalDateTime = LocalDateTime.now(), +) diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt index f75174d..e08e480 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt @@ -8,12 +8,17 @@ import jakarta.persistence.* @Entity class Member( - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_id") - val id: Long? = null, + var id: Long? = null, - @Embedded - var socialInfo: SocialInfo, + @Column(name = "social_id", nullable = false, length = 300) + var socialId: String, + + @Column(name = "social_type", nullable = false, length = 30) + @Enumerated(EnumType.STRING) + var socialType: MemberSocialType, @Column(name = "member_role", nullable = false, length = 30) @Enumerated(EnumType.STRING) @@ -22,14 +27,36 @@ class Member( @Column(name = "fcm_token", nullable = false, length = 300) var fcmToken: String, - @OneToOne(mappedBy = "member", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) + @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = [CascadeType.ALL]) + @JoinColumn(name = "setting_id", nullable = false) var setting: Setting, @OneToOne(mappedBy = "member", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) - var onboarding: Onboarding, + var onboarding: Onboarding?, @OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) - var answers: List, - - ) : BaseEntity() { + var answers: List = mutableListOf() + +) : BaseEntity() { + + companion object { + fun newInstance( + socialId: String, socialType: MemberSocialType, fcmToken: String, + setting: Setting + ): Member { + return Member( + id = null, + socialId = socialId, + socialType = socialType, + role = MemberRole.MEMBER, + fcmToken = fcmToken, + setting = setting, + onboarding = null + ) + } + } + + fun initOnboarding(onboarding: Onboarding) { + this.onboarding = onboarding + } } diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Onboarding.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Onboarding.kt index 8d3665f..5c157d2 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Onboarding.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Onboarding.kt @@ -7,12 +7,27 @@ import jakarta.persistence.* @Entity class Onboarding( - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "onboarding_id") - val id: Long? = null, + var id: Long? = null, @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = [CascadeType.ALL]) @JoinColumn(name = "member_id", nullable = false) var member: Member, + + @Column(name = "nickname", nullable = false, length = 30) + var nickname: String, ) : BaseEntity() { + + companion object { + + fun newInstance(member: Member, nickname: String): Onboarding { + return Onboarding( + id = null, + member = member, + nickname = nickname + ) + } + } } diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Setting.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Setting.kt index 04b970a..834c661 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Setting.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Setting.kt @@ -7,16 +7,22 @@ import jakarta.persistence.* @Entity class Setting( - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "setting_id") - val id: Long? = null, + var id: Long? = null, @Column(name = "notification_status", nullable = false) - var notificationStatus: Boolean, - - @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = [CascadeType.ALL]) - @JoinColumn(name = "member_id", nullable = false) - var member: Member + var notificationStatus: Boolean ) : BaseEntity() { + + companion object { + fun newInstance(): Setting { + return Setting( + id = null, + notificationStatus = false + ) + } + } } diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/SocialInfo.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/SocialInfo.kt deleted file mode 100644 index e9801ec..0000000 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/SocialInfo.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.th.plu.domain.domain.member - -import jakarta.persistence.* -import lombok.AccessLevel -import lombok.AllArgsConstructor -import lombok.EqualsAndHashCode -import lombok.Getter -import lombok.NoArgsConstructor - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@EqualsAndHashCode -@Embeddable -class SocialInfo( - - @Column(name = "social_id", nullable = false, length = 300) - val socialId: String, - - @Column(name = "social_type", nullable = false, length = 30) - @Enumerated(EnumType.STRING) - val socialType : MemberSocialType -) diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/repository/MemberRepositoryImpl.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/repository/MemberRepositoryImpl.kt index b369995..5c20ea1 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/repository/MemberRepositoryImpl.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/repository/MemberRepositoryImpl.kt @@ -19,8 +19,8 @@ class MemberRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberRe return queryFactory .selectFrom(member) .where( - member.socialInfo.socialId.eq(socialId), - member.socialInfo.socialType.eq(socialType) + member.socialId.eq(socialId), + member.socialType.eq(socialType) ).fetchOne() } @@ -28,8 +28,8 @@ class MemberRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberRe return queryFactory .selectFrom(member) .where( - member.socialInfo.socialId.eq(socialId), - member.socialInfo.socialType.eq(socialType) + member.socialId.eq(socialId), + member.socialType.eq(socialType) ).fetchOne() != null } } From 65e6286555f5572f470dc569db6e0a5d97fee099 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 01:31:27 +0900 Subject: [PATCH 09/20] =?UTF-8?q?=E2=9C=A8:=20feat=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/th/plu/api/controller/auth/AuthController.kt | 12 +++++++++++- .../com/th/plu/api/service/auth/KakaoAuthService.kt | 11 +++++++++-- .../th/plu/api/service/member/MemberServiceUtils.kt | 11 +++++++++++ .../kotlin/com/th/plu/domain/domain/member/Member.kt | 4 ++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt index 866f8e9..fca360e 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt @@ -1,5 +1,6 @@ package com.th.plu.api.controller.auth +import com.th.plu.api.controller.auth.dto.request.LoginRequestDto import com.th.plu.api.controller.auth.dto.request.SignupRequestDto import com.th.plu.api.controller.auth.dto.response.TokenResponseDto import com.th.plu.api.service.auth.AuthServiceProvider @@ -23,7 +24,7 @@ class AuthController( private val tokenService: TokenService ) { - @Operation(summary = "카카오 소셜 회원가입") + @Operation(summary = "소셜 회원가입") @PostMapping("/v1/auth/signup") fun signup(@Valid @RequestBody request: SignupRequestDto): ApiResponse { val authService = authServiceProvider.getAuthService(request.socialType) @@ -31,4 +32,13 @@ class AuthController( val tokenInfo = tokenService.createTokenInfo(memberId) return ApiResponse.success(tokenInfo) } + + @Operation(summary = "소셜 로그인") + @PostMapping("/v1/auth/login") + fun login(@Valid @RequestBody request: LoginRequestDto): ApiResponse { + val authService = authServiceProvider.getAuthService(request.socialType) + val memberId = authService.login(request) + val tokenInfo = tokenService.createTokenInfo(memberId) + return ApiResponse.success(tokenInfo) + } } \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt index ce07a55..09f430b 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/KakaoAuthService.kt @@ -3,7 +3,9 @@ package com.th.plu.api.service.auth import com.th.plu.api.controller.auth.dto.request.LoginRequestDto import com.th.plu.api.controller.auth.dto.request.SignupRequestDto import com.th.plu.api.service.member.MemberService +import com.th.plu.api.service.member.MemberServiceUtils import com.th.plu.domain.domain.member.MemberSocialType +import com.th.plu.domain.domain.member.repository.MemberRepository import com.th.plu.external.client.kakao.KakaoApiCaller import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -12,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional class KakaoAuthService( private val kakaoApiCaller: KakaoApiCaller, private val memberService: MemberService, + private val memberRepository: MemberRepository ) : AuthService { companion object { @@ -24,8 +27,12 @@ class KakaoAuthService( return memberService.registerUser(request.toCreateUserDto(response.id)) } + @Transactional override fun login(request: LoginRequestDto): Long { - TODO("Not yet implemented") + val response = kakaoApiCaller.getProfileInfo(request.token) + val member = MemberServiceUtils.findMemberBySocialIdAndSocialType(memberRepository, response.id, socialType) + member.updateFcmToken(request.fcmToken) + return member.id!! } - + } diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt index 8dd409f..00b32f2 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt @@ -2,6 +2,8 @@ package com.th.plu.api.service.member import com.th.plu.common.exception.code.ErrorCode import com.th.plu.common.exception.model.ConflictException +import com.th.plu.common.exception.model.NotFoundException +import com.th.plu.domain.domain.member.Member import com.th.plu.domain.domain.member.MemberSocialType import com.th.plu.domain.domain.member.repository.MemberRepository @@ -16,5 +18,14 @@ class MemberServiceUtils { throw ConflictException(ErrorCode.CONFLICT_MEMBER_EXCEPTION, "이미 존재하는 유저 $socialId - $socialType 입니다") } } + + fun findMemberBySocialIdAndSocialType( + memberRepository: MemberRepository, + socialId: String, + socialType: MemberSocialType + ): Member { + return memberRepository.findMemberBySocialIdAndSocialType(socialId, socialType) + ?: throw NotFoundException(ErrorCode.NOT_FOUND_MEMBER_EXCEPTION, "존재하지 않는 유저 $socialId $socialType 입니다") + } } } \ No newline at end of file diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt index e08e480..9cfa29f 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt @@ -59,4 +59,8 @@ class Member( fun initOnboarding(onboarding: Onboarding) { this.onboarding = onboarding } + + fun updateFcmToken(fcmToken: String) { + this.fcmToken = fcmToken + } } From 945b92fb737c9233533324ddafe2e1900d90790d Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 02:16:54 +0900 Subject: [PATCH 10/20] =?UTF-8?q?=E2=9C=A8:=20feat=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20API=20=EC=B6=94=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plu/api/controller/auth/AuthController.kt | 7 ++++ .../auth/dto/request/TokenRequestDto.kt | 15 +++++++ .../th/plu/api/service/auth/TokenService.kt | 42 ++++++++++++++++++- .../api/service/member/MemberServiceUtils.kt | 8 ++++ .../kotlin/com/th/plu/common/util/JwtUtils.kt | 3 +- .../com/th/plu/domain/domain/member/Member.kt | 8 +++- .../external/sqs/dto/FirebaseMessageDto.kt | 2 +- 7 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/TokenRequestDto.kt diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt index fca360e..174635b 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt @@ -2,6 +2,7 @@ package com.th.plu.api.controller.auth import com.th.plu.api.controller.auth.dto.request.LoginRequestDto import com.th.plu.api.controller.auth.dto.request.SignupRequestDto +import com.th.plu.api.controller.auth.dto.request.TokenRequestDto import com.th.plu.api.controller.auth.dto.response.TokenResponseDto import com.th.plu.api.service.auth.AuthServiceProvider import com.th.plu.api.service.auth.TokenService @@ -41,4 +42,10 @@ class AuthController( val tokenInfo = tokenService.createTokenInfo(memberId) return ApiResponse.success(tokenInfo) } + + @Operation(summary = "토큰 갱신") + @PostMapping("/v1/auth/refresh") + fun reissue(@Valid @RequestBody request: TokenRequestDto): ApiResponse { + return ApiResponse.success(tokenService.reissueToken(request)) + } } \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/TokenRequestDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/TokenRequestDto.kt new file mode 100644 index 0000000..672a894 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/dto/request/TokenRequestDto.kt @@ -0,0 +1,15 @@ +package com.th.plu.api.controller.auth.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank + +data class TokenRequestDto( + + @field:Schema(description = "토큰 - accessToken", example = "eyJhbGciOiJIUzUxMiJ9.udnKnDSK08EuX56E5k-") + @field:NotBlank(message = "accessToken 을 입력해주세요.") + val accessToken: String, + + @field:Schema(description = "토큰 - refreshToken", example = "eyJhbGciOiJIUzUxMiJ9.udnKnDSK08EuX56E5k-") + @field:NotBlank(message = "refreshToken 을 입력해주세요.") + val refreshToken: String +) \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt index a6a8ac5..6675a83 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt @@ -1,17 +1,57 @@ package com.th.plu.api.service.auth +import com.th.plu.api.controller.auth.dto.request.TokenRequestDto import com.th.plu.api.controller.auth.dto.response.TokenResponseDto +import com.th.plu.api.service.member.MemberServiceUtils +import com.th.plu.common.constant.RedisKey +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.UnauthorizedException import com.th.plu.common.util.JwtUtils +import com.th.plu.domain.domain.member.repository.MemberRepository +import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class TokenService( - private val jwtUtils: JwtUtils + private val jwtUtils: JwtUtils, + private val memberRepository: MemberRepository, + private val redisTemplate: RedisTemplate ) { @Transactional fun createTokenInfo(memberId: Long): TokenResponseDto { val tokens: List = jwtUtils.createTokenInfo(memberId) return TokenResponseDto(accessToken = tokens[0], refreshToken = tokens[1]) } + + @Transactional + fun reissueToken(request: TokenRequestDto): TokenResponseDto { + val memberId = jwtUtils.getMemberIdFromJwt(request.accessToken) + val member = MemberServiceUtils.findMemberById(memberRepository, memberId) + if (!jwtUtils.validateToken(request.refreshToken)) { + member.resetFcmToken() + throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "주어진 리프레시 토큰 ${request.refreshToken} 이 유효하지 않습니다." + ) + } + val refreshToken = redisTemplate.opsForValue().get(RedisKey.REFRESH_TOKEN + memberId) as String? + if (refreshToken == null) { + member.resetFcmToken() + throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "이미 만료된 리프레시 토큰 ${request.refreshToken} 입니다." + ) + } + if (refreshToken != request.refreshToken) { + jwtUtils.expireRefreshToken(member.id!!) + member.resetFcmToken() + throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "해당 리프레시 토큰 ${request.refreshToken} 의 정보가 일치하지 않습니다." + ) + } + val newTokens = jwtUtils.createTokenInfo(memberId) + return TokenResponseDto(accessToken = newTokens[0], refreshToken = newTokens[1]) + } } diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt index 00b32f2..9bff5b9 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/member/MemberServiceUtils.kt @@ -19,6 +19,14 @@ class MemberServiceUtils { } } + fun findMemberById( + memberRepository: MemberRepository, + id: Long + ): Member { + return memberRepository.findMemberById(id) + ?: throw NotFoundException(ErrorCode.NOT_FOUND_MEMBER_EXCEPTION, "존재하지 않는 유저 $id 입니다") + } + fun findMemberBySocialIdAndSocialType( memberRepository: MemberRepository, socialId: String, diff --git a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt b/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt index 93646a4..c7e1c4c 100644 --- a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt +++ b/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt @@ -92,7 +92,8 @@ class JwtUtils( } fun getMemberIdFromJwt(accessToken: String): Long { - return parseClaims(accessToken).get(JwtKey.MEMBER_ID, Long::class.java) + val memberId = parseClaims(accessToken)[JwtKey.MEMBER_ID] as Int + return memberId.toLong() } private fun parseClaims(accessToken: String): Claims { diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt index 9cfa29f..72349c4 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/domain/member/Member.kt @@ -24,8 +24,8 @@ class Member( @Enumerated(EnumType.STRING) var role: MemberRole, - @Column(name = "fcm_token", nullable = false, length = 300) - var fcmToken: String, + @Column(name = "fcm_token", nullable = true, length = 300) + var fcmToken: String?, @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = [CascadeType.ALL]) @JoinColumn(name = "setting_id", nullable = false) @@ -63,4 +63,8 @@ class Member( fun updateFcmToken(fcmToken: String) { this.fcmToken = fcmToken } + + fun resetFcmToken() { + this.fcmToken = null + } } diff --git a/plu-external/src/main/kotlin/com/th/plu/external/sqs/dto/FirebaseMessageDto.kt b/plu-external/src/main/kotlin/com/th/plu/external/sqs/dto/FirebaseMessageDto.kt index e59d3ee..e21ef0c 100644 --- a/plu-external/src/main/kotlin/com/th/plu/external/sqs/dto/FirebaseMessageDto.kt +++ b/plu-external/src/main/kotlin/com/th/plu/external/sqs/dto/FirebaseMessageDto.kt @@ -1,5 +1,5 @@ package com.th.plu.external.sqs.dto -data class FirebaseMessageDto(val type: MessageType, val fcmToken: String, val title: String, val body: String) : +data class FirebaseMessageDto(val type: MessageType, val fcmToken: String?, val title: String, val body: String) : SqsMessageDto(type) { } From 1128c06b4c8aad7b40fedc0475a990b258fed20f Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 03:09:59 +0900 Subject: [PATCH 11/20] =?UTF-8?q?=E2=9C=A8:=20feat=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=9A=A9=20interceptor,=20resolver=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/th/plu/api/config/WebConfig.kt | 24 ++++++++++++ .../com/th/plu/api/config/interceptor/Auth.kt | 5 +++ .../api/config/interceptor/AuthInterceptor.kt | 24 ++++++++++++ .../config/interceptor/LoginCheckHandler.kt | 26 +++++++++++++ .../th/plu/api/config/resolver/MemberId.kt | 5 +++ .../api/config/resolver/MemberIdResolver.kt | 37 +++++++++++++++++++ .../th/plu/api/service/auth/TokenService.kt | 4 ++ .../kotlin/com/th/plu/common/util/JwtUtils.kt | 6 +-- 8 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/config/WebConfig.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/Auth.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/AuthInterceptor.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberId.kt create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberIdResolver.kt diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/WebConfig.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/WebConfig.kt new file mode 100644 index 0000000..95c6174 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/WebConfig.kt @@ -0,0 +1,24 @@ +package com.th.plu.api.config + +import com.th.plu.api.config.interceptor.AuthInterceptor +import com.th.plu.api.config.resolver.MemberIdResolver +import org.springframework.context.annotation.Configuration +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig( + private val authInterceptor: AuthInterceptor, + private val memberIdResolver: MemberIdResolver +) : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(authInterceptor) + } + + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(memberIdResolver) + } + +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/Auth.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/Auth.kt new file mode 100644 index 0000000..f3176a5 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/Auth.kt @@ -0,0 +1,5 @@ +package com.th.plu.api.config.interceptor + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Auth diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/AuthInterceptor.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/AuthInterceptor.kt new file mode 100644 index 0000000..88661ef --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/AuthInterceptor.kt @@ -0,0 +1,24 @@ +package com.th.plu.api.config.interceptor + +import com.th.plu.common.constant.JwtKey +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.method.HandlerMethod +import org.springframework.web.servlet.HandlerInterceptor + +@Component +class AuthInterceptor( + private val loginCheckHandler: LoginCheckHandler +) : HandlerInterceptor { + + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { + if (handler !is HandlerMethod) { + return true + } + handler.getMethodAnnotation(Auth::class.java) ?: return true + val memberId = loginCheckHandler.getMemberId(request) + request.setAttribute(JwtKey.MEMBER_ID, memberId) + return true + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt new file mode 100644 index 0000000..2304c23 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt @@ -0,0 +1,26 @@ +package com.th.plu.api.config.interceptor + +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.UnauthorizedException +import com.th.plu.common.util.JwtUtils +import jakarta.servlet.http.HttpServletRequest +import org.springframework.stereotype.Component + +@Component +class LoginCheckHandler( + private val jwtUtils: JwtUtils +) { + fun getMemberId(request: HttpServletRequest): Long { + val bearerToken: String? = request.getHeader("Authorization") + if (!bearerToken.isNullOrBlank() && bearerToken.startsWith("Bearer ")) { + val accessToken = bearerToken.substring("Bearer ".length) + if (jwtUtils.validateToken(accessToken)) { + val memberId = jwtUtils.getMemberIdFromJwt(accessToken) + if (memberId != null) { + return memberId + } + } + } + throw UnauthorizedException(ErrorCode.UNAUTHORIZED_EXCEPTION, "잘못된 JWT $bearerToken 입니다.") + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberId.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberId.kt new file mode 100644 index 0000000..62699c8 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberId.kt @@ -0,0 +1,5 @@ +package com.th.plu.api.config.resolver + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class MemberId diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberIdResolver.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberIdResolver.kt new file mode 100644 index 0000000..1043222 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/resolver/MemberIdResolver.kt @@ -0,0 +1,37 @@ +package com.th.plu.api.config.resolver + +import com.th.plu.api.config.interceptor.Auth +import com.th.plu.common.constant.JwtKey +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.InternalServerException +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class MemberIdResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(MemberId::class.java) && Long::class.java == parameter.parameterType + } + + override fun resolveArgument( + parameter: MethodParameter, mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory? + ): Any? { + parameter.getMethodAnnotation(Auth::class.java) + ?: throw InternalServerException( + ErrorCode.INTERNAL_SERVER_EXCEPTION, + "인증이 필요한 컨트롤러 입니다. @Auth 어노테이션을 붙여주세요." + ) + + return webRequest.getAttribute(JwtKey.MEMBER_ID, 0) + ?: throw InternalServerException( + ErrorCode.INTERNAL_SERVER_EXCEPTION, + "MEMBER_ID 를 가져오지 못했습니다. ($parameter.javaClass - $parameter.method)" + ) + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt index 6675a83..df0dfc7 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt @@ -27,6 +27,10 @@ class TokenService( @Transactional fun reissueToken(request: TokenRequestDto): TokenResponseDto { val memberId = jwtUtils.getMemberIdFromJwt(request.accessToken) + ?: throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "주어진 액세스 토큰 ${request.accessToken} 으로 유저 정보를 찾을 수 없습니다." + ) val member = MemberServiceUtils.findMemberById(memberRepository, memberId) if (!jwtUtils.validateToken(request.refreshToken)) { member.resetFcmToken() diff --git a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt b/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt index c7e1c4c..88fd4b3 100644 --- a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt +++ b/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt @@ -91,9 +91,9 @@ class JwtUtils( return false } - fun getMemberIdFromJwt(accessToken: String): Long { - val memberId = parseClaims(accessToken)[JwtKey.MEMBER_ID] as Int - return memberId.toLong() + fun getMemberIdFromJwt(accessToken: String): Long? { + val memberId = parseClaims(accessToken)[JwtKey.MEMBER_ID] as Int? + return memberId?.toLong() } private fun parseClaims(accessToken: String): Claims { From 3d96f66c76a0f44cc4c61245ce96b4faf564e424 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 03:10:34 +0900 Subject: [PATCH 12/20] =?UTF-8?q?=E2=9C=A8:=20feat=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20API=20=EC=B6=94=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plu/api/config/swagger/SwaggerConfig.kt | 5 +++++ .../plu/api/controller/auth/AuthController.kt | 16 +++++++++++--- .../plu/api/service/auth/CommonAuthService.kt | 21 +++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt index edb34cf..695a36c 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/swagger/SwaggerConfig.kt @@ -1,10 +1,12 @@ package com.th.plu.api.config.swagger +import com.th.plu.api.config.resolver.MemberId import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info import io.swagger.v3.oas.models.security.SecurityRequirement import io.swagger.v3.oas.models.security.SecurityScheme +import org.springdoc.core.utils.SpringDocUtils import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -34,4 +36,7 @@ class SwaggerConfig { .info(info) } + init { + SpringDocUtils.getConfig().addAnnotationsToIgnore(MemberId::class.java) + } } \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt index 174635b..9df2bf0 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt @@ -1,12 +1,14 @@ package com.th.plu.api.controller.auth +import com.th.plu.api.config.interceptor.Auth +import com.th.plu.api.config.resolver.MemberId import com.th.plu.api.controller.auth.dto.request.LoginRequestDto import com.th.plu.api.controller.auth.dto.request.SignupRequestDto import com.th.plu.api.controller.auth.dto.request.TokenRequestDto import com.th.plu.api.controller.auth.dto.response.TokenResponseDto import com.th.plu.api.service.auth.AuthServiceProvider +import com.th.plu.api.service.auth.CommonAuthService import com.th.plu.api.service.auth.TokenService -import com.th.plu.api.service.member.MemberService import com.th.plu.common.dto.response.ApiResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag @@ -21,8 +23,8 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/api") class AuthController( private val authServiceProvider: AuthServiceProvider, - private val memberService: MemberService, - private val tokenService: TokenService + private val tokenService: TokenService, + private val commonAuthService: CommonAuthService ) { @Operation(summary = "소셜 회원가입") @@ -48,4 +50,12 @@ class AuthController( fun reissue(@Valid @RequestBody request: TokenRequestDto): ApiResponse { return ApiResponse.success(tokenService.reissueToken(request)) } + + @Operation(summary = "로그아웃") + @PostMapping("/v1/auth/logout") + @Auth + fun logout(@MemberId memberId: Long): ApiResponse { + commonAuthService.logout(memberId) + return ApiResponse.success() + } } \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt new file mode 100644 index 0000000..7ea3af8 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt @@ -0,0 +1,21 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.service.member.MemberServiceUtils +import com.th.plu.common.util.JwtUtils +import com.th.plu.domain.domain.member.repository.MemberRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CommonAuthService( + private val memberRepository: MemberRepository, + private val jwtUtils: JwtUtils +) { + + @Transactional + fun logout(memberId: Long) { + val member = MemberServiceUtils.findMemberById(memberRepository, memberId) + jwtUtils.expireRefreshToken(member.id!!) + member.resetFcmToken() + } +} \ No newline at end of file From 1fa63ad91ea997de34b7b12ae959f7ac413df3ae Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 03:54:55 +0900 Subject: [PATCH 13/20] =?UTF-8?q?=E2=9C=A8:=20feat=20=EC=95=A0=ED=94=8C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plu/api/service/auth/AppleAuthService.kt | 38 +++++++++++++ .../api/service/auth/AuthServiceProvider.kt | 4 +- plu-external/build.gradle.kts | 26 ++++----- .../external/client/apple/AppleApiCaller.kt | 8 +++ .../client/apple/AppleTokenProvider.kt | 54 +++++++++++++++++++ .../client/apple/WebClientAppleCaller.kt | 28 ++++++++++ .../dto/response/AppleProfileResponseDto.kt | 27 ++++++++++ 7 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/auth/AppleAuthService.kt create mode 100644 plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleApiCaller.kt create mode 100644 plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleTokenProvider.kt create mode 100644 plu-external/src/main/kotlin/com/th/plu/external/client/apple/WebClientAppleCaller.kt create mode 100644 plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AppleAuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AppleAuthService.kt new file mode 100644 index 0000000..87d44a3 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AppleAuthService.kt @@ -0,0 +1,38 @@ +package com.th.plu.api.service.auth + +import com.th.plu.api.controller.auth.dto.request.LoginRequestDto +import com.th.plu.api.controller.auth.dto.request.SignupRequestDto +import com.th.plu.api.service.member.MemberService +import com.th.plu.api.service.member.MemberServiceUtils +import com.th.plu.domain.domain.member.MemberSocialType +import com.th.plu.domain.domain.member.repository.MemberRepository +import com.th.plu.external.client.apple.AppleTokenProvider +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AppleAuthService( + private val appleTokenProvider: AppleTokenProvider, + private val memberService: MemberService, + private val memberRepository: MemberRepository +) : AuthService { + + companion object { + private val socialType: MemberSocialType = MemberSocialType.APPLE + } + + @Transactional + override fun signup(request: SignupRequestDto): Long { + val socialId = appleTokenProvider.getSocialIdFromIdToken(request.token) + return memberService.registerUser(request.toCreateUserDto(socialId)) + } + + @Transactional + override fun login(request: LoginRequestDto): Long { + val socialId = appleTokenProvider.getSocialIdFromIdToken(request.token) + val member = MemberServiceUtils.findMemberBySocialIdAndSocialType(memberRepository, socialId, socialType) + member.updateFcmToken(request.fcmToken) + return member.id!! + } + +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt index bd23590..b35f720 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/AuthServiceProvider.kt @@ -6,7 +6,7 @@ import org.springframework.stereotype.Component @Component class AuthServiceProvider( -// private val appleAuthService: AppleAuthService, + private val appleAuthService: AppleAuthService, private val kakaoAuthService: KakaoAuthService ) { companion object { @@ -16,7 +16,7 @@ class AuthServiceProvider( @PostConstruct fun initAuthServiceMap() { authServiceMap[MemberSocialType.KAKAO] = kakaoAuthService -// authServiceMap[MemberSocialType.APPLE] = appleAuthService + authServiceMap[MemberSocialType.APPLE] = appleAuthService } fun getAuthService(socialType: MemberSocialType): AuthService { diff --git a/plu-external/build.gradle.kts b/plu-external/build.gradle.kts index 3e76096..66125d7 100644 --- a/plu-external/build.gradle.kts +++ b/plu-external/build.gradle.kts @@ -1,31 +1,33 @@ plugins { - kotlin("jvm") + kotlin("jvm") } tasks.jar { - enabled = true + enabled = true } tasks.bootJar { - enabled = false + enabled = false } dependencies { - implementation(project(":plu-common")) + implementation(project(":plu-common")) - // SQS - api(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1")) - api("io.awspring.cloud:spring-cloud-aws-starter-sqs") + // SQS + api(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1")) + api("io.awspring.cloud:spring-cloud-aws-starter-sqs") + // WebFlux + implementation("org.springframework.boot:spring-boot-starter-webflux") - // WebFlux - implementation("org.springframework.boot:spring-boot-starter-webflux") + //jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.2") - implementation(kotlin("stdlib-jdk8")) + implementation(kotlin("stdlib-jdk8")) } repositories { - mavenCentral() + mavenCentral() } kotlin { - jvmToolchain(17) + jvmToolchain(17) } diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleApiCaller.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleApiCaller.kt new file mode 100644 index 0000000..82c4dfe --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleApiCaller.kt @@ -0,0 +1,8 @@ +package com.th.plu.external.client.apple + +import com.th.plu.external.client.apple.dto.response.AppleProfileResponseDto + +interface AppleApiCaller { + + fun getProfileInfo(): AppleProfileResponseDto +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleTokenProvider.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleTokenProvider.kt new file mode 100644 index 0000000..8447bc3 --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/AppleTokenProvider.kt @@ -0,0 +1,54 @@ +package com.th.plu.external.client.apple + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.UnauthorizedException +import io.jsonwebtoken.Jwts +import org.springframework.stereotype.Component +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.RSAPublicKeySpec +import java.util.* + +@Component +class AppleTokenProvider( + private val appleApiCaller: AppleApiCaller, + private val objectMapper: ObjectMapper +) { + + fun getSocialIdFromIdToken(idToken: String): String { + val headerIdToken = idToken.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + return try { + val header = objectMapper.readValue( + String(Base64.getDecoder().decode(headerIdToken), StandardCharsets.UTF_8), + object : TypeReference>() {}) + val publicKey: PublicKey = getPublicKey(header) + val claims = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(idToken) + .body + claims.subject // return socialId + } catch (e: Exception) { + throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "잘못된 애플 idToken $idToken 입니다 (reason: ${e.message})" + ) + } + } + + private fun getPublicKey(header: Map): PublicKey { + val response = appleApiCaller.getProfileInfo() + val key = response.getMatchedPublicKey(header["kid"].toString(), header["alg"].toString()) + val nBytes: ByteArray = Base64.getUrlDecoder().decode(key.n) + val eBytes: ByteArray = Base64.getUrlDecoder().decode(key.e) + val nBigInt = BigInteger(1, nBytes) + val eBigInt = BigInteger(1, eBytes) + val publicKeySpec = RSAPublicKeySpec(nBigInt, eBigInt) + val keyFactory = KeyFactory.getInstance(key.kty) + return keyFactory.generatePublic(publicKeySpec) + } +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/WebClientAppleCaller.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/WebClientAppleCaller.kt new file mode 100644 index 0000000..821500e --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/WebClientAppleCaller.kt @@ -0,0 +1,28 @@ +package com.th.plu.external.client.apple + +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.BadGatewayException +import com.th.plu.external.client.apple.dto.response.AppleProfileResponseDto +import org.springframework.http.HttpStatusCode +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient + +@Component +class WebClientAppleCaller( + private val webClient: WebClient +) : AppleApiCaller { + + override fun getProfileInfo(): AppleProfileResponseDto { + return webClient.get() + .uri("https://appleid.apple.com/auth/keys") + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError) { + throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "애플 로그인 연동 중 에러가 발생하였습니다.") + } + .onStatus(HttpStatusCode::is5xxServerError) { + throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "애플 로그인 연동 중 에러가 발생하였습니다.") + } + .bodyToMono(AppleProfileResponseDto::class.java) + .block()!! + } +} diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt new file mode 100644 index 0000000..66be01f --- /dev/null +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt @@ -0,0 +1,27 @@ +package com.th.plu.external.client.apple.dto.response + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.BadGatewayException + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AppleProfileResponseDto( + val keys: List +) { + class Key { + val alg: String? = null + val e: String? = null + val kid: String? = null + val kty: String? = null + val n: String? = null + val use: String? = null + } + + fun getMatchedPublicKey(kid: String, alg: String): Key { + return try { + this.keys.first { it.kid.equals(kid) && it.alg.equals(alg) } + } catch (_: NoSuchElementException) { + throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "애플 로그인 연동 중 에러가 발생하였습니다.") + } + } +} From 64e3e8cc4a8929e5501b8437ade9fefceae72a8a Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Sat, 17 Feb 2024 15:01:36 +0900 Subject: [PATCH 14/20] =?UTF-8?q?=E2=9A=A1=EF=B8=8F:=20fix=20Redis=20Templ?= =?UTF-8?q?ate=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/com/th/plu/common/util/JwtUtils.kt | 12 ++++++------ .../firebase/FirebaseCloudMessageService.kt | 13 +++++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt b/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt index 88fd4b3..34a843b 100644 --- a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt +++ b/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt @@ -16,13 +16,12 @@ import java.util.* import java.util.concurrent.TimeUnit @Component -class JwtUtils( - private val redisTemplate: RedisTemplate -) { +class JwtUtils { @Value("\${jwt.secret}") private var jwtSecret: String? = null + private var redisTemplate: RedisTemplate? = null private var secretKey: Key? = null private val log = LoggerFactory.getLogger(this.javaClass) @@ -37,6 +36,7 @@ class JwtUtils( @PostConstruct fun init() { + this.redisTemplate = RedisTemplate() val keyBytes: ByteArray = Decoders.BASE64.decode(jwtSecret) this.secretKey = Keys.hmacShaKeyFor(keyBytes) } @@ -59,14 +59,14 @@ class JwtUtils( .signWith(secretKey, SignatureAlgorithm.HS512) .compact() - redisTemplate.opsForValue() - .set(RedisKey.REFRESH_TOKEN + memberId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS) + redisTemplate?.opsForValue() + ?.set(RedisKey.REFRESH_TOKEN + memberId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS) return listOf(accessToken, refreshToken) } fun expireRefreshToken(memberId: Long) { - redisTemplate.opsForValue().set(RedisKey.REFRESH_TOKEN + memberId, "", EXPIRED_TIME, TimeUnit.MILLISECONDS) + redisTemplate?.opsForValue()?.set(RedisKey.REFRESH_TOKEN + memberId, "", EXPIRED_TIME, TimeUnit.MILLISECONDS) } fun validateToken(token: String?): Boolean { diff --git a/plu-notification/src/main/kotlin/com/th/plu/notification/firebase/FirebaseCloudMessageService.kt b/plu-notification/src/main/kotlin/com/th/plu/notification/firebase/FirebaseCloudMessageService.kt index 7c8e527..34e35c8 100644 --- a/plu-notification/src/main/kotlin/com/th/plu/notification/firebase/FirebaseCloudMessageService.kt +++ b/plu-notification/src/main/kotlin/com/th/plu/notification/firebase/FirebaseCloudMessageService.kt @@ -25,9 +25,11 @@ class FirebaseCloudMessageService( private val log = LoggerFactory.getLogger(this.javaClass) private val LOG_PREFIX = "====> [Firebase Cloud Message]" - fun sendMessageTo(fcmToken: String, title: String, body: String) { - val message = makeMessage(fcmToken, title, body) - firebaseApiCaller.requestFcmMessaging(getAccessToken(), message) + fun sendMessageTo(fcmToken: String?, title: String, body: String) { + if (fcmToken != null) { + val message = makeMessage(fcmToken, title, body) + firebaseApiCaller.requestFcmMessaging(getAccessToken(), message) + } } fun makeMessage(fcmToken: String, title: String, body: String): String { @@ -45,7 +47,10 @@ class FirebaseCloudMessageService( return googleCredentials.accessToken.tokenValue } catch (exception: Exception) { log.error(exception.message, exception) - throw BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION, "${LOG_PREFIX} FCM Access Token 발급 과정에서 에러가 발생하였습니다.") + throw BadGatewayException( + ErrorCode.BAD_GATEWAY_EXCEPTION, + "${LOG_PREFIX} FCM Access Token 발급 과정에서 에러가 발생하였습니다." + ) } } } From e7176bf7493cc1339d2fb8b2ac9c6949cbc94f3d Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 1 Mar 2024 17:17:47 +0900 Subject: [PATCH 15/20] =?UTF-8?q?=E2=9A=A1=EF=B8=8F:=20fix=20QuerydslConfi?= =?UTF-8?q?g=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/th/plu/domain/config/querydsl/QueryDslConfig.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt b/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt index d3cfc3d..f6e5785 100644 --- a/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt +++ b/plu-domain/src/main/kotlin/com/th/plu/domain/config/querydsl/QueryDslConfig.kt @@ -7,11 +7,10 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration -class QueryDslConfig { - +class QueryDslConfig( @PersistenceContext - private lateinit var entityManager: EntityManager - + private val entityManager: EntityManager +) { @Bean fun queryFactory(): JPAQueryFactory { return JPAQueryFactory(entityManager) From 8663d1cd95fc2a2e5d9cd0ab608e90cbdd680a1d Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 1 Mar 2024 17:19:05 +0900 Subject: [PATCH 16/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F:=20refactor=20RedisTem?= =?UTF-8?q?plate=20wrapping=20=EC=A0=81=EC=9A=A9,=20class=20=EB=AA=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plu-api/build.gradle.kts | 5 ++++ .../config/interceptor/LoginCheckHandler.kt | 8 +++---- .../plu/api/service/auth/CommonAuthService.kt | 6 ++--- .../th/plu/api/service/auth/TokenService.kt | 14 +++++------ .../th/plu/api/service/auth/jwt/JwtHandler.kt | 23 ++++++++----------- .../th/plu/api/service/redis/RedisHandler.kt | 18 +++++++++++++++ plu-common/build.gradle.kts | 8 ------- 7 files changed, 46 insertions(+), 36 deletions(-) rename plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt => plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt (81%) create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt diff --git a/plu-api/build.gradle.kts b/plu-api/build.gradle.kts index 0867965..a09e21d 100644 --- a/plu-api/build.gradle.kts +++ b/plu-api/build.gradle.kts @@ -24,6 +24,11 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springframework.session:spring-session-data-redis") implementation(kotlin("stdlib-jdk8")) + + //jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") } repositories { mavenCentral() diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt index 2304c23..a6bebca 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt @@ -1,21 +1,21 @@ package com.th.plu.api.config.interceptor +import com.th.plu.api.service.auth.jwt.JwtHandler import com.th.plu.common.exception.code.ErrorCode import com.th.plu.common.exception.model.UnauthorizedException -import com.th.plu.common.util.JwtUtils import jakarta.servlet.http.HttpServletRequest import org.springframework.stereotype.Component @Component class LoginCheckHandler( - private val jwtUtils: JwtUtils + private val jwtHandler: JwtHandler ) { fun getMemberId(request: HttpServletRequest): Long { val bearerToken: String? = request.getHeader("Authorization") if (!bearerToken.isNullOrBlank() && bearerToken.startsWith("Bearer ")) { val accessToken = bearerToken.substring("Bearer ".length) - if (jwtUtils.validateToken(accessToken)) { - val memberId = jwtUtils.getMemberIdFromJwt(accessToken) + if (jwtHandler.validateToken(accessToken)) { + val memberId = jwtHandler.getMemberIdFromJwt(accessToken) if (memberId != null) { return memberId } diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt index 7ea3af8..65901c9 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/CommonAuthService.kt @@ -1,7 +1,7 @@ package com.th.plu.api.service.auth +import com.th.plu.api.service.auth.jwt.JwtHandler import com.th.plu.api.service.member.MemberServiceUtils -import com.th.plu.common.util.JwtUtils import com.th.plu.domain.domain.member.repository.MemberRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -9,13 +9,13 @@ import org.springframework.transaction.annotation.Transactional @Service class CommonAuthService( private val memberRepository: MemberRepository, - private val jwtUtils: JwtUtils + private val jwtHandler: JwtHandler ) { @Transactional fun logout(memberId: Long) { val member = MemberServiceUtils.findMemberById(memberRepository, memberId) - jwtUtils.expireRefreshToken(member.id!!) + jwtHandler.expireRefreshToken(member.id!!) member.resetFcmToken() } } \ No newline at end of file diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt index df0dfc7..89cc703 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt @@ -2,11 +2,11 @@ package com.th.plu.api.service.auth import com.th.plu.api.controller.auth.dto.request.TokenRequestDto import com.th.plu.api.controller.auth.dto.response.TokenResponseDto +import com.th.plu.api.service.auth.jwt.JwtHandler import com.th.plu.api.service.member.MemberServiceUtils import com.th.plu.common.constant.RedisKey import com.th.plu.common.exception.code.ErrorCode import com.th.plu.common.exception.model.UnauthorizedException -import com.th.plu.common.util.JwtUtils import com.th.plu.domain.domain.member.repository.MemberRepository import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Service @@ -14,25 +14,25 @@ import org.springframework.transaction.annotation.Transactional @Service class TokenService( - private val jwtUtils: JwtUtils, + private val jwtHandler: JwtHandler, private val memberRepository: MemberRepository, private val redisTemplate: RedisTemplate ) { @Transactional fun createTokenInfo(memberId: Long): TokenResponseDto { - val tokens: List = jwtUtils.createTokenInfo(memberId) + val tokens: List = jwtHandler.createTokenInfo(memberId) return TokenResponseDto(accessToken = tokens[0], refreshToken = tokens[1]) } @Transactional fun reissueToken(request: TokenRequestDto): TokenResponseDto { - val memberId = jwtUtils.getMemberIdFromJwt(request.accessToken) + val memberId = jwtHandler.getMemberIdFromJwt(request.accessToken) ?: throw UnauthorizedException( ErrorCode.UNAUTHORIZED_EXCEPTION, "주어진 액세스 토큰 ${request.accessToken} 으로 유저 정보를 찾을 수 없습니다." ) val member = MemberServiceUtils.findMemberById(memberRepository, memberId) - if (!jwtUtils.validateToken(request.refreshToken)) { + if (!jwtHandler.validateToken(request.refreshToken)) { member.resetFcmToken() throw UnauthorizedException( ErrorCode.UNAUTHORIZED_EXCEPTION, @@ -48,14 +48,14 @@ class TokenService( ) } if (refreshToken != request.refreshToken) { - jwtUtils.expireRefreshToken(member.id!!) + jwtHandler.expireRefreshToken(member.id!!) member.resetFcmToken() throw UnauthorizedException( ErrorCode.UNAUTHORIZED_EXCEPTION, "해당 리프레시 토큰 ${request.refreshToken} 의 정보가 일치하지 않습니다." ) } - val newTokens = jwtUtils.createTokenInfo(memberId) + val newTokens = jwtHandler.createTokenInfo(memberId) return TokenResponseDto(accessToken = newTokens[0], refreshToken = newTokens[1]) } } diff --git a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt similarity index 81% rename from plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt rename to plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt index 34a843b..ab9b8c9 100644 --- a/plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt @@ -1,5 +1,6 @@ -package com.th.plu.common.util +package com.th.plu.api.service.auth.jwt +import com.th.plu.api.service.redis.RedisHandler import com.th.plu.common.constant.JwtKey import com.th.plu.common.constant.RedisKey import io.jsonwebtoken.* @@ -9,26 +10,22 @@ import io.jsonwebtoken.security.Keys import jakarta.annotation.PostConstruct import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value -import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Component import java.security.Key import java.util.* -import java.util.concurrent.TimeUnit @Component -class JwtUtils { - +class JwtHandler( + private val redisHandler: RedisHandler, +) { @Value("\${jwt.secret}") private var jwtSecret: String? = null - - private var redisTemplate: RedisTemplate? = null private var secretKey: Key? = null private val log = LoggerFactory.getLogger(this.javaClass) companion object { -// private const val ACCESS_TOKEN_EXPIRE_TIME = 10 * 60 * 1000L // 10분 -// private const val REFRESH_TOKEN_EXPIRE_TIME = 6 * 30 * 24 * 60 * 60 * 1000L // 180일 - + // private const val ACCESS_TOKEN_EXPIRE_TIME = 10 * 60 * 1000L // 10분 + // private const val REFRESH_TOKEN_EXPIRE_TIME = 6 * 30 * 24 * 60 * 60 * 1000L // 180일 private const val ACCESS_TOKEN_EXPIRE_TIME = 365 * 24 * 60 * 60 * 1000L; // 1년 private const val REFRESH_TOKEN_EXPIRE_TIME = 365 * 24 * 60 * 60 * 1000L; // 1년 private const val EXPIRED_TIME = 1L @@ -36,7 +33,6 @@ class JwtUtils { @PostConstruct fun init() { - this.redisTemplate = RedisTemplate() val keyBytes: ByteArray = Decoders.BASE64.decode(jwtSecret) this.secretKey = Keys.hmacShaKeyFor(keyBytes) } @@ -59,14 +55,13 @@ class JwtUtils { .signWith(secretKey, SignatureAlgorithm.HS512) .compact() - redisTemplate?.opsForValue() - ?.set(RedisKey.REFRESH_TOKEN + memberId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS) + redisHandler.set(RedisKey.REFRESH_TOKEN + memberId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME) return listOf(accessToken, refreshToken) } fun expireRefreshToken(memberId: Long) { - redisTemplate?.opsForValue()?.set(RedisKey.REFRESH_TOKEN + memberId, "", EXPIRED_TIME, TimeUnit.MILLISECONDS) + redisHandler.delete(RedisKey.REFRESH_TOKEN + memberId) } fun validateToken(token: String?): Boolean { diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt new file mode 100644 index 0000000..1d8a9c7 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt @@ -0,0 +1,18 @@ +package com.th.plu.api.service.redis + +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit + +@Component +class RedisHandler( + private val redisTemplate: RedisTemplate +) { + fun set(key: String, value: Any, timeout: Long) { + redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MILLISECONDS) + } + + fun delete(key: String) { + redisTemplate.delete(key) + } +} \ No newline at end of file diff --git a/plu-common/build.gradle.kts b/plu-common/build.gradle.kts index 72bdcf9..f79ba45 100644 --- a/plu-common/build.gradle.kts +++ b/plu-common/build.gradle.kts @@ -15,14 +15,6 @@ dependencies { // web implementation("org.springframework.boot:spring-boot-starter-web") - - // Redis - implementation("org.springframework.boot:spring-boot-starter-data-redis") - - //jwt - implementation("io.jsonwebtoken:jjwt-api:0.11.2") - runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") - runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") } repositories { mavenCentral() From 59832a38361b241b7cfd81b3e9202555a8d2bdbc Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 1 Mar 2024 17:19:49 +0900 Subject: [PATCH 17/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F:=20refactor=20TokenSer?= =?UTF-8?q?vice=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20RedisHandler=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/th/plu/api/controller/auth/AuthController.kt | 2 +- .../th/plu/api/service/auth/{ => jwt}/TokenService.kt | 9 ++++----- .../kotlin/com/th/plu/api/service/redis/RedisHandler.kt | 4 ++++ 3 files changed, 9 insertions(+), 6 deletions(-) rename plu-api/src/main/kotlin/com/th/plu/api/service/auth/{ => jwt}/TokenService.kt (89%) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt index 9df2bf0..d98a762 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/controller/auth/AuthController.kt @@ -8,7 +8,7 @@ import com.th.plu.api.controller.auth.dto.request.TokenRequestDto import com.th.plu.api.controller.auth.dto.response.TokenResponseDto import com.th.plu.api.service.auth.AuthServiceProvider import com.th.plu.api.service.auth.CommonAuthService -import com.th.plu.api.service.auth.TokenService +import com.th.plu.api.service.auth.jwt.TokenService import com.th.plu.common.dto.response.ApiResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt similarity index 89% rename from plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt rename to plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt index 89cc703..995464d 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/TokenService.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt @@ -1,14 +1,13 @@ -package com.th.plu.api.service.auth +package com.th.plu.api.service.auth.jwt import com.th.plu.api.controller.auth.dto.request.TokenRequestDto import com.th.plu.api.controller.auth.dto.response.TokenResponseDto -import com.th.plu.api.service.auth.jwt.JwtHandler import com.th.plu.api.service.member.MemberServiceUtils +import com.th.plu.api.service.redis.RedisHandler import com.th.plu.common.constant.RedisKey import com.th.plu.common.exception.code.ErrorCode import com.th.plu.common.exception.model.UnauthorizedException import com.th.plu.domain.domain.member.repository.MemberRepository -import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -16,7 +15,7 @@ import org.springframework.transaction.annotation.Transactional class TokenService( private val jwtHandler: JwtHandler, private val memberRepository: MemberRepository, - private val redisTemplate: RedisTemplate + private val redisHandler: RedisHandler ) { @Transactional fun createTokenInfo(memberId: Long): TokenResponseDto { @@ -39,7 +38,7 @@ class TokenService( "주어진 리프레시 토큰 ${request.refreshToken} 이 유효하지 않습니다." ) } - val refreshToken = redisTemplate.opsForValue().get(RedisKey.REFRESH_TOKEN + memberId) as String? + val refreshToken = redisHandler.get(RedisKey.REFRESH_TOKEN + memberId) if (refreshToken == null) { member.resetFcmToken() throw UnauthorizedException( diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt index 1d8a9c7..92eacca 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/redis/RedisHandler.kt @@ -8,6 +8,10 @@ import java.util.concurrent.TimeUnit class RedisHandler( private val redisTemplate: RedisTemplate ) { + fun get(key: String): String? { + return redisTemplate.opsForValue().get(key) as String? + } + fun set(key: String, value: Any, timeout: Long) { redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MILLISECONDS) } From 17513742a3a75667e48639f42c5978e7195572dd Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 1 Mar 2024 17:37:29 +0900 Subject: [PATCH 18/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F:=20refactor=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=83=9D=EC=84=B1=20=ED=9B=84=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/th/plu/api/service/auth/jwt/JwtHandler.kt | 4 ++-- .../com/th/plu/api/service/auth/jwt/TokenDto.kt | 12 ++++++++++++ .../com/th/plu/api/service/auth/jwt/TokenService.kt | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenDto.kt diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt index ab9b8c9..4355e89 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt @@ -37,7 +37,7 @@ class JwtHandler( this.secretKey = Keys.hmacShaKeyFor(keyBytes) } - fun createTokenInfo(memberId: Long): List { + fun createTokenInfo(memberId: Long): TokenDto { val now = Date().time val accessTokenExpiresIn = Date(now + ACCESS_TOKEN_EXPIRE_TIME) val refreshTokenExpiresIn = Date(now + REFRESH_TOKEN_EXPIRE_TIME) @@ -57,7 +57,7 @@ class JwtHandler( redisHandler.set(RedisKey.REFRESH_TOKEN + memberId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME) - return listOf(accessToken, refreshToken) + return TokenDto(accessToken, refreshToken) } fun expireRefreshToken(memberId: Long) { diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenDto.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenDto.kt new file mode 100644 index 0000000..3de5aa9 --- /dev/null +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenDto.kt @@ -0,0 +1,12 @@ +package com.th.plu.api.service.auth.jwt + +import com.th.plu.api.controller.auth.dto.response.TokenResponseDto + +data class TokenDto( + val accessToken: String, + val refreshToken: String +) { + fun toResponseDto(): TokenResponseDto { + return TokenResponseDto(accessToken, refreshToken) + } +} diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt index 995464d..91daef8 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt @@ -19,8 +19,8 @@ class TokenService( ) { @Transactional fun createTokenInfo(memberId: Long): TokenResponseDto { - val tokens: List = jwtHandler.createTokenInfo(memberId) - return TokenResponseDto(accessToken = tokens[0], refreshToken = tokens[1]) + val tokens = jwtHandler.createTokenInfo(memberId) + return tokens.toResponseDto() } @Transactional @@ -55,6 +55,6 @@ class TokenService( ) } val newTokens = jwtHandler.createTokenInfo(memberId) - return TokenResponseDto(accessToken = newTokens[0], refreshToken = newTokens[1]) + return newTokens.toResponseDto() } } From 4b5fe80d293973ff5aeb509422df55927c6ece2e Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 1 Mar 2024 17:50:06 +0900 Subject: [PATCH 19/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F:=20refactor=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../th/plu/api/config/interceptor/LoginCheckHandler.kt | 5 +---- .../kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt | 9 +++++++-- .../com/th/plu/api/service/auth/jwt/TokenService.kt | 5 ----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt index a6bebca..3139fd3 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/config/interceptor/LoginCheckHandler.kt @@ -15,10 +15,7 @@ class LoginCheckHandler( if (!bearerToken.isNullOrBlank() && bearerToken.startsWith("Bearer ")) { val accessToken = bearerToken.substring("Bearer ".length) if (jwtHandler.validateToken(accessToken)) { - val memberId = jwtHandler.getMemberIdFromJwt(accessToken) - if (memberId != null) { - return memberId - } + return jwtHandler.getMemberIdFromJwt(accessToken) } } throw UnauthorizedException(ErrorCode.UNAUTHORIZED_EXCEPTION, "잘못된 JWT $bearerToken 입니다.") diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt index 4355e89..3c5a2b8 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/JwtHandler.kt @@ -3,6 +3,8 @@ package com.th.plu.api.service.auth.jwt import com.th.plu.api.service.redis.RedisHandler import com.th.plu.common.constant.JwtKey import com.th.plu.common.constant.RedisKey +import com.th.plu.common.exception.code.ErrorCode +import com.th.plu.common.exception.model.UnauthorizedException import io.jsonwebtoken.* import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.DecodingException @@ -86,9 +88,12 @@ class JwtHandler( return false } - fun getMemberIdFromJwt(accessToken: String): Long? { + fun getMemberIdFromJwt(accessToken: String): Long { val memberId = parseClaims(accessToken)[JwtKey.MEMBER_ID] as Int? - return memberId?.toLong() + return memberId?.toLong() ?: throw UnauthorizedException( + ErrorCode.UNAUTHORIZED_EXCEPTION, + "주어진 액세스 토큰 $accessToken 으로 유저 정보를 찾을 수 없습니다." + ) } private fun parseClaims(accessToken: String): Claims { diff --git a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt index 91daef8..9733ea5 100644 --- a/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt +++ b/plu-api/src/main/kotlin/com/th/plu/api/service/auth/jwt/TokenService.kt @@ -17,7 +17,6 @@ class TokenService( private val memberRepository: MemberRepository, private val redisHandler: RedisHandler ) { - @Transactional fun createTokenInfo(memberId: Long): TokenResponseDto { val tokens = jwtHandler.createTokenInfo(memberId) return tokens.toResponseDto() @@ -26,10 +25,6 @@ class TokenService( @Transactional fun reissueToken(request: TokenRequestDto): TokenResponseDto { val memberId = jwtHandler.getMemberIdFromJwt(request.accessToken) - ?: throw UnauthorizedException( - ErrorCode.UNAUTHORIZED_EXCEPTION, - "주어진 액세스 토큰 ${request.accessToken} 으로 유저 정보를 찾을 수 없습니다." - ) val member = MemberServiceUtils.findMemberById(memberRepository, memberId) if (!jwtHandler.validateToken(request.refreshToken)) { member.resetFcmToken() From 28c6c84a2d81a053b8f126735e3934a32296f7a6 Mon Sep 17 00:00:00 2001 From: orijoon98 Date: Fri, 1 Mar 2024 18:05:12 +0900 Subject: [PATCH 20/20] =?UTF-8?q?=E2=9A=A1=EF=B8=8F:=20fix=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/apple/dto/response/AppleProfileResponseDto.kt | 2 -- .../client/kakao/dto/response/KakaoProfileResponseDto.kt | 3 --- 2 files changed, 5 deletions(-) diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt index 66be01f..5f254ef 100644 --- a/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/apple/dto/response/AppleProfileResponseDto.kt @@ -1,10 +1,8 @@ package com.th.plu.external.client.apple.dto.response -import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.th.plu.common.exception.code.ErrorCode import com.th.plu.common.exception.model.BadGatewayException -@JsonIgnoreProperties(ignoreUnknown = true) data class AppleProfileResponseDto( val keys: List ) { diff --git a/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt index d17e3c4..d0122db 100644 --- a/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt +++ b/plu-external/src/main/kotlin/com/th/plu/external/client/kakao/dto/response/KakaoProfileResponseDto.kt @@ -1,8 +1,5 @@ package com.th.plu.external.client.kakao.dto.response -import com.fasterxml.jackson.annotation.JsonIgnoreProperties - -@JsonIgnoreProperties(ignoreUnknown = true) data class KakaoProfileResponseDto( val id: String )