From 7e1fd0dd106d982780ae3b5efb6da05ad32c62d9 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 5 Jan 2026 22:18:20 +0900 Subject: [PATCH 01/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20kotlinLogging=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20https://github.com/Ca?= =?UTF-8?q?mpusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CT-common/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/CT-common/build.gradle.kts b/CT-common/build.gradle.kts index ff31113..a51f896 100644 --- a/CT-common/build.gradle.kts +++ b/CT-common/build.gradle.kts @@ -25,6 +25,9 @@ dependencies { // Jackson api(libs.jackson.module.kotlin) + // Kotlin Logging + api(libs.kotlin.logging) + api(libs.kotlin.reflect) api(libs.kotlin.stdlib) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a0388e..9b266c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,8 @@ kotlin = "1.9.25" springBoot = "3.5.9" springDependencyManagement = "1.1.7" swaggerUI = "3.0.1" +jjwt = "0.12.7" +kotlinLogging = "7.0.14" apiChangeLog = "1.0.1" sejongPortalLogin = "1.0.0" httpLogging = "0.0.9" @@ -43,6 +45,12 @@ junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher # Swagger UI swagger-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "swaggerUI" } +# JJWT +jjwt = { module = "io.jsonwebtoken:jjwt", version.ref = "jjwt" } + +# Kotlin Logging +kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlinLogging" } + # Chuseok22 ApiChangeLog api-change-log = { module = "com.chuseok22:ApiChangeLog", version.ref = "apiChangeLog" } From 129c64ce54c2ef1947546c9855c31adf9baa1145 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Tue, 6 Jan 2026 10:54:56 +0900 Subject: [PATCH 02/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20CustomException=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20https://github.com/Ca?= =?UTF-8?q?mpusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/exception/CustomException.kt | 5 +++++ .../ctcommon/application/exception/ErrorCode.kt | 17 +++++++++++++++++ .../application/exception/ErrorResponse.kt | 6 ++++++ 3 files changed, 28 insertions(+) create mode 100644 CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/CustomException.kt create mode 100644 CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt create mode 100644 CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorResponse.kt diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/CustomException.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/CustomException.kt new file mode 100644 index 0000000..a2cb085 --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/CustomException.kt @@ -0,0 +1,5 @@ +package com.chuseok22.ctcommon.application.exception + +class CustomException( + val errorCode: ErrorCode, +) : RuntimeException(errorCode.message) \ No newline at end of file diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt new file mode 100644 index 0000000..8d467df --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt @@ -0,0 +1,17 @@ +package com.chuseok22.ctcommon.application.exception + +import org.springframework.http.HttpStatus + +enum class ErrorCode( + val status: HttpStatus, + val message: String +) { + + // Global + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 발생했습니다"), + ACCESS_DENIED(HttpStatus.FORBIDDEN, "접근이 거부되었습니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다"), + + // JWT + INVALID_JWT(HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT"), +} \ No newline at end of file diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorResponse.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorResponse.kt new file mode 100644 index 0000000..1e9b69b --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorResponse.kt @@ -0,0 +1,6 @@ +package com.chuseok22.ctcommon.application.exception + +data class ErrorResponse( + val errorCode: ErrorCode, + val errorMessage: String +) \ No newline at end of file From 98f452bb14719bcb35e7156a7d7ee1c2b099012a Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Tue, 6 Jan 2026 10:55:05 +0900 Subject: [PATCH 03/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 CT-web/src/main/kotlin/com/chuseok22/ctweb/application/exception/GlobalExceptionHandler.kt diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/application/exception/GlobalExceptionHandler.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/application/exception/GlobalExceptionHandler.kt new file mode 100644 index 0000000..89f6367 --- /dev/null +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/application/exception/GlobalExceptionHandler.kt @@ -0,0 +1,50 @@ +package com.chuseok22.ctweb.application.exception + +import com.chuseok22.ctcommon.application.exception.CustomException +import com.chuseok22.ctcommon.application.exception.ErrorCode +import com.chuseok22.ctcommon.application.exception.ErrorResponse +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +private val log = KotlinLogging.logger { } + +@RestControllerAdvice +class GlobalExceptionHandler { + + /** + * 커스텀 예외처리 (CustomException) + */ + @ExceptionHandler(CustomException::class) + fun handleCustomException(e: CustomException): ResponseEntity { + log.error(e) { "CustomException 발생: ${e.message}" } + + return ResponseEntity + .status(e.errorCode.status) + .body( + ErrorResponse( + errorCode = e.errorCode, + errorMessage = e.errorCode.message + ) + ) + } + + /** + * 그 외 예외처리 (500 에러) + */ + @ExceptionHandler(Exception::class) + fun handleException(e: Exception): ResponseEntity { + log.error { "예상치 못한 예외 발생: ${e.message}" } + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + ErrorResponse( + errorCode = ErrorCode.INTERNAL_SERVER_ERROR, + errorMessage = ErrorCode.INTERNAL_SERVER_ERROR.message + ) + ) + } +} \ No newline at end of file From b805e1926f173006f2ac07bf45f39573cf18e125 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Tue, 6 Jan 2026 10:55:20 +0900 Subject: [PATCH 04/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B4=80=EB=A0=A8=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80=20https://gith?= =?UTF-8?q?ub.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctauth/core/token/TokenProvider.kt | 31 +++++++++++++++++++ .../chuseok22/ctauth/core/token/TokenStore.kt | 14 +++++++++ 2 files changed, 45 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenProvider.kt create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenStore.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenProvider.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenProvider.kt new file mode 100644 index 0000000..8cedafa --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenProvider.kt @@ -0,0 +1,31 @@ +package com.chuseok22.ctauth.core.token + +import java.util.* + +interface TokenProvider { + + /** + * accessToken 생성 + */ + fun createAccessToken(memberId: String, role: String): String + + /** + * refreshToken 생성 + */ + fun createRefreshToken(memberId: String, role: String): String + + /** + * 토큰 유효 검사 + */ + fun isValidToken(token: String): Boolean + + /** + * 토큰에서 memberId 파싱 + */ + fun getMemberId(token: String): String + + /** + * 토큰 만료시간 반환 (ms) + */ + fun getExpiredAt(token: String): Date +} \ No newline at end of file diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenStore.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenStore.kt new file mode 100644 index 0000000..2ca915d --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenStore.kt @@ -0,0 +1,14 @@ +package com.chuseok22.ctauth.core.token + +interface TokenStore { + + /** + * 리프레시 토큰을 주어진 Key로 저장하고 TTL(ms) 설정 + */ + fun save(key: String, refreshToken: String, ttlMillis: Long) + + /** + * Key에 해당하는 리프레시 토큰 삭제 + */ + fun remove(key: String) +} \ No newline at end of file From f9256f83f31ed940f68d7b52c07d0a521352198f Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Tue, 6 Jan 2026 10:55:41 +0900 Subject: [PATCH 05/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20[auth=EB=AA=A8=EB=93=88]=20?= =?UTF-8?q?jjwt=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20https?= =?UTF-8?q?://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CT-auth/build.gradle.kts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CT-auth/build.gradle.kts b/CT-auth/build.gradle.kts index b5c05c1..a6c72ab 100644 --- a/CT-auth/build.gradle.kts +++ b/CT-auth/build.gradle.kts @@ -14,7 +14,14 @@ tasks.jar { dependencies { implementation(project(":CT-common")) + // Spring Security api(libs.spring.boot.starter.security) api(libs.spring.security.test) + + // JWT + api(libs.jjwt) + + // Sejong Portal Login + api(libs.sejong.portal.login) } From b8c301a64b20402854e99ae37bc0625f9599d5fb Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Tue, 6 Jan 2026 11:17:14 +0900 Subject: [PATCH 06/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JWT=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=B6=94=EA=B0=80=20htt?= =?UTF-8?q?ps://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/properties/JwtProperties.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/properties/JwtProperties.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/properties/JwtProperties.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/properties/JwtProperties.kt new file mode 100644 index 0000000..729eaac --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/properties/JwtProperties.kt @@ -0,0 +1,16 @@ +package com.chuseok22.ctauth.infrastructure.properties + +import jakarta.validation.constraints.NotBlank +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.validation.annotation.Validated + +@Validated +@ConfigurationProperties(prefix = "jwt") +data class JwtProperties( + @field:NotBlank + val secretKey: String, + val accessExpMillis: Long, + val refreshExpMillis: Long, + @field:NotBlank + val issuer: String +) From 798025212d6d747d74b6c1c895c2f4d4a325c014 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Tue, 6 Jan 2026 11:17:29 +0900 Subject: [PATCH 07/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JwtProvider.kt=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/constant/AuthConstants.kt | 19 +++ .../ctauth/infrastructure/jwt/JwtProvider.kt | 109 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt new file mode 100644 index 0000000..ce085ff --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt @@ -0,0 +1,19 @@ +package com.chuseok22.ctauth.infrastructure.constant + +object AuthConstants { + + // Auth + const val TOKEN_PREFIX: String = "Bearer " + const val HEADER_AUTHORIZATION: String = "Authorization" + + // CookieUtil + const val ROOT_DOMAIN: String = "campustable.shop" + const val ACCESS_TOKEN_KEY: String = "accessToken" + const val REFRESH_TOKEN_KEY: String = "refreshToken" + + // JwtUtil + const val ACCESS_TOKEN_CATEGORY: String = "accessToken" + const val REFRESH_TOKEN_CATEGORY: String = "refreshToken" + const val REDIS_REFRESH_TOKEN_KEY_PREFIX: String = "RT:" + +} \ No newline at end of file diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt new file mode 100644 index 0000000..2b91273 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt @@ -0,0 +1,109 @@ +package com.chuseok22.ctauth.infrastructure.jwt + +import com.chuseok22.ctauth.core.token.TokenProvider +import com.chuseok22.ctauth.infrastructure.constant.AuthConstants +import com.chuseok22.ctauth.infrastructure.properties.JwtProperties +import com.chuseok22.ctcommon.application.exception.CustomException +import com.chuseok22.ctcommon.application.exception.ErrorCode +import io.github.oshai.kotlinlogging.KotlinLogging +import io.jsonwebtoken.* +import io.jsonwebtoken.security.SignatureException +import java.time.Instant +import java.util.* +import javax.crypto.SecretKey + +private val log = KotlinLogging.logger { } + +class JwtProvider( + private val secretKey: SecretKey, + private val properties: JwtProperties +) : TokenProvider { + + override fun createAccessToken(memberId: String, role: String): String { + return createToken( + category = AuthConstants.ACCESS_TOKEN_CATEGORY, + memberId = memberId, + role = role, + expMillis = properties.accessExpMillis + ).also { log.info { "엑세스 토큰 생성완료: memberId = $memberId" } } + } + + override fun createRefreshToken(memberId: String, role: String): String { + return createToken( + category = AuthConstants.REFRESH_TOKEN_CATEGORY, + memberId = memberId, + role = role, + expMillis = properties.refreshExpMillis + ).also { log.info { "리프레시 토큰 생성완료: memberId = $memberId" } } + } + + override fun isValidToken(token: String): Boolean { + return try { + getClaims(token) + .also { log.debug { "JWT 토큰이 유효합니다" } } + true + } catch (e: ExpiredJwtException) { + log.warn(e) { "JWT 토큰 만료: ${e.message}" } + throw e // 만료 예외는 재전달 + } catch (e: UnsupportedJwtException) { + log.warn(e) { "지원하지 않는 JWT: ${e.message}" } + false + } catch (e: MalformedJwtException) { + log.warn(e) { "형식이 올바르지 않은 JWT: ${e.message}" } + false + } catch (e: SignatureException) { + log.warn(e) { "JWT 서명이 유효하지 않음: ${e.message}" } + false + } catch (e: IllegalArgumentException) { + log.warn(e) { "JWT 토큰이 비어있음: ${e.message}" } + false + } + } + + override fun getMemberId(token: String): String { + return try { + getClaims(token)["memberId"] as? String + ?: throw CustomException(ErrorCode.INVALID_JWT) + } catch (e: JwtException) { + log.error(e) { "JWT memberId 추출 실패: ${e.message}" } + throw e + } + } + + override fun getExpiredAt(token: String): Date { + return try { + getClaims(token).expiration + } catch (e: Exception) { + log.error(e) { "JWT 만료시간 추출 실패: ${e.message}" } + throw CustomException(ErrorCode.INVALID_JWT) + } + } + + private fun createToken( + category: String, + memberId: String, + role: String, + expMillis: Long + ): String { + val now = Instant.now() + return Jwts.builder() + .subject(memberId) + .claim("category", category) + .claim("role", role) + .issuer(properties.issuer) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusMillis(expMillis))) + .signWith(secretKey) + .compact() + } + + // 토큰에서 페이로드 (Claim) 추출 + private fun getClaims(token: String): Claims { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .payload + } + +} \ No newline at end of file From a33325e11fd38fb552ecb2cba37f935b92a3e061 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Tue, 6 Jan 2026 14:43:47 +0900 Subject: [PATCH 08/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20kotlin=20=EB=B0=8F=20swagge?= =?UTF-8?q?r-ui=20=EB=B2=84=EC=A0=84=20=EC=88=98=EC=A0=95=20https://github?= =?UTF-8?q?.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b266c1..4f12622 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] # Plugins & Core -kotlin = "1.9.25" +kotlin = "2.3.0" springBoot = "3.5.9" springDependencyManagement = "1.1.7" -swaggerUI = "3.0.1" +swaggerUI = "2.8.15" jjwt = "0.12.7" kotlinLogging = "7.0.14" apiChangeLog = "1.0.1" From 6916bf197cb2f0579823fa5a4c18fab0c72ff564 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 11:05:54 +0900 Subject: [PATCH 09/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=B9=B4=EB=A9=9C=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chuseok22/ctweb/infrastructure/config/SwaggerConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/SwaggerConfig.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/SwaggerConfig.kt index 82fbf78..625e7a8 100644 --- a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/SwaggerConfig.kt +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/SwaggerConfig.kt @@ -25,7 +25,7 @@ class SwaggerConfig( ) { @Bean - fun OpenAPI(): OpenAPI { + fun openAPI(): OpenAPI { val apiKey: SecurityScheme = SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") From 6ce26cd239f05bdb74ba82c5186d2b12103f8857 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 11:06:14 +0900 Subject: [PATCH 10/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20Redis=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20https://github.com/CampusTable/campus-t?= =?UTF-8?q?able-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CT-redis/build.gradle.kts | 19 +++++++++++++++++++ settings.gradle.kts | 1 + 2 files changed, 20 insertions(+) create mode 100644 CT-redis/build.gradle.kts diff --git a/CT-redis/build.gradle.kts b/CT-redis/build.gradle.kts new file mode 100644 index 0000000..ea91150 --- /dev/null +++ b/CT-redis/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("java-library") +} + +tasks.bootJar { + enabled = false +} + +tasks.jar { + enabled = true + archiveClassifier.set("") +} + +dependencies { + implementation(project(":CT-common")) + + // Redis + api(libs.spring.boot.starter.data.redis) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ea89535..b3efe18 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,4 +3,5 @@ rootProject.name = "campus-table-server" include("CT-auth") include("CT-common") include("CT-member") +include("CT-redis") include("CT-web") \ No newline at end of file From 10c022f030c302eb5dc7012ea8b5a2987f0f30b5 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 11:07:01 +0900 Subject: [PATCH 11/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20[redis=EB=AA=A8=EB=93=88]?= =?UTF-8?q?=20Redis=20=EA=B4=80=EB=A0=A8=20=ED=99=98=EA=B2=BD=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20Properties=20=EC=B6=94=EA=B0=80=20https?= =?UTF-8?q?://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/config/RedisConfig.kt | 86 +++++++++++++++++++ .../properties/RedisProperties.kt | 13 +++ 2 files changed, 99 insertions(+) create mode 100644 CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt create mode 100644 CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/properties/RedisProperties.kt diff --git a/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt new file mode 100644 index 0000000..439d704 --- /dev/null +++ b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt @@ -0,0 +1,86 @@ +package com.chuseok22.ctredis.infrastructure.config + +import com.chuseok22.ctredis.infrastructure.properties.RedisProperties +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinModule +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.connection.RedisStandaloneConfiguration +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer +import org.springframework.data.redis.serializer.StringRedisSerializer + +@Configuration +@EnableConfigurationProperties(RedisProperties::class) +class RedisConfig( + private val properties: RedisProperties +) { + + /** + * Redis Factory + */ + @Bean + fun redisConnectionFactory(redisProperties: RedisProperties): RedisConnectionFactory { + + val config = RedisStandaloneConfiguration().apply { + hostName = properties.host + port = properties.port + setPassword(redisProperties.password) + } + + return LettuceConnectionFactory(config) + } + + /** + * RedisTemplate + */ + @Bean + fun redisTemplate( + connectionFactory: RedisConnectionFactory + ): RedisTemplate { + val objectMapper = createObjectMapper() + + return RedisTemplate().apply { + this.connectionFactory = connectionFactory + + // Key Serializer: String + keySerializer = StringRedisSerializer() + hashKeySerializer = StringRedisSerializer() + + // Value Serializer: JSON + val jsonSerializer = GenericJackson2JsonRedisSerializer(objectMapper) + valueSerializer = jsonSerializer + hashValueSerializer = jsonSerializer + + afterPropertiesSet() + } + } + + /** + * ObjectMapper 생성 + */ + private fun createObjectMapper(): ObjectMapper { + return ObjectMapper().apply { + // Kotlin 모듈 + registerModule(KotlinModule.Builder().build()) + + // Java 8 Time API (LocalDateTime 등) + registerModule(JavaTimeModule()) + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + + // 다형성 지원 + activateDefaultTyping( + BasicPolymorphicTypeValidator.builder() + .allowIfBaseType(Any::class.java) + .build(), + ObjectMapper.DefaultTyping.NON_FINAL + ) + } + } +} \ No newline at end of file diff --git a/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/properties/RedisProperties.kt b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/properties/RedisProperties.kt new file mode 100644 index 0000000..4d323ee --- /dev/null +++ b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/properties/RedisProperties.kt @@ -0,0 +1,13 @@ +package com.chuseok22.ctredis.infrastructure.properties + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.validation.annotation.Validated + +@Validated +@ConfigurationProperties(prefix = "spring.data.redis") +data class RedisProperties( + val host: String, + val port: Int, + val password: String +) { +} \ No newline at end of file From ce77e7f96e87d77e476f63642a35f312dfa9fabc Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 13:25:47 +0900 Subject: [PATCH 12/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20[auth=EB=AA=A8=EB=93=88]=20?= =?UTF-8?q?redis=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20http?= =?UTF-8?q?s://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CT-auth/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/CT-auth/build.gradle.kts b/CT-auth/build.gradle.kts index a6c72ab..8c7e7ba 100644 --- a/CT-auth/build.gradle.kts +++ b/CT-auth/build.gradle.kts @@ -13,6 +13,7 @@ tasks.jar { dependencies { implementation(project(":CT-common")) + implementation(project(":CT-redis")) // Spring Security api(libs.spring.boot.starter.security) From 68ef431bff8ff97101b92ea84cb198742bc7d1da Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 13:27:37 +0900 Subject: [PATCH 13/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20CorsConfig=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctweb/infrastructure/config/CorsConfig.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/CorsConfig.kt diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/CorsConfig.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/CorsConfig.kt new file mode 100644 index 0000000..ed785a5 --- /dev/null +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/CorsConfig.kt @@ -0,0 +1,29 @@ +package com.chuseok22.ctweb.infrastructure.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.UrlBasedCorsConfigurationSource + +@Configuration +class CorsConfig { + + @Bean + fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration().apply { + allowedOriginPatterns = listOf( + "https://www.campustable.shop", + "http://localhost:3000" + ) + allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + allowedHeaders = listOf("*") + allowCredentials = true + maxAge = 3600L + } + + return UrlBasedCorsConfigurationSource().apply { + registerCorsConfiguration("/**", configuration) + } + } +} \ No newline at end of file From 55ab325eec96fe562b3e31392a935074e375d511 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 13:27:52 +0900 Subject: [PATCH 14/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20SecurityUrls=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/constant/SecurityUrls.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/SecurityUrls.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/SecurityUrls.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/SecurityUrls.kt new file mode 100644 index 0000000..e2de9ce --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/SecurityUrls.kt @@ -0,0 +1,18 @@ +package com.chuseok22.ctauth.infrastructure.constant + +object SecurityUrls { + + /** + * 인증을 생략할 URL 패턴 목록 + */ + @JvmStatic + val AUTH_WHITELIST = listOf( + // AUTH + "/api/auth/login", + "/api/auth/reissue", + + // Swagger + "/docs/swagger-ui/**", + "/v3/api-docs/**", + ) +} \ No newline at end of file From d32bb1ef520288cc5b2f19f67391bfd65a567f5b Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 13:28:19 +0900 Subject: [PATCH 15/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JwtConfig=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctauth/infrastructure/config/JwtConfig.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt new file mode 100644 index 0000000..3805baa --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt @@ -0,0 +1,35 @@ +package com.chuseok22.ctauth.infrastructure.config + +import com.chuseok22.ctauth.infrastructure.jwt.JwtProvider +import com.chuseok22.ctauth.infrastructure.properties.JwtProperties +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.Keys +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import javax.crypto.SecretKey + +@Configuration +@EnableConfigurationProperties(JwtProperties::class) +class JwtConfig( + private val properties: JwtProperties +) { + + /** + * JWT 서명에 사용할 secretKey 생성 + * base64로 인코딩 된 secretKey를 디코딩해서 SecretKey 객체 생성 + */ + @Bean + fun jwtSecretKey(): SecretKey { + val keyBytes: ByteArray = Decoders.BASE64.decode(properties.secretKey) + return Keys.hmacShaKeyFor(keyBytes) + } + + /** + * TokenProvider 구현체 Bean 등록 + */ + @Bean + fun jwtProvider(jwtSecretKey: SecretKey): JwtProvider { + return JwtProvider(jwtSecretKey(), properties) + } +} \ No newline at end of file From 8fc4bc257c79a24b3b45ee20b060b1bc891f4fee Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 13:28:27 +0900 Subject: [PATCH 16/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JwtStore=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctauth/infrastructure/jwt/JwtStore.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtStore.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtStore.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtStore.kt new file mode 100644 index 0000000..9f394e9 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtStore.kt @@ -0,0 +1,17 @@ +package com.chuseok22.ctauth.infrastructure.jwt + +import com.chuseok22.ctauth.core.token.TokenStore +import org.springframework.data.redis.core.RedisTemplate +import java.util.concurrent.TimeUnit + +class JwtStore( + private val redisTemplate: RedisTemplate +) : TokenStore { + override fun save(key: String, refreshToken: String, ttlMillis: Long) { + redisTemplate.opsForValue().set(key, refreshToken, ttlMillis, TimeUnit.MILLISECONDS) + } + + override fun remove(key: String) { + redisTemplate.delete(key) + } +} \ No newline at end of file From 72ca88a8ccc9efecc5b033cc2a49a832f748e5ed Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 14:21:14 +0900 Subject: [PATCH 17/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20[web=EB=AA=A8=EB=93=88]=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=8A=A4=EC=BA=94=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EC=99=80=EC=9D=BC=EB=93=9C=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20https://github.com/CampusTable?= =?UTF-8?q?/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctweb/infrastructure/config/ComponentScanConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/ComponentScanConfig.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/ComponentScanConfig.kt index c619122..688f7a6 100644 --- a/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/ComponentScanConfig.kt +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/infrastructure/config/ComponentScanConfig.kt @@ -5,7 +5,7 @@ import org.springframework.context.annotation.Configuration @Configuration @ComponentScan(basePackages = [ - "com.chuseok22.*" + "com.chuseok22" ]) class ComponentScanConfig { } \ No newline at end of file From 1c2e0e2233af0bafacefda4ffb996428885a69a2 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 14:21:37 +0900 Subject: [PATCH 18/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JwtConfig=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20redisTemplate=20=EC=A7=81=EB=A0=AC?= =?UTF-8?q?=ED=99=94=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95=20https://g?= =?UTF-8?q?ithub.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctauth/infrastructure/config/JwtConfig.kt | 12 +++++++- .../infrastructure/config/RedisConfig.kt | 30 +++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt index 3805baa..1457750 100644 --- a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt @@ -1,12 +1,14 @@ package com.chuseok22.ctauth.infrastructure.config import com.chuseok22.ctauth.infrastructure.jwt.JwtProvider +import com.chuseok22.ctauth.infrastructure.jwt.JwtStore import com.chuseok22.ctauth.infrastructure.properties.JwtProperties import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.security.Keys import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.core.RedisTemplate import javax.crypto.SecretKey @Configuration @@ -30,6 +32,14 @@ class JwtConfig( */ @Bean fun jwtProvider(jwtSecretKey: SecretKey): JwtProvider { - return JwtProvider(jwtSecretKey(), properties) + return JwtProvider(jwtSecretKey, properties) + } + + /** + * TokenStore 구현체 Bean 등록 + */ + @Bean + fun jwtStore(redisTemplate: RedisTemplate): JwtStore { + return JwtStore(redisTemplate) } } \ No newline at end of file diff --git a/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt index 439d704..95e4dc5 100644 --- a/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt +++ b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt @@ -26,12 +26,12 @@ class RedisConfig( * Redis Factory */ @Bean - fun redisConnectionFactory(redisProperties: RedisProperties): RedisConnectionFactory { + fun redisConnectionFactory(): RedisConnectionFactory { val config = RedisStandaloneConfiguration().apply { hostName = properties.host port = properties.port - setPassword(redisProperties.password) + setPassword(properties.password) } return LettuceConnectionFactory(config) @@ -42,26 +42,30 @@ class RedisConfig( */ @Bean fun redisTemplate( - connectionFactory: RedisConnectionFactory + factory: RedisConnectionFactory, + serializer: GenericJackson2JsonRedisSerializer ): RedisTemplate { - val objectMapper = createObjectMapper() - return RedisTemplate().apply { - this.connectionFactory = connectionFactory + val stringSerializer = StringRedisSerializer() - // Key Serializer: String - keySerializer = StringRedisSerializer() - hashKeySerializer = StringRedisSerializer() + return RedisTemplate().apply { + connectionFactory = factory - // Value Serializer: JSON - val jsonSerializer = GenericJackson2JsonRedisSerializer(objectMapper) - valueSerializer = jsonSerializer - hashValueSerializer = jsonSerializer + // 직렬화 설정 + keySerializer = stringSerializer + hashKeySerializer = stringSerializer + valueSerializer = serializer + hashValueSerializer = serializer afterPropertiesSet() } } + @Bean + fun redisSerializer(): GenericJackson2JsonRedisSerializer { + return GenericJackson2JsonRedisSerializer(createObjectMapper()) + } + /** * ObjectMapper 생성 */ From 36e80a3ec2737337c830503317c34b351d5277f7 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 16:42:13 +0900 Subject: [PATCH 19/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JPA=20Auditing=20=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=20https://github.com/CampusTable/campus-tabl?= =?UTF-8?q?e-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/chuseok22/ctweb/CampusTableServerApplication.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CT-web/src/main/kotlin/com/chuseok22/ctweb/CampusTableServerApplication.kt b/CT-web/src/main/kotlin/com/chuseok22/ctweb/CampusTableServerApplication.kt index 6583231..9a31c47 100644 --- a/CT-web/src/main/kotlin/com/chuseok22/ctweb/CampusTableServerApplication.kt +++ b/CT-web/src/main/kotlin/com/chuseok22/ctweb/CampusTableServerApplication.kt @@ -2,8 +2,10 @@ package com.chuseok22.ctweb import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.data.jpa.repository.config.EnableJpaAuditing @SpringBootApplication +@EnableJpaAuditing class CampusTableServerApplication fun main(args: Array) { From 77cb58f6e8c1556f75a2724180b23bacf962c603 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Fri, 9 Jan 2026 22:45:13 +0900 Subject: [PATCH 20/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20=EC=A0=84=EC=97=AD=20Clock?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20Bean=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20https://github.com/CampusTable/campus-tabl?= =?UTF-8?q?e-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctcommon/core/time/TimeProvider.kt | 8 +++++++ .../infrastructure/config/TimeConfig.kt | 21 +++++++++++++++++++ .../infrastructure/time/SystemTimeProvider.kt | 13 ++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 CT-common/src/main/kotlin/com/chuseok22/ctcommon/core/time/TimeProvider.kt create mode 100644 CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/config/TimeConfig.kt create mode 100644 CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/time/SystemTimeProvider.kt diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/core/time/TimeProvider.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/core/time/TimeProvider.kt new file mode 100644 index 0000000..aea2608 --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/core/time/TimeProvider.kt @@ -0,0 +1,8 @@ +package com.chuseok22.ctcommon.core.time + +import java.time.Instant + +interface TimeProvider { + + fun now(): Instant +} \ No newline at end of file diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/config/TimeConfig.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/config/TimeConfig.kt new file mode 100644 index 0000000..6c79a24 --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/config/TimeConfig.kt @@ -0,0 +1,21 @@ +package com.chuseok22.ctcommon.infrastructure.config + +import com.chuseok22.ctcommon.core.time.TimeProvider +import com.chuseok22.ctcommon.infrastructure.time.SystemTimeProvider +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.time.Clock + +@Configuration +class TimeConfig { + + @Bean + fun utcClock(): Clock { + return Clock.systemUTC() + } + + @Bean + fun timeProvider(clock: Clock): TimeProvider { + return SystemTimeProvider(clock) + } +} \ No newline at end of file diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/time/SystemTimeProvider.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/time/SystemTimeProvider.kt new file mode 100644 index 0000000..9d2dff1 --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/time/SystemTimeProvider.kt @@ -0,0 +1,13 @@ +package com.chuseok22.ctcommon.infrastructure.time + +import com.chuseok22.ctcommon.core.time.TimeProvider +import java.time.Clock +import java.time.Instant + +class SystemTimeProvider( + private val clock: Clock +) : TimeProvider { + override fun now(): Instant { + return Instant.now(clock) + } +} \ No newline at end of file From 0dcf5a438524ea766078a028deaadd658aa6b6dc Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Sun, 11 Jan 2026 20:42:45 +0900 Subject: [PATCH 21/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20AuthConstants.kt=20API=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=B6=94=EA=B0=80=20https://github.com/Ca?= =?UTF-8?q?mpusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chuseok22/ctauth/infrastructure/constant/AuthConstants.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt index ce085ff..caa31c9 100644 --- a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt @@ -16,4 +16,8 @@ object AuthConstants { const val REFRESH_TOKEN_CATEGORY: String = "refreshToken" const val REDIS_REFRESH_TOKEN_KEY_PREFIX: String = "RT:" + // API + const val API_REQUEST_PREFIX: String = "/api/" + const val ADMIN_REQUEST_PREFIX: String = "/admin/" + const val TEST_REQUEST_PREFIX: String = "/test/" } \ No newline at end of file From 53589da014396649cf88a8aa497f75369b209a9f Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Sun, 11 Jan 2026 20:43:50 +0900 Subject: [PATCH 22/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JPA=20Auditing=20=EC=9D=84?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20BaseEntity.kt=20=EC=B6=94=EC=83=81?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20https://gith?= =?UTF-8?q?ub.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/persistance/BaseEntity.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistance/BaseEntity.kt diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistance/BaseEntity.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistance/BaseEntity.kt new file mode 100644 index 0000000..48c5a81 --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistance/BaseEntity.kt @@ -0,0 +1,48 @@ +package com.chuseok22.ctcommon.infrastructure.persistance + +import jakarta.persistence.Column +import jakarta.persistence.EntityListeners +import jakarta.persistence.MappedSuperclass +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.Instant + +@MappedSuperclass +@EntityListeners(AuditingEntityListener::class) +abstract class BaseEntity { + + /** + * 생성일시 (UTC) + */ + @field:CreatedDate + @field:Column(name = "created_at", nullable = false, updatable = false, columnDefinition = "TIMESTAMPTZ") + var createdAt: Instant? = null + + /** + * 수정일시 (UTC) + */ + @field:LastModifiedDate + @field:Column(name = "updated_at", nullable = false, columnDefinition = "TIMESTAMPTZ") + var updatedAt: Instant? = null + + /** + * 삭제 여부 + */ + @field:Column(name = "is_deleted", nullable = false) + var isDeleted: Boolean = false + + /** + * 삭제일시 (UTC) + */ + @field:Column(name = "deleted_at", columnDefinition = "TIMESTAMPTZ") + var deletedAt: Instant? = null + + /** + * Soft Delete 실행 + */ + fun delete(now: Instant) { + this.isDeleted = true + deletedAt = now + } +} \ No newline at end of file From 6502ecb8e9d4b97f0235e6a297d7c590ac176a47 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 08:49:30 +0900 Subject: [PATCH 23/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20BaseEntity.kt=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?https://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/{persistance => persistence}/BaseEntity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/{persistance => persistence}/BaseEntity.kt (95%) diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistance/BaseEntity.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt similarity index 95% rename from CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistance/BaseEntity.kt rename to CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt index 48c5a81..f17ae42 100644 --- a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistance/BaseEntity.kt +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt @@ -1,4 +1,4 @@ -package com.chuseok22.ctcommon.infrastructure.persistance +package com.chuseok22.ctcommon.infrastructure.persistence import jakarta.persistence.Column import jakarta.persistence.EntityListeners From f338ad03bb38db13b21716109e66caf8de9f8ff4 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 08:49:46 +0900 Subject: [PATCH 24/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20Member.kt=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80=20https://github.com/Ca?= =?UTF-8?q?mpusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chuseok22/ctmember/core/constant/Role.kt | 6 +++ .../ctmember/infrastructure/entity/Member.kt | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 CT-member/src/main/kotlin/com/chuseok22/ctmember/core/constant/Role.kt create mode 100644 CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt diff --git a/CT-member/src/main/kotlin/com/chuseok22/ctmember/core/constant/Role.kt b/CT-member/src/main/kotlin/com/chuseok22/ctmember/core/constant/Role.kt new file mode 100644 index 0000000..9207db0 --- /dev/null +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/core/constant/Role.kt @@ -0,0 +1,6 @@ +package com.chuseok22.ctmember.core.constant + +enum class Role { + ROLE_USER, + ROLE_ADMIN +} \ No newline at end of file diff --git a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt new file mode 100644 index 0000000..bf4397d --- /dev/null +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt @@ -0,0 +1,52 @@ +package com.chuseok22.ctmember.infrastructure.entity + +import com.chuseok22.ctcommon.infrastructure.persistence.BaseEntity +import com.chuseok22.ctmember.core.constant.Role +import jakarta.persistence.* +import java.util.* + +@Entity +@Table(name = "member") +open class Member protected constructor() : BaseEntity() { + @field:Id + @field:GeneratedValue(strategy = GenerationType.UUID) + var id: UUID? = null + protected set + + @field:Column(name = "student_name", nullable = false, unique = true) + var studentName: String = "" + protected set + + @field:Column(name = "name", nullable = false) + var name: String = "" + protected set + + @field:Enumerated(EnumType.STRING) + @field:Column(name = "role", nullable = false) + var role: Role = Role.ROLE_USER + protected set + + private constructor(studentName: String, name: String, role: Role) : this() { + this.studentName = normalizeStudentName(studentName) + this.name = normalizeName(name) + this.role = role + } + + companion object { + fun create(studentName: String, name: String): Member { + return Member(studentName, name, Role.ROLE_USER) + } + + private fun normalizeStudentName(raw: String): String { + val normalized: String = raw.trim() + require(normalized.isNotBlank()) { "학번은 필수로 입력되어야 합니다 " } + return normalized + } + + private fun normalizeName(raw: String): String { + val normalized: String = raw.trim() + require(normalized.isNotBlank()) { "이름은 필수로 입력되어야 합니다" } + return normalized + } + } +} \ No newline at end of file From 9c1a4aa215fb6edf1ef538072388dc66d1b7fb51 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 08:51:10 +0900 Subject: [PATCH 25/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20MemberRepository.kt=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20https://github.com/CampusTable/campus-tabl?= =?UTF-8?q?e-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/repository/MemberRepository.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt diff --git a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt new file mode 100644 index 0000000..43301ca --- /dev/null +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt @@ -0,0 +1,9 @@ +package com.chuseok22.ctmember.infrastructure.repository + +import com.chuseok22.ctmember.infrastructure.entity.Member +import org.springframework.data.jpa.repository.JpaRepository +import java.util.* + +interface MemberRepository : JpaRepository { + fun findByStudentName(studentName: String) +} \ No newline at end of file From 3953d8acb852802f2dc1dfa33d21a523d684cc0c Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 08:54:10 +0900 Subject: [PATCH 26/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20[auth=EB=AA=A8=EB=93=88]=20?= =?UTF-8?q?member=20=EB=AA=A8=EB=93=88=20=EC=9D=98=EC=A1=B4=20https://gith?= =?UTF-8?q?ub.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CT-auth/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/CT-auth/build.gradle.kts b/CT-auth/build.gradle.kts index 8c7e7ba..5d742bf 100644 --- a/CT-auth/build.gradle.kts +++ b/CT-auth/build.gradle.kts @@ -13,6 +13,7 @@ tasks.jar { dependencies { implementation(project(":CT-common")) + implementation(project(":CT-member")) implementation(project(":CT-redis")) // Spring Security From 8c0b5b347817e51a3c4868444fb3145586f19f79 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 09:22:01 +0900 Subject: [PATCH 27/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20Member.kt=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=ED=95=84=EB=93=9C=20lateinit=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chuseok22/ctmember/infrastructure/entity/Member.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt index bf4397d..8e35f80 100644 --- a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt @@ -14,16 +14,16 @@ open class Member protected constructor() : BaseEntity() { protected set @field:Column(name = "student_name", nullable = false, unique = true) - var studentName: String = "" + lateinit var studentName: String protected set @field:Column(name = "name", nullable = false) - var name: String = "" + lateinit var name: String protected set @field:Enumerated(EnumType.STRING) @field:Column(name = "role", nullable = false) - var role: Role = Role.ROLE_USER + lateinit var role: Role protected set private constructor(studentName: String, name: String, role: Role) : this() { From 5ae411cd54c0efb590611767c89be1ab86f0d04d Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 09:22:21 +0900 Subject: [PATCH 28/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20MemberRepository.kt=20usern?= =?UTF-8?q?ame=20=EA=B8=B0=EB=B0=98=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20sof?= =?UTF-8?q?tDelete=20=EB=90=9C=20=ED=9A=8C=EC=9B=90=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=20https://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctmember/infrastructure/repository/MemberRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt index 43301ca..e7eed8c 100644 --- a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt @@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository import java.util.* interface MemberRepository : JpaRepository { - fun findByStudentName(studentName: String) + fun findByStudentNameAndDeletedFalse(studentName: String): Member? } \ No newline at end of file From f698aafac6bf805d19e79f15f1a2412e8bb72e4a Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 09:22:55 +0900 Subject: [PATCH 29/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20Spring=20Security=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=20https://github.com/CampusTable?= =?UTF-8?q?/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/user/CustomUserDetails.kt | 36 +++++++++++++++++++ .../user/CustomUserDetailsService.kt | 22 ++++++++++++ .../application/exception/ErrorCode.kt | 3 ++ 3 files changed, 61 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetailsService.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt new file mode 100644 index 0000000..deb4fd6 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt @@ -0,0 +1,36 @@ +package com.chuseok22.ctauth.infrastructure.user + +import com.chuseok22.ctauth.core.user.UserPrincipal +import com.chuseok22.ctmember.infrastructure.entity.Member +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import java.security.Principal + +class CustomUserDetails( + private val member: Member +) : UserDetails, UserPrincipal, Principal { + override fun getAuthorities(): Collection { + return listOf(SimpleGrantedAuthority(member.role.name)) + } + + override fun getPassword(): String { + return "" + } + + override fun getMemberId(): String { + return member.id.toString() + } + + override fun getUsername(): String { + return member.studentName + } + + override fun getRoles(): List { + return listOf(member.role.name) + } + + override fun getName(): String { + return member.name + } +} \ No newline at end of file diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetailsService.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetailsService.kt new file mode 100644 index 0000000..5114c41 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetailsService.kt @@ -0,0 +1,22 @@ +package com.chuseok22.ctauth.infrastructure.user + +import com.chuseok22.ctcommon.application.exception.CustomException +import com.chuseok22.ctcommon.application.exception.ErrorCode +import com.chuseok22.ctmember.infrastructure.repository.MemberRepository +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CustomUserDetailsService( + private val memberRepository: MemberRepository +) : UserDetailsService { + + @Transactional(readOnly = true) + override fun loadUserByUsername(username: String): UserDetails { + val member = memberRepository.findByStudentNameAndDeletedFalse(username) + ?: throw CustomException(ErrorCode.MEMBER_NOT_FOUND) + return CustomUserDetails(member) + } +} \ No newline at end of file diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt index 8d467df..371fe3c 100644 --- a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt @@ -14,4 +14,7 @@ enum class ErrorCode( // JWT INVALID_JWT(HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT"), + + // Member + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 없습니다"), } \ No newline at end of file From ce6ad6cde935627fd116e89e17e481d127f32b7c Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 09:23:11 +0900 Subject: [PATCH 30/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20Spring=20Security=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1=20https://github.com/CampusTable?= =?UTF-8?q?/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctauth/core/user/UserPrincipal.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/user/UserPrincipal.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/user/UserPrincipal.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/user/UserPrincipal.kt new file mode 100644 index 0000000..08960b3 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/user/UserPrincipal.kt @@ -0,0 +1,19 @@ +package com.chuseok22.ctauth.core.user + +interface UserPrincipal { + + /** + * 회원 고유 ID + */ + fun getMemberId(): String + + /** + * 로그인 ID + */ + fun getUsername(): String + + /** + * 사용자 권한 + */ + fun getRoles(): List +} \ No newline at end of file From 5edd14a0d76c4a0309a54a002df77abe7db2f22f Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 09:24:23 +0900 Subject: [PATCH 31/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20Member.kt=20Role=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chuseok22/ctmember/infrastructure/entity/Member.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt index 8e35f80..b994bd1 100644 --- a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/entity/Member.kt @@ -23,7 +23,7 @@ open class Member protected constructor() : BaseEntity() { @field:Enumerated(EnumType.STRING) @field:Column(name = "role", nullable = false) - lateinit var role: Role + var role: Role = Role.ROLE_USER protected set private constructor(studentName: String, name: String, role: Role) : this() { From 68e301fef237f0fb32a7c00002b55118cb8855d6 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 09:42:37 +0900 Subject: [PATCH 32/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JWT=20=EB=82=B4=EB=B6=80=20?= =?UTF-8?q?role=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=9C=EA=B1=B0=20https?= =?UTF-8?q?://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chuseok22/ctauth/core/token/TokenProvider.kt | 4 ++-- .../chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenProvider.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenProvider.kt index 8cedafa..8f7cacb 100644 --- a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenProvider.kt +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/core/token/TokenProvider.kt @@ -7,12 +7,12 @@ interface TokenProvider { /** * accessToken 생성 */ - fun createAccessToken(memberId: String, role: String): String + fun createAccessToken(memberId: String): String /** * refreshToken 생성 */ - fun createRefreshToken(memberId: String, role: String): String + fun createRefreshToken(memberId: String): String /** * 토큰 유효 검사 diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt index 2b91273..622121c 100644 --- a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt @@ -19,20 +19,18 @@ class JwtProvider( private val properties: JwtProperties ) : TokenProvider { - override fun createAccessToken(memberId: String, role: String): String { + override fun createAccessToken(memberId: String): String { return createToken( category = AuthConstants.ACCESS_TOKEN_CATEGORY, memberId = memberId, - role = role, expMillis = properties.accessExpMillis ).also { log.info { "엑세스 토큰 생성완료: memberId = $memberId" } } } - override fun createRefreshToken(memberId: String, role: String): String { + override fun createRefreshToken(memberId: String): String { return createToken( category = AuthConstants.REFRESH_TOKEN_CATEGORY, memberId = memberId, - role = role, expMillis = properties.refreshExpMillis ).also { log.info { "리프레시 토큰 생성완료: memberId = $memberId" } } } @@ -82,14 +80,12 @@ class JwtProvider( private fun createToken( category: String, memberId: String, - role: String, expMillis: Long ): String { val now = Instant.now() return Jwts.builder() .subject(memberId) .claim("category", category) - .claim("role", role) .issuer(properties.issuer) .issuedAt(Date.from(now)) .expiration(Date.from(now.plusMillis(expMillis))) From dd90e6d996e41c467307923e51d9aab88cf4cd07 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 09:42:54 +0900 Subject: [PATCH 33/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20MemberRepository.kt=20pk=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20https://github.com/CampusTable/campus-tabl?= =?UTF-8?q?e-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctmember/infrastructure/repository/MemberRepository.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt index e7eed8c..4a3f569 100644 --- a/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt @@ -5,5 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository import java.util.* interface MemberRepository : JpaRepository { + + fun findByIdAndDeletedFalse(memberId: UUID): Member? fun findByStudentNameAndDeletedFalse(studentName: String): Member? } \ No newline at end of file From 49535c75a0f6f5a29694950de31b8b516b944132 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 10:23:30 +0900 Subject: [PATCH 34/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20MemberService.kt=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20https://github.com/CampusTable/campus-tabl?= =?UTF-8?q?e-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctmember/application/MemberService.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CT-member/src/main/kotlin/com/chuseok22/ctmember/application/MemberService.kt diff --git a/CT-member/src/main/kotlin/com/chuseok22/ctmember/application/MemberService.kt b/CT-member/src/main/kotlin/com/chuseok22/ctmember/application/MemberService.kt new file mode 100644 index 0000000..a67ce2f --- /dev/null +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/application/MemberService.kt @@ -0,0 +1,25 @@ +package com.chuseok22.ctmember.application + +import com.chuseok22.ctcommon.application.exception.CustomException +import com.chuseok22.ctcommon.application.exception.ErrorCode +import com.chuseok22.ctmember.infrastructure.entity.Member +import com.chuseok22.ctmember.infrastructure.repository.MemberRepository +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.* + +private val log = KotlinLogging.logger { } + +@Service +class MemberService( + private val memberRepository: MemberRepository +) { + + @Transactional(readOnly = true) + fun findMemberById(memberId: UUID): Member { + return memberRepository.findByIdAndDeletedFalse(memberId) + ?: throw CustomException(ErrorCode.MEMBER_NOT_FOUND) + } + +} \ No newline at end of file From 0e59ed6f6728b9027040a4f2680e8bcfdce61b82 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 10:35:30 +0900 Subject: [PATCH 35/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JWT=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20TokenAuthenticationFilter.kt=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/TokenAuthenticationFilter.kt | 163 ++++++++++++++++++ .../application/exception/ErrorCode.kt | 1 + 2 files changed, 164 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt new file mode 100644 index 0000000..2e6ec8b --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt @@ -0,0 +1,163 @@ +package com.chuseok22.ctauth.infrastructure.filter + +import com.chuseok22.ctauth.core.token.TokenProvider +import com.chuseok22.ctauth.infrastructure.constant.AuthConstants +import com.chuseok22.ctauth.infrastructure.constant.SecurityUrls +import com.chuseok22.ctauth.infrastructure.user.CustomUserDetails +import com.chuseok22.ctauth.infrastructure.util.AuthUtil +import com.chuseok22.ctcommon.application.exception.CustomException +import com.chuseok22.ctcommon.application.exception.ErrorCode +import com.chuseok22.ctcommon.application.exception.ErrorResponse +import com.chuseok22.ctmember.application.MemberService +import com.chuseok22.ctmember.core.constant.Role +import com.chuseok22.ctmember.infrastructure.entity.Member +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.oshai.kotlinlogging.KotlinLogging +import io.jsonwebtoken.ExpiredJwtException +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.MediaType +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.util.AntPathMatcher +import org.springframework.web.filter.OncePerRequestFilter +import java.util.* + +private val log = KotlinLogging.logger { } + +class TokenAuthenticationFilter( + private val tokenProvider: TokenProvider, + private val memberService: MemberService, + private val objectMapper: ObjectMapper +) : OncePerRequestFilter() { + + private val pathMatcher = AntPathMatcher() + + override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { + + val uri = request.requestURI + val apiRequestType = determineApiRequestType(uri) + + if (isWhitelistedPath(uri)) { + filterChain.doFilter(request, response) + return + } + + try { + val token = AuthUtil.extractAccessTokenFromRequest(request) + ?: handleInvalidToken(null) + + if (tokenProvider.isValidToken(token)) { + handleValidToken( + request = request, + response = response, + filterChain = filterChain, + token = token, + apiRequestType = apiRequestType + ) + return + } else { + handleInvalidToken(token) + } + } catch (e: CustomException) { + SecurityContextHolder.clearContext() + log.error(e) { "[TokenAuthenticationFilter] CustomException 발생: ${e.message}" } + sendErrorResponse(response, e.errorCode) + return + } catch (e: ExpiredJwtException) { + SecurityContextHolder.clearContext() + log.error { "만료된 JWT: ${e.message}" } + sendErrorResponse(response, ErrorCode.EXPIRED_JWT) + return + } + } + + private fun isWhitelistedPath(uri: String): Boolean { + return SecurityUrls.AUTH_WHITELIST.any { pattern -> + pathMatcher.match(pattern, uri) + }.also { isWhitelisted -> + if (isWhitelisted) { + log.debug { "인증 생략 경로 요청입니다: $uri 인증을 건너뜁니다." } + } + } + } + + private fun determineApiRequestType(uri: String): ApiRequestType { + return when { + uri.startsWith(AuthConstants.API_REQUEST_PREFIX) -> ApiRequestType.API + uri.startsWith(AuthConstants.ADMIN_REQUEST_PREFIX) -> ApiRequestType.ADMIN + uri.startsWith(AuthConstants.TEST_REQUEST_PREFIX) -> ApiRequestType.TEST + else -> { + log.warn { "요청 uri가 정의되지 않은 API Type 입니다. 요청 URI: $uri" } + ApiRequestType.OTHER + } + } + } + + /** + * 유효한 JWT 토큰 처리 + */ + private fun handleValidToken(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain, token: String, apiRequestType: ApiRequestType) { + val memberId = tokenProvider.getMemberId(token) + val member = memberService.findMemberById(UUID.fromString(memberId)) + + // 관리자 경로 및 권한 검증 + assertAdminAuthenticated(member, apiRequestType) + + val customUserDetails = CustomUserDetails(member) + val authentication = UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.authorities) + + authentication.details = WebAuthenticationDetailsSource().buildDetails(request) + + // Security Context 등록 + SecurityContextHolder.getContext().authentication = authentication + + // 인증성공 + filterChain.doFilter(request, response) + } + + private fun handleInvalidToken(token: String?): Nothing { + when { + token.isNullOrBlank() -> { + log.error { "토큰이 존재하지 않습니다." } + throw CustomException(ErrorCode.UNAUTHORIZED) + } + + else -> { + log.error { "토큰이 유효하지 않습니다." } + throw CustomException(ErrorCode.INVALID_JWT) + } + } + } + + /** + * 관리자 API 접근 권한 체크 + */ + private fun assertAdminAuthenticated(member: Member, apiRequestType: ApiRequestType) { + if (apiRequestType == ApiRequestType.ADMIN && member.role != Role.ROLE_ADMIN) { + log.error { "관리자 권한이 없습니다" } + throw CustomException(ErrorCode.ACCESS_DENIED) + } + } + + private fun sendErrorResponse(response: HttpServletResponse, errorCode: ErrorCode) { + response.apply { + contentType = MediaType.APPLICATION_JSON_VALUE + status = errorCode.status.value() + characterEncoding = "UTF-8" + } + + val errorResponse = ErrorResponse( + errorCode = errorCode, + errorMessage = errorCode.message + ) + + objectMapper.writeValue(response.writer, errorResponse) + } + + private enum class ApiRequestType { + API, ADMIN, TEST, OTHER + } +} \ No newline at end of file diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt index 371fe3c..5b42ebc 100644 --- a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt @@ -14,6 +14,7 @@ enum class ErrorCode( // JWT INVALID_JWT(HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT"), + EXPIRED_JWT(HttpStatus.UNAUTHORIZED, "만료된 JWT"), // Member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 없습니다"), From 343279dc639fa48cfe8cede88ab083b8eeb35245 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 10:35:43 +0900 Subject: [PATCH 36/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20SecurityConfig.kt=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20https://github.com/CampusTable/campus-tabl?= =?UTF-8?q?e-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/config/SecurityConfig.kt | 58 +++++++++++++++++++ .../ctauth/infrastructure/util/AuthUtil.kt | 28 +++++++++ 2 files changed, 86 insertions(+) create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/SecurityConfig.kt create mode 100644 CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/util/AuthUtil.kt diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/SecurityConfig.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/SecurityConfig.kt new file mode 100644 index 0000000..f31dcb6 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/SecurityConfig.kt @@ -0,0 +1,58 @@ +package com.chuseok22.ctauth.infrastructure.config + +import com.chuseok22.ctauth.core.token.TokenProvider +import com.chuseok22.ctauth.infrastructure.constant.SecurityUrls +import com.chuseok22.ctauth.infrastructure.filter.TokenAuthenticationFilter +import com.chuseok22.ctmember.application.MemberService +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +@EnableWebSecurity +class SecurityConfig( + private val tokenProvider: TokenProvider, + private val memberService: MemberService, + private val objectMapper: ObjectMapper +) { + /** + * SecurityFilterChain 설정 + */ + @Bean + fun filterChain(http: HttpSecurity, tokenAuthenticationFilter: TokenAuthenticationFilter): SecurityFilterChain { + return http + .cors {} + .csrf { it.disable() } + .httpBasic { it.disable() } + .formLogin { it.disable() } + + .authorizeHttpRequests { authorize -> + authorize + // AUTH_WHITELIST 에 등록된 URL은 인증 허용 + .requestMatchers(*SecurityUrls.AUTH_WHITELIST.toTypedArray()).permitAll() + .anyRequest().authenticated() + } + + // 세션 설정 (STATELESS) + .sessionManagement { session -> + session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + + .addFilterBefore( + tokenAuthenticationFilter, + UsernamePasswordAuthenticationFilter::class.java + ) + .build() + } + + @Bean + fun tokenAuthenticationFilter(): TokenAuthenticationFilter { + return TokenAuthenticationFilter(tokenProvider, memberService, objectMapper) + } +} \ No newline at end of file diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/util/AuthUtil.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/util/AuthUtil.kt new file mode 100644 index 0000000..b460293 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/util/AuthUtil.kt @@ -0,0 +1,28 @@ +package com.chuseok22.ctauth.infrastructure.util + +import com.chuseok22.ctauth.infrastructure.constant.AuthConstants +import jakarta.servlet.http.HttpServletRequest + +object AuthUtil { + + /** + * HTTP 요청에서 accessToken 추출 + * - null or value 반환 (empty, blank 는 null 반환) + */ + fun extractAccessTokenFromRequest(request: HttpServletRequest): String? { + val bearerToken = request.getHeader(AuthConstants.HEADER_AUTHORIZATION) + return extractTokenWithoutBearer(bearerToken) + } + + fun getRefreshTokenTtlKey(memberId: String): String { + return "${AuthConstants.REDIS_REFRESH_TOKEN_KEY_PREFIX}$memberId" + } + + private fun extractTokenWithoutBearer(bearerToken: String?): String? { + return bearerToken + ?.takeIf { it.startsWith(AuthConstants.TOKEN_PREFIX) } + ?.removePrefix(AuthConstants.TOKEN_PREFIX) + ?.trim() + ?.takeIf { it.isNotBlank() } + } +} \ No newline at end of file From 33c93ca31f72ffb8359fc3db2c47045555f14bd6 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 11:00:08 +0900 Subject: [PATCH 37/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=20Boolean=20=ED=83=80=EC=9E=85=20=EB=84=A4=EC=9D=B4=EB=B0=8D?= =?UTF-8?q?=20is=20=EC=A0=91=EB=91=90=EC=82=AC=20=EC=A0=9C=EA=B1=B0=20http?= =?UTF-8?q?s://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ctcommon/infrastructure/persistence/BaseEntity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt index f17ae42..b5bc677 100644 --- a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt @@ -29,8 +29,8 @@ abstract class BaseEntity { /** * 삭제 여부 */ - @field:Column(name = "is_deleted", nullable = false) - var isDeleted: Boolean = false + @field:Column(name = "deleted", nullable = false) + var deleted: Boolean = false /** * 삭제일시 (UTC) @@ -42,7 +42,7 @@ abstract class BaseEntity { * Soft Delete 실행 */ fun delete(now: Instant) { - this.isDeleted = true + this.deleted = true deletedAt = now } } \ No newline at end of file From 7d7092cd2733f1442823a261e8cef1e933f74e82 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 11:01:27 +0900 Subject: [PATCH 38/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20cicd.yml=20=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EA=B1=B0=20=ED=91=B8=EC=8B=9C=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0=20https://g?= =?UTF-8?q?ithub.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/spring-boot-cicd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spring-boot-cicd.yml b/.github/workflows/spring-boot-cicd.yml index 92efedd..4de145a 100644 --- a/.github/workflows/spring-boot-cicd.yml +++ b/.github/workflows/spring-boot-cicd.yml @@ -3,7 +3,7 @@ name: spring-boot-cicd on: push: branches: -# - main + - main env: PROJECT_NAME: campus-table From 902caacc2df2c5d04c3613212a76fb46de3b9b05 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 11:04:52 +0900 Subject: [PATCH 39/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20jwt=20runtime=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20https://github.com/Ca?= =?UTF-8?q?mpusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CT-auth/build.gradle.kts | 8 +++++--- gradle/libs.versions.toml | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CT-auth/build.gradle.kts b/CT-auth/build.gradle.kts index 5d742bf..180ece5 100644 --- a/CT-auth/build.gradle.kts +++ b/CT-auth/build.gradle.kts @@ -18,12 +18,14 @@ dependencies { // Spring Security api(libs.spring.boot.starter.security) - api(libs.spring.security.test) + implementation(libs.spring.security.test) // JWT - api(libs.jjwt) + implementation(libs.jjwt.api) + runtimeOnly(libs.jjwt.impl) + runtimeOnly(libs.jjwt.jackson) // Sejong Portal Login - api(libs.sejong.portal.login) + implementation(libs.sejong.portal.login) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f12622..9ea040a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,9 @@ junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher swagger-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "swaggerUI" } # JJWT -jjwt = { module = "io.jsonwebtoken:jjwt", version.ref = "jjwt" } +jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jjwt" } +jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "jjwt" } +jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "jjwt" } # Kotlin Logging kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlinLogging" } From a38a36b39bc28e3c5ad02f31771f38c1b527fb7f Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 11:04:58 +0900 Subject: [PATCH 40/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20https://github.com/CampusTable?= =?UTF-8?q?/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/filter/TokenAuthenticationFilter.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt index 2e6ec8b..6a567a8 100644 --- a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt @@ -71,6 +71,11 @@ class TokenAuthenticationFilter( log.error { "만료된 JWT: ${e.message}" } sendErrorResponse(response, ErrorCode.EXPIRED_JWT) return + } catch (e: Exception) { + SecurityContextHolder.clearContext() + log.error { "인증 처리 중 예외 발생: ${e.message}" } + sendErrorResponse(response, ErrorCode.UNAUTHORIZED) + return } } From 9f76e0316ec62b6bd226262eb75a622cf21349ce Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 11:07:17 +0900 Subject: [PATCH 41/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20JWT=20=EB=82=B4=EB=B6=80=20?= =?UTF-8?q?memberId=20=EC=B6=94=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20https://github.com/CampusTable/campus-table-server/?= =?UTF-8?q?issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt index 622121c..860a9e6 100644 --- a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt @@ -60,7 +60,7 @@ class JwtProvider( override fun getMemberId(token: String): String { return try { - getClaims(token)["memberId"] as? String + getClaims(token).subject ?: throw CustomException(ErrorCode.INVALID_JWT) } catch (e: JwtException) { log.error(e) { "JWT memberId 추출 실패: ${e.message}" } From 26abfab63910c65243b3b93aa1ba9e2f19093fe2 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 11:08:35 +0900 Subject: [PATCH 42/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20CustomUserDetails.kt=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20memberId=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20htt?= =?UTF-8?q?ps://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt index deb4fd6..4e2fc82 100644 --- a/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt @@ -19,7 +19,8 @@ class CustomUserDetails( } override fun getMemberId(): String { - return member.id.toString() + return member.id?.toString() + ?: throw IllegalStateException("memberId가 초기화 되지 않았습니다") } override fun getUsername(): String { From 0ed5bee2473ec596ae92394676147f5ca6cd21d3 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 11:09:02 +0900 Subject: [PATCH 43/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20ErrorCode=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?https://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/chuseok22/ctcommon/application/exception/ErrorCode.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt index 5b42ebc..32f06fb 100644 --- a/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt @@ -17,5 +17,5 @@ enum class ErrorCode( EXPIRED_JWT(HttpStatus.UNAUTHORIZED, "만료된 JWT"), // Member - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 없습니다"), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다"), } \ No newline at end of file From 113df9b58ac24abb2978d1a543b08eafd390304b Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 11:10:02 +0900 Subject: [PATCH 44/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20objectMapper=20=ED=8A=B9?= =?UTF-8?q?=EC=A0=95=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20https://github.com/CampusTable/campus-tabl?= =?UTF-8?q?e-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chuseok22/ctredis/infrastructure/config/RedisConfig.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt index 95e4dc5..c4bdea5 100644 --- a/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt +++ b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt @@ -81,7 +81,9 @@ class RedisConfig( // 다형성 지원 activateDefaultTyping( BasicPolymorphicTypeValidator.builder() - .allowIfBaseType(Any::class.java) + .allowIfBaseType("com.chuseok22") + .allowIfSubType("java.util") + .allowIfSubType("java.time") .build(), ObjectMapper.DefaultTyping.NON_FINAL ) From b6bf452ef79a9866278a2e45c814a1f768565293 Mon Sep 17 00:00:00 2001 From: Chuseok22 Date: Mon, 12 Jan 2026 13:10:29 +0900 Subject: [PATCH 45/45] =?UTF-8?q?Spring=5FSecurity=5F=EA=B8=B0=EB=B3=B8=5F?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20:=20feat=20:=20Dockerfile=20.jar=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?https://github.com/CampusTable/campus-table-server/issues/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4c9bb27..25dadab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app RUN apk add --no-cache curl # 빌드된 JAR 파일을 복사 -COPY build/libs/campus-table-server-*.jar /app.jar +COPY CT-web/build/libs/CT-web-*.jar /app.jar # 애플리케이션 실행 (기본 Spring Boot 설정) ENTRYPOINT ["java", "-jar", "/app.jar"]