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 diff --git a/CT-auth/build.gradle.kts b/CT-auth/build.gradle.kts index b5c05c1..180ece5 100644 --- a/CT-auth/build.gradle.kts +++ b/CT-auth/build.gradle.kts @@ -13,8 +13,19 @@ tasks.jar { dependencies { implementation(project(":CT-common")) + implementation(project(":CT-member")) + implementation(project(":CT-redis")) + // Spring Security api(libs.spring.boot.starter.security) - api(libs.spring.security.test) + implementation(libs.spring.security.test) + + // JWT + implementation(libs.jjwt.api) + runtimeOnly(libs.jjwt.impl) + runtimeOnly(libs.jjwt.jackson) + + // Sejong Portal Login + implementation(libs.sejong.portal.login) } 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..8f7cacb --- /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): String + + /** + * refreshToken 생성 + */ + fun createRefreshToken(memberId: 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 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 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..1457750 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/config/JwtConfig.kt @@ -0,0 +1,45 @@ +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 +@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) + } + + /** + * TokenStore 구현체 Bean 등록 + */ + @Bean + fun jwtStore(redisTemplate: RedisTemplate): JwtStore { + return JwtStore(redisTemplate) + } +} \ No newline at end of file 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/constant/AuthConstants.kt b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt new file mode 100644 index 0000000..caa31c9 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/constant/AuthConstants.kt @@ -0,0 +1,23 @@ +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:" + + // 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 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 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..6a567a8 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/filter/TokenAuthenticationFilter.kt @@ -0,0 +1,168 @@ +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 + } catch (e: Exception) { + SecurityContextHolder.clearContext() + log.error { "인증 처리 중 예외 발생: ${e.message}" } + sendErrorResponse(response, ErrorCode.UNAUTHORIZED) + 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-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..860a9e6 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/jwt/JwtProvider.kt @@ -0,0 +1,105 @@ +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): String { + return createToken( + category = AuthConstants.ACCESS_TOKEN_CATEGORY, + memberId = memberId, + expMillis = properties.accessExpMillis + ).also { log.info { "엑세스 토큰 생성완료: memberId = $memberId" } } + } + + override fun createRefreshToken(memberId: String): String { + return createToken( + category = AuthConstants.REFRESH_TOKEN_CATEGORY, + memberId = memberId, + 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).subject + ?: 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, + expMillis: Long + ): String { + val now = Instant.now() + return Jwts.builder() + .subject(memberId) + .claim("category", category) + .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 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 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 +) 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..4e2fc82 --- /dev/null +++ b/CT-auth/src/main/kotlin/com/chuseok22/ctauth/infrastructure/user/CustomUserDetails.kt @@ -0,0 +1,37 @@ +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() + ?: throw IllegalStateException("memberId가 초기화 되지 않았습니다") + } + + 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-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 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/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..32f06fb --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/application/exception/ErrorCode.kt @@ -0,0 +1,21 @@ +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"), + EXPIRED_JWT(HttpStatus.UNAUTHORIZED, "만료된 JWT"), + + // Member + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다"), +} \ 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 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/persistence/BaseEntity.kt b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt new file mode 100644 index 0000000..b5bc677 --- /dev/null +++ b/CT-common/src/main/kotlin/com/chuseok22/ctcommon/infrastructure/persistence/BaseEntity.kt @@ -0,0 +1,48 @@ +package com.chuseok22.ctcommon.infrastructure.persistence + +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 = "deleted", nullable = false) + var deleted: Boolean = false + + /** + * 삭제일시 (UTC) + */ + @field:Column(name = "deleted_at", columnDefinition = "TIMESTAMPTZ") + var deletedAt: Instant? = null + + /** + * Soft Delete 실행 + */ + fun delete(now: Instant) { + this.deleted = true + deletedAt = now + } +} \ 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 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 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..b994bd1 --- /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) + lateinit var studentName: String + protected set + + @field:Column(name = "name", nullable = false) + lateinit 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 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..4a3f569 --- /dev/null +++ b/CT-member/src/main/kotlin/com/chuseok22/ctmember/infrastructure/repository/MemberRepository.kt @@ -0,0 +1,11 @@ +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 findByIdAndDeletedFalse(memberId: UUID): Member? + fun findByStudentNameAndDeletedFalse(studentName: String): Member? +} \ No newline at end of file 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/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..c4bdea5 --- /dev/null +++ b/CT-redis/src/main/kotlin/com/chuseok22/ctredis/infrastructure/config/RedisConfig.kt @@ -0,0 +1,92 @@ +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(): RedisConnectionFactory { + + val config = RedisStandaloneConfiguration().apply { + hostName = properties.host + port = properties.port + setPassword(properties.password) + } + + return LettuceConnectionFactory(config) + } + + /** + * RedisTemplate + */ + @Bean + fun redisTemplate( + factory: RedisConnectionFactory, + serializer: GenericJackson2JsonRedisSerializer + ): RedisTemplate { + + val stringSerializer = StringRedisSerializer() + + return RedisTemplate().apply { + connectionFactory = factory + + // 직렬화 설정 + keySerializer = stringSerializer + hashKeySerializer = stringSerializer + valueSerializer = serializer + hashValueSerializer = serializer + + afterPropertiesSet() + } + } + + @Bean + fun redisSerializer(): GenericJackson2JsonRedisSerializer { + return GenericJackson2JsonRedisSerializer(createObjectMapper()) + } + + /** + * 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("com.chuseok22") + .allowIfSubType("java.util") + .allowIfSubType("java.time") + .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 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) { 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 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 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 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") 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"] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a0388e..9ea040a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,11 @@ [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" sejongPortalLogin = "1.0.0" httpLogging = "0.0.9" @@ -43,6 +45,14 @@ 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-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" } + # Chuseok22 ApiChangeLog api-change-log = { module = "com.chuseok22:ApiChangeLog", version.ref = "apiChangeLog" } 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