From a2050cd2cb137c3a1277a60f69b2f8377cfb2176 Mon Sep 17 00:00:00 2001 From: hod Date: Mon, 2 Feb 2026 21:14:06 +0900 Subject: [PATCH 01/10] configure application profiles and secrets management --- .github/workflows/be-deploy.yml | 4 ++- .gitmodules | 3 ++ build.gradle.kts | 7 ++++- gradlew | 0 src/main/kotlin/com/moa/Application.kt | 2 ++ src/main/resources/application-local.yml | 35 ++++++++++++++++++++++++ src/main/resources/application-prod.yml | 27 ++++++++++++++++++ src/main/resources/application.yaml | 3 -- src/main/resources/application.yml | 6 ++++ src/main/resources/moa-secret | 1 + src/test/resources/application.yaml | 14 ++++++++++ 11 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 .gitmodules mode change 100644 => 100755 gradlew create mode 100644 src/main/resources/application-local.yml create mode 100644 src/main/resources/application-prod.yml delete mode 100644 src/main/resources/application.yaml create mode 100644 src/main/resources/application.yml create mode 160000 src/main/resources/moa-secret create mode 100644 src/test/resources/application.yaml diff --git a/.github/workflows/be-deploy.yml b/.github/workflows/be-deploy.yml index 030d179..206eb92 100644 --- a/.github/workflows/be-deploy.yml +++ b/.github/workflows/be-deploy.yml @@ -22,6 +22,8 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + submodules: recursive + token: ${{ secrets.BE_SUBMODULE_TOKEN }} - name: Calculate version id: calc @@ -94,5 +96,5 @@ jobs: --name moa-test-server \ --restart unless-stopped \ -p 8080:8080 \ - -e PROFILE= \ + -e PROFILE=prod\ -d godqhr721/moa_server:${{ needs.setup.outputs.docker_tag }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b1e5601 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/main/resources/moa-secret"] + path = src/main/resources/moa-secret + url = https://github.com/subsub97/moa-secret diff --git a/build.gradle.kts b/build.gradle.kts index 03891b8..68e873b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,7 +31,12 @@ dependencies { // db implementation("org.springframework.boot:spring-boot-h2console") runtimeOnly("com.h2database:h2") - // runtimeOnly("com.mysql:mysql-connector-j") + runtimeOnly("com.mysql:mysql-connector-j") + + //jwt + implementation("io.jsonwebtoken:jjwt-api:0.13.0") + implementation("io.jsonwebtoken:jjwt-impl:0.13.0") + implementation("io.jsonwebtoken:jjwt-jackson:0.13.0") // etc implementation("org.jetbrains.kotlin:kotlin-reflect") diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/kotlin/com/moa/Application.kt b/src/main/kotlin/com/moa/Application.kt index 247c13e..a87e2c6 100644 --- a/src/main/kotlin/com/moa/Application.kt +++ b/src/main/kotlin/com/moa/Application.kt @@ -1,9 +1,11 @@ package com.moa import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication @SpringBootApplication +@ConfigurationPropertiesScan class Application fun main(args: Array) { diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..1dceafe --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,35 @@ +spring: + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + show_sql: true + defer-datasource-initialization: true + h2: + console: + enabled: true + path: /h2-console + + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:core;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + pool-name: moa-db-pool + + sql: + init: + mode: always + data-locations: classpath:data-local.sql + +jwt: + secret-key: ${jwt.local.access.secret-key} + expiration-milliseconds: ${jwt.local.access.expiration} + +oidc: + kakao: + jwks-uri: ${oauth.kakao.jwks-uri} + issuer: ${oauth.kakao.issuer} + audience: ${oauth.kakao.audience} + cache-ttl-seconds: ${oauth.cache-ttl-seconds} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..835a012 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,27 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${db.prod.host}:${db.prod.port}/${db.prod.name}?serverTimezone=UTC&useUniCode=yes&characterEncoding=UTF-8 + username: ${db.prod.username} + password: ${db.prod.password} + pool-name: moa-db-pool + + jpa: + database-platform: org.hibernate.dialect.MySQLDialect + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + show_sql: true + +jwt: + secret-key: ${jwt.prod.access.secret-key} + expiration-milliseconds: ${jwt.prod.access.expiration} + +oidc: + kakao: + jwks-uri: ${ouath.kakao.jwks-uri} + issuer: ${ouath.kakao.issuer} + audience: ${ouath.kakao.audience} + cache-ttl-seconds: ${oauth.cache-ttl-seconds} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml deleted file mode 100644 index c226a2c..0000000 --- a/src/main/resources/application.yaml +++ /dev/null @@ -1,3 +0,0 @@ -spring: - application: - name: demo diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..13ccc23 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,6 @@ +spring: + config: + import: + - classpath:/moa-secret/oauth.yml + - classpath:/moa-secret/jwt.yml + - classpath:/moa-secret/db.yml diff --git a/src/main/resources/moa-secret b/src/main/resources/moa-secret new file mode 160000 index 0000000..c307feb --- /dev/null +++ b/src/main/resources/moa-secret @@ -0,0 +1 @@ +Subproject commit c307feb71657aed46363f38cf92b8f6f3d493c03 diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml new file mode 100644 index 0000000..5ef034a --- /dev/null +++ b/src/test/resources/application.yaml @@ -0,0 +1,14 @@ +spring: + application: + name: demo + +jwt: + secret-key: moa-temporary-secret-key-for-testing-minimum-256-bits-required + expiration-milliseconds: 3600000 + +oidc: + kakao: + jwks-uri: https://kauth.kakao.com/.well-known/jwks.json + issuer: https://kauth.kakao.com + audience: test-app-key + cache-ttl-seconds: 3600 From 21dd3f892219c7c7ee3a55168a4daaeb68af6c42 Mon Sep 17 00:00:00 2001 From: hod Date: Mon, 2 Feb 2026 21:17:20 +0900 Subject: [PATCH 02/10] add Member Entity and Associate existing entities with Member --- src/main/kotlin/com/moa/entity/Member.kt | 21 +++++++++ src/main/kotlin/com/moa/entity/Profile.kt | 3 ++ src/main/kotlin/com/moa/entity/Term.kt | 3 ++ .../com/moa/repository/MemberRepository.kt | 9 ++++ .../com/moa/repository/ProfileRepository.kt | 4 +- .../com/moa/repository/TermRepository.kt | 4 +- .../moa/service/OnboardingStatusService.kt | 13 ++---- .../kotlin/com/moa/service/PayrollService.kt | 6 +-- .../kotlin/com/moa/service/ProfileService.kt | 6 +-- .../kotlin/com/moa/service/TermsService.kt | 46 +++++++++++-------- .../com/moa/service/WorkPolicyService.kt | 6 +-- 11 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 src/main/kotlin/com/moa/entity/Member.kt create mode 100644 src/main/kotlin/com/moa/repository/MemberRepository.kt diff --git a/src/main/kotlin/com/moa/entity/Member.kt b/src/main/kotlin/com/moa/entity/Member.kt new file mode 100644 index 0000000..8144180 --- /dev/null +++ b/src/main/kotlin/com/moa/entity/Member.kt @@ -0,0 +1,21 @@ +package com.moa.entity + +import com.moa.service.auth.oidc.ProviderType +import jakarta.persistence.* + +@Entity +class Member( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + + @Enumerated(EnumType.STRING) + val provider: ProviderType, + + val providerSubject: String, + + @OneToOne(fetch = FetchType.LAZY) + val profile: Profile?, + + ) : BaseEntity() { +} diff --git a/src/main/kotlin/com/moa/entity/Profile.kt b/src/main/kotlin/com/moa/entity/Profile.kt index 7f4d987..1d0399b 100644 --- a/src/main/kotlin/com/moa/entity/Profile.kt +++ b/src/main/kotlin/com/moa/entity/Profile.kt @@ -4,6 +4,9 @@ import jakarta.persistence.* @Entity class Profile( + @Column(nullable = false, unique = true) + val memberId: Long, + @Column(nullable = false) var nickname: String, diff --git a/src/main/kotlin/com/moa/entity/Term.kt b/src/main/kotlin/com/moa/entity/Term.kt index 7a1b341..c2bf434 100644 --- a/src/main/kotlin/com/moa/entity/Term.kt +++ b/src/main/kotlin/com/moa/entity/Term.kt @@ -17,4 +17,7 @@ class Term( @Column(nullable = false) val contentUrl: String, + + @Column(nullable = false) + val active: Boolean = true, ) diff --git a/src/main/kotlin/com/moa/repository/MemberRepository.kt b/src/main/kotlin/com/moa/repository/MemberRepository.kt new file mode 100644 index 0000000..820dcf2 --- /dev/null +++ b/src/main/kotlin/com/moa/repository/MemberRepository.kt @@ -0,0 +1,9 @@ +package com.moa.repository + +import com.moa.entity.Member +import com.moa.service.auth.oidc.ProviderType +import org.springframework.data.jpa.repository.JpaRepository + +interface MemberRepository : JpaRepository { + fun findByProviderAndProviderSubject(provider: ProviderType, providerSubject: String): Member? +} diff --git a/src/main/kotlin/com/moa/repository/ProfileRepository.kt b/src/main/kotlin/com/moa/repository/ProfileRepository.kt index 56dcf8c..22c0a69 100644 --- a/src/main/kotlin/com/moa/repository/ProfileRepository.kt +++ b/src/main/kotlin/com/moa/repository/ProfileRepository.kt @@ -3,4 +3,6 @@ package com.moa.repository import com.moa.entity.Profile import org.springframework.data.jpa.repository.JpaRepository -interface ProfileRepository : JpaRepository +interface ProfileRepository : JpaRepository { + fun findByMemberId(memberId: Long): Profile? +} diff --git a/src/main/kotlin/com/moa/repository/TermRepository.kt b/src/main/kotlin/com/moa/repository/TermRepository.kt index 7dd2a90..cacf944 100644 --- a/src/main/kotlin/com/moa/repository/TermRepository.kt +++ b/src/main/kotlin/com/moa/repository/TermRepository.kt @@ -3,4 +3,6 @@ package com.moa.repository import com.moa.entity.Term import org.springframework.data.jpa.repository.JpaRepository -interface TermRepository : JpaRepository +interface TermRepository : JpaRepository { + fun findAllByActiveTrue(): List +} diff --git a/src/main/kotlin/com/moa/service/OnboardingStatusService.kt b/src/main/kotlin/com/moa/service/OnboardingStatusService.kt index 479d22e..2b0d388 100644 --- a/src/main/kotlin/com/moa/service/OnboardingStatusService.kt +++ b/src/main/kotlin/com/moa/service/OnboardingStatusService.kt @@ -16,15 +16,10 @@ class OnboardingStatusService( private val workPolicyDayPolicyRepository: WorkPolicyDayPolicyRepository, ) { - // TODO. 로그인/Member 연동 후 memberId 가져오도록 변경 - private fun currentMemberId(): Long = 1L - @Transactional(readOnly = true) - fun getStatus(today: LocalDate = LocalDate.now()): OnboardingStatusResponse { - val memberId = currentMemberId() - + fun getStatus(memberId: Long, today: LocalDate = LocalDate.now()): OnboardingStatusResponse { // 필수 약관 동의 여부 - val requiredCodes = termRepository.findAll() + val requiredCodes = termRepository.findAllByActiveTrue() .filter { it.required } .map { it.code } .toSet() @@ -34,8 +29,8 @@ class OnboardingStatusService( val hasRequiredTermsAgreed = requiredCodes.all { agreements[it] == true } - // 프로필 완료 여부 (임시 stub) - val profile = profileRepository.findAll().firstOrNull() + // 프로필 완료 여부 + val profile = profileRepository.findByMemberId(memberId) val profileCompleted = profile != null && profile.nickname.isNotBlank() && profile.workplaceName.isNotBlank() diff --git a/src/main/kotlin/com/moa/service/PayrollService.kt b/src/main/kotlin/com/moa/service/PayrollService.kt index 9535c1a..2e0664d 100644 --- a/src/main/kotlin/com/moa/service/PayrollService.kt +++ b/src/main/kotlin/com/moa/service/PayrollService.kt @@ -14,12 +14,8 @@ class PayrollService( private val payrollVersionRepository: PayrollVersionRepository, ) { - // TODO. 로그인/Member 연동 후 memberId 가져오도록 변경 - private fun currentMemberId(): Long = 1L - @Transactional - fun upsert(req: PayrollUpsertRequest): PayrollResponse { - val memberId = currentMemberId() + fun upsert(memberId: Long, req: PayrollUpsertRequest): PayrollResponse { val effectiveFrom = req.effectiveFrom val salaryInputType = req.salaryInputType diff --git a/src/main/kotlin/com/moa/service/ProfileService.kt b/src/main/kotlin/com/moa/service/ProfileService.kt index b940388..6b79ec9 100644 --- a/src/main/kotlin/com/moa/service/ProfileService.kt +++ b/src/main/kotlin/com/moa/service/ProfileService.kt @@ -14,16 +14,16 @@ class ProfileService( ) { @Transactional - fun upsertProfile(req: ProfileUpsertRequest): ProfileResponse { + fun upsertProfile(memberId: Long, req: ProfileUpsertRequest): ProfileResponse { val nickname = req.nickname val workplaceName = req.workplace.name - // TODO. Member 연동 후 member.profile = profile 방식으로 1:1 연결 예정 - val profile = profileRepository.findAll().firstOrNull()?.apply { + val profile = profileRepository.findByMemberId(memberId)?.apply { this.nickname = nickname this.workplaceName = workplaceName } ?: profileRepository.save( Profile( + memberId = memberId, nickname = nickname, workplaceName = workplaceName, ) diff --git a/src/main/kotlin/com/moa/service/TermsService.kt b/src/main/kotlin/com/moa/service/TermsService.kt index 8fd818e..b293254 100644 --- a/src/main/kotlin/com/moa/service/TermsService.kt +++ b/src/main/kotlin/com/moa/service/TermsService.kt @@ -16,12 +16,9 @@ class TermsService( private val termAgreementRepository: TermAgreementRepository, ) { - // TODO. 로그인/Member 연동 후 memberId 가져오도록 변경 - private fun currentMemberId(): Long = 1L - @Transactional(readOnly = true) fun getTerms(): TermsResponse { - val terms = termRepository.findAll() + val terms = termRepository.findAllByActiveTrue() .sortedWith(compareByDescending { it.required }.thenBy { it.code }) .map { TermDto( @@ -35,26 +32,41 @@ class TermsService( return TermsResponse(terms = terms) } - @Transactional(readOnly = true) - fun getAgreements(): TermsAgreementsResponse { - val memberId = currentMemberId() - - val terms = termRepository.findAll() + @Transactional + fun getAgreements(memberId: Long): TermsAgreementsResponse { + val terms = termRepository.findAllByActiveTrue() val requiredCodes = terms.filter { it.required }.map { it.code }.toSet() - val agreements = termAgreementRepository.findAllByMemberId(memberId) - .associate { it.termCode to it.agreed } + val existingAgreements = termAgreementRepository.findAllByMemberId(memberId) + .associateBy { it.termCode } + + // Lazy create TermAgreement for new terms + val newAgreements = terms + .filter { !existingAgreements.containsKey(it.code) } + .map { term -> + TermAgreement( + memberId = memberId, + termCode = term.code, + agreed = false, + ) + } + + if (newAgreements.isNotEmpty()) { + termAgreementRepository.saveAll(newAgreements) + } + + val allAgreements = existingAgreements + newAgreements.associateBy { it.termCode } val responseAgreements = terms .sortedWith(compareByDescending { it.required }.thenBy { it.code }) .map { term -> TermAgreementDto( code = term.code, - agreed = agreements[term.code] == true, + agreed = allAgreements[term.code]?.agreed == true, ) } - val hasRequiredTermsAgreed = requiredCodes.all { agreements[it] == true } + val hasRequiredTermsAgreed = requiredCodes.all { allAgreements[it]?.agreed == true } return TermsAgreementsResponse( agreements = responseAgreements, @@ -63,10 +75,8 @@ class TermsService( } @Transactional - fun upsertAgreements(req: TermsAgreementRequest): TermsAgreementsResponse { - val memberId = currentMemberId() - - val terms = termRepository.findAll() + fun upsertAgreements(memberId: Long, req: TermsAgreementRequest): TermsAgreementsResponse { + val terms = termRepository.findAllByActiveTrue() val termByCode = terms.associateBy { it.code } val requiredCodes = terms.filter { it.required }.map { it.code }.toSet() @@ -100,6 +110,6 @@ class TermsService( } } - return getAgreements() + return getAgreements(memberId) } } diff --git a/src/main/kotlin/com/moa/service/WorkPolicyService.kt b/src/main/kotlin/com/moa/service/WorkPolicyService.kt index 753a752..e662a60 100644 --- a/src/main/kotlin/com/moa/service/WorkPolicyService.kt +++ b/src/main/kotlin/com/moa/service/WorkPolicyService.kt @@ -19,12 +19,8 @@ class WorkPolicyService( private val dayPolicyRepository: WorkPolicyDayPolicyRepository, ) { - // TODO. 로그인/Member 연동 후 memberId 가져오도록 변경 - private fun currentMemberId(): Long = 1L - @Transactional - fun upsert(req: WorkPolicyUpsertRequest): WorkPolicyResponse { - val memberId = currentMemberId() + fun upsert(memberId: Long, req: WorkPolicyUpsertRequest): WorkPolicyResponse { validateDays(req.days) From dcd3830adcc1f424dfc24a802c3dd8d4056379bc Mon Sep 17 00:00:00 2001 From: hod Date: Mon, 2 Feb 2026 21:20:35 +0900 Subject: [PATCH 03/10] Implement JWT authentication feature --- .../com/moa/common/auth/AuthConstants.kt | 5 ++ .../moa/common/auth/AuthenticatedMember.kt | 9 +++ .../auth/AuthenticatedMemberResolver.kt | 34 +++++++++ .../com/moa/common/auth/JwtTokenProvider.kt | 63 +++++++++++++++++ .../kotlin/com/moa/common/config/WebConfig.kt | 16 +++++ .../common/filter/JwtAuthenticationFilter.kt | 70 +++++++++++++++++++ .../moa/controller/OnboardingController.kt | 42 ++++++----- 7 files changed, 221 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/com/moa/common/auth/AuthConstants.kt create mode 100644 src/main/kotlin/com/moa/common/auth/AuthenticatedMember.kt create mode 100644 src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt create mode 100644 src/main/kotlin/com/moa/common/auth/JwtTokenProvider.kt create mode 100644 src/main/kotlin/com/moa/common/config/WebConfig.kt create mode 100644 src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt diff --git a/src/main/kotlin/com/moa/common/auth/AuthConstants.kt b/src/main/kotlin/com/moa/common/auth/AuthConstants.kt new file mode 100644 index 0000000..d7460a5 --- /dev/null +++ b/src/main/kotlin/com/moa/common/auth/AuthConstants.kt @@ -0,0 +1,5 @@ +package com.moa.common.auth + +object AuthConstants { + const val CURRENT_MEMBER_ID = "currentMemberId" +} diff --git a/src/main/kotlin/com/moa/common/auth/AuthenticatedMember.kt b/src/main/kotlin/com/moa/common/auth/AuthenticatedMember.kt new file mode 100644 index 0000000..3c64d5b --- /dev/null +++ b/src/main/kotlin/com/moa/common/auth/AuthenticatedMember.kt @@ -0,0 +1,9 @@ +package com.moa.common.auth + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class AuthenticatedMember + +data class AuthenticatedMemberInfo( + val id: Long, +) diff --git a/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt b/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt new file mode 100644 index 0000000..9a21ee6 --- /dev/null +++ b/src/main/kotlin/com/moa/common/auth/AuthenticatedMemberResolver.kt @@ -0,0 +1,34 @@ +package com.moa.common.auth + +import com.moa.common.exception.BadRequestException +import com.moa.common.exception.ErrorCode +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.context.request.RequestAttributes +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class AuthenticatedMemberResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(AuthenticatedMember::class.java) && + parameter.parameterType == AuthenticatedMemberInfo::class.java + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): AuthenticatedMemberInfo { + val memberId = webRequest.getAttribute( + AuthConstants.CURRENT_MEMBER_ID, + RequestAttributes.SCOPE_REQUEST + ) as? Long ?: throw BadRequestException(ErrorCode.INVALID_ID_TOKEN) + + return AuthenticatedMemberInfo(id = memberId) + } +} diff --git a/src/main/kotlin/com/moa/common/auth/JwtTokenProvider.kt b/src/main/kotlin/com/moa/common/auth/JwtTokenProvider.kt new file mode 100644 index 0000000..98e2215 --- /dev/null +++ b/src/main/kotlin/com/moa/common/auth/JwtTokenProvider.kt @@ -0,0 +1,63 @@ +package com.moa.common.auth + +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.Keys +import jakarta.servlet.http.HttpServletRequest +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.nio.charset.StandardCharsets +import java.time.Instant +import java.util.* + +@Component +class JwtTokenProvider( + @Value("\${jwt.secret-key}") + private val accessTokenSecretKey: String, + + @Value("\${jwt.expiration-milliseconds}") + private val accessTokenExpirationInMilliseconds: Long, +) { + + private val accessKey = Keys.hmacShaKeyFor(accessTokenSecretKey.toByteArray(StandardCharsets.UTF_8)) + + fun createAccessToken(userId: Long): String { + val now = Instant.now() + val expiryDate = now.plusMillis(accessTokenExpirationInMilliseconds) + + return Jwts.builder() + .subject(userId.toString()) + .issuedAt(Date.from(now)) + .expiration(Date.from(expiryDate)) + .signWith(accessKey) + .compact() + } + + fun extractToken(request: HttpServletRequest): String? { + val bearerToken = request.getHeader("Authorization") + return if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + bearerToken.substring(7) + } else null + } + + fun getUserIdFromToken(token: String): Long? { + return getClaims(token).subject.toLong() + } + + fun validateToken(token: String): Boolean { + return try { + getClaims(token) + true + } catch (ex: Exception) { + false + } + } + + private fun getClaims(token: String): Claims { + return Jwts.parser() + .verifyWith(accessKey) + .build() + .parseSignedClaims(token) + .payload + } +} diff --git a/src/main/kotlin/com/moa/common/config/WebConfig.kt b/src/main/kotlin/com/moa/common/config/WebConfig.kt new file mode 100644 index 0000000..64b3c8c --- /dev/null +++ b/src/main/kotlin/com/moa/common/config/WebConfig.kt @@ -0,0 +1,16 @@ +package com.moa.common.config + +import com.moa.common.auth.AuthenticatedMemberResolver +import org.springframework.context.annotation.Configuration +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig( + private val authenticatedMemberResolver: AuthenticatedMemberResolver, +) : WebMvcConfigurer { + + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(authenticatedMemberResolver) + } +} diff --git a/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..4cfc141 --- /dev/null +++ b/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt @@ -0,0 +1,70 @@ +package com.moa.common.filter + +import com.moa.common.auth.AuthConstants +import com.moa.common.auth.JwtTokenProvider +import com.moa.common.exception.ErrorCode +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtAuthenticationFilter( + private val jwtTokenProvider: JwtTokenProvider, +) : OncePerRequestFilter() { + + companion object { + private val EXCLUDED_PATHS = listOf( + "/api/v1/auth", + "/health", + "/h2-console", + "/actuator", + ) + } + + override fun shouldNotFilter(request: HttpServletRequest): Boolean { + val path = request.requestURI + return EXCLUDED_PATHS.any { path.startsWith(it) } + } + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + val token = jwtTokenProvider.extractToken(request) + + if (token == null || !jwtTokenProvider.validateToken(token)) { + writeUnauthorizedResponse(response) + return + } + + val memberId = jwtTokenProvider.getUserIdFromToken(token) + if (memberId == null) { + writeUnauthorizedResponse(response) + return + } + + request.setAttribute(AuthConstants.CURRENT_MEMBER_ID, memberId) + filterChain.doFilter(request, response) + } + + private fun writeUnauthorizedResponse(response: HttpServletResponse) { + response.status = HttpServletResponse.SC_UNAUTHORIZED + response.contentType = MediaType.APPLICATION_JSON_VALUE + response.characterEncoding = "UTF-8" + + val errorCode = ErrorCode.UNAUTHORIZED + val body = """ + { + "code": "${errorCode.code}", + "message": "${errorCode.message}", + "content": null + } + """.trimIndent() + + response.writer.write(body) + } +} diff --git a/src/main/kotlin/com/moa/controller/OnboardingController.kt b/src/main/kotlin/com/moa/controller/OnboardingController.kt index e6d4568..e715c6b 100644 --- a/src/main/kotlin/com/moa/controller/OnboardingController.kt +++ b/src/main/kotlin/com/moa/controller/OnboardingController.kt @@ -1,11 +1,9 @@ package com.moa.controller +import com.moa.common.auth.AuthenticatedMember +import com.moa.common.auth.AuthenticatedMemberInfo import com.moa.common.response.ApiResponse -import com.moa.service.OnboardingStatusService -import com.moa.service.PayrollService -import com.moa.service.ProfileService -import com.moa.service.TermsService -import com.moa.service.WorkPolicyService +import com.moa.service.* import com.moa.service.dto.PayrollUpsertRequest import com.moa.service.dto.ProfileUpsertRequest import com.moa.service.dto.TermsAgreementRequest @@ -14,7 +12,7 @@ import jakarta.validation.Valid import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/v1/onboarding") +@RequestMapping("/api/v1/onboarding") class OnboardingController( private val onboardingStatusService: OnboardingStatusService, private val profileService: ProfileService, @@ -24,30 +22,38 @@ class OnboardingController( ) { @GetMapping("/status") - fun status() = - ApiResponse.success(onboardingStatusService.getStatus()) + fun status(@AuthenticatedMember member: AuthenticatedMemberInfo) = + ApiResponse.success(onboardingStatusService.getStatus(member.id)) @PatchMapping("/profile") - fun upsertProfile(@RequestBody @Valid req: ProfileUpsertRequest) = - ApiResponse.success(profileService.upsertProfile(req)) + fun upsertProfile( + @AuthenticatedMember member: AuthenticatedMemberInfo, + @RequestBody @Valid req: ProfileUpsertRequest, + ) = ApiResponse.success(profileService.upsertProfile(member.id, req)) @PatchMapping("/payroll") - fun upsertPayroll(@RequestBody @Valid req: PayrollUpsertRequest) = - ApiResponse.success(payrollService.upsert(req)) + fun upsertPayroll( + @AuthenticatedMember member: AuthenticatedMemberInfo, + @RequestBody @Valid req: PayrollUpsertRequest, + ) = ApiResponse.success(payrollService.upsert(member.id, req)) @PatchMapping("/work-policy") - fun upsertWorkPolicy(@RequestBody @Valid req: WorkPolicyUpsertRequest) = - ApiResponse.success(workPolicyService.upsert(req)) + fun upsertWorkPolicy( + @AuthenticatedMember member: AuthenticatedMemberInfo, + @RequestBody @Valid req: WorkPolicyUpsertRequest, + ) = ApiResponse.success(workPolicyService.upsert(member.id, req)) @GetMapping("/terms") fun terms() = ApiResponse.success(termsService.getTerms()) @GetMapping("/terms/agreements") - fun agreements() = - ApiResponse.success(termsService.getAgreements()) + fun agreements(@AuthenticatedMember member: AuthenticatedMemberInfo) = + ApiResponse.success(termsService.getAgreements(member.id)) @PutMapping("/terms/agreements") - fun agree(@RequestBody @Valid req: TermsAgreementRequest) = - ApiResponse.success(termsService.upsertAgreements(req)) + fun agree( + @AuthenticatedMember member: AuthenticatedMemberInfo, + @RequestBody @Valid req: TermsAgreementRequest, + ) = ApiResponse.success(termsService.upsertAgreements(member.id, req)) } From 715ea3621d48a303a29b003cfa48a0dae9497cbe Mon Sep 17 00:00:00 2001 From: hod Date: Mon, 2 Feb 2026 21:21:04 +0900 Subject: [PATCH 04/10] Implement OIDC client for Kakao login --- .../com/moa/common/exception/ErrorCode.kt | 4 + .../com/moa/controller/AuthController.kt | 22 ++++++ .../com/moa/service/auth/AuthService.kt | 72 +++++++++++++++++ .../com/moa/service/auth/oidc/OidcClient.kt | 54 +++++++++++++ .../service/auth/oidc/OidcIdTokenValidator.kt | 78 +++++++++++++++++++ .../service/auth/oidc/OidcProviderConfig.kt | 15 ++++ .../service/auth/oidc/OidcPublicKeyCache.kt | 45 +++++++++++ .../com/moa/service/auth/oidc/OidcUserInfo.kt | 8 ++ .../com/moa/service/auth/oidc/ProviderType.kt | 6 ++ .../moa/service/dto/KaKaoSignInUpRequest.kt | 7 ++ .../moa/service/dto/KakaoSignInUpResponse.kt | 5 ++ 11 files changed, 316 insertions(+) create mode 100644 src/main/kotlin/com/moa/controller/AuthController.kt create mode 100644 src/main/kotlin/com/moa/service/auth/AuthService.kt create mode 100644 src/main/kotlin/com/moa/service/auth/oidc/OidcClient.kt create mode 100644 src/main/kotlin/com/moa/service/auth/oidc/OidcIdTokenValidator.kt create mode 100644 src/main/kotlin/com/moa/service/auth/oidc/OidcProviderConfig.kt create mode 100644 src/main/kotlin/com/moa/service/auth/oidc/OidcPublicKeyCache.kt create mode 100644 src/main/kotlin/com/moa/service/auth/oidc/OidcUserInfo.kt create mode 100644 src/main/kotlin/com/moa/service/auth/oidc/ProviderType.kt create mode 100644 src/main/kotlin/com/moa/service/dto/KaKaoSignInUpRequest.kt create mode 100644 src/main/kotlin/com/moa/service/dto/KakaoSignInUpResponse.kt diff --git a/src/main/kotlin/com/moa/common/exception/ErrorCode.kt b/src/main/kotlin/com/moa/common/exception/ErrorCode.kt index 3e3ac06..6f65a9f 100644 --- a/src/main/kotlin/com/moa/common/exception/ErrorCode.kt +++ b/src/main/kotlin/com/moa/common/exception/ErrorCode.kt @@ -16,4 +16,8 @@ enum class ErrorCode( INVALID_PAYROLL_INPUT("INVALID_PAYROLL_INPUT", "급여 입력값이 유효하지 않습니다"), INVALID_WORK_POLICY_INPUT("INVALID_WORK_POLICY_INPUT", "근무정책 입력값이 유효하지 않습니다"), REQUIRED_TERMS_MUST_BE_AGREED("REQUIRED_TERMS_MUST_BE_AGREED", "필수 약관은 동의해야 합니다"), + + INVALID_ID_TOKEN("INVALID_ID_TOKEN", "유효하지 않은 ID 토큰입니다"), + INVALID_PROVIDER("INVALID_PROVIDER", "유효하지 않는 로그인 방식입니다."), + OIDC_PROVIDER_ERROR("OIDC_PROVIDER_ERROR", "인증 제공자 연동 중 오류가 발생했습니다"), } diff --git a/src/main/kotlin/com/moa/controller/AuthController.kt b/src/main/kotlin/com/moa/controller/AuthController.kt new file mode 100644 index 0000000..9611ae1 --- /dev/null +++ b/src/main/kotlin/com/moa/controller/AuthController.kt @@ -0,0 +1,22 @@ +package com.moa.controller + +import com.moa.common.response.ApiResponse +import com.moa.service.auth.AuthService +import com.moa.service.dto.KaKaoSignInUpRequest +import com.moa.service.dto.KakaoSignInUpResponse +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class AuthController( + private val authService: AuthService, +) { + + @PostMapping("/api/v1/auth/kakao") + fun kakaoSignInUp(@RequestBody kaKaoSignInUpRequest: KaKaoSignInUpRequest): ResponseEntity> { + val response = authService.kakaoSignInUp(kaKaoSignInUpRequest) + return ResponseEntity.ok(ApiResponse.Companion.success(response)) + } +} diff --git a/src/main/kotlin/com/moa/service/auth/AuthService.kt b/src/main/kotlin/com/moa/service/auth/AuthService.kt new file mode 100644 index 0000000..f1e932d --- /dev/null +++ b/src/main/kotlin/com/moa/service/auth/AuthService.kt @@ -0,0 +1,72 @@ +package com.moa.service.auth + +import com.moa.common.auth.JwtTokenProvider +import com.moa.entity.Member +import com.moa.entity.TermAgreement +import com.moa.repository.MemberRepository +import com.moa.repository.TermAgreementRepository +import com.moa.repository.TermRepository +import com.moa.service.auth.oidc.OidcIdTokenValidator +import com.moa.service.auth.oidc.ProviderType +import com.moa.service.dto.KaKaoSignInUpRequest +import com.moa.service.dto.KakaoSignInUpResponse +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AuthService( + private val oidcIdTokenValidator: OidcIdTokenValidator, + private val jwtTokenProvider: JwtTokenProvider, + private val memberRepository: MemberRepository, + private val termRepository: TermRepository, + private val termAgreementRepository: TermAgreementRepository, +) { + + @Transactional + fun kakaoSignInUp(request: KaKaoSignInUpRequest): KakaoSignInUpResponse { + val userInfo = oidcIdTokenValidator.validate(ProviderType.KAKAO, request.idToken) + + val member = memberRepository.findByProviderAndProviderSubject( + provider = userInfo.provider, + providerSubject = userInfo.subject, + ) + + member?.let { + return KakaoSignInUpResponse( + jwtTokenProvider.createAccessToken(member.id) + ) + } + + val registeredMember = memberRepository.save( + Member( + provider = ProviderType.KAKAO, + providerSubject = userInfo.subject, + profile = null, + ) + ) + + createTermAgreementsForNewMember(registeredMember.id) + + val registerToken = jwtTokenProvider.createAccessToken( + registeredMember.id + ) + + return KakaoSignInUpResponse( + registerToken, + ) + } + + private fun createTermAgreementsForNewMember(memberId: Long) { + val activeTerms = termRepository.findAllByActiveTrue() + + val termAgreements = activeTerms.map { term -> + TermAgreement( + memberId = memberId, + termCode = term.code, + agreed = false, + ) + } + + termAgreementRepository.saveAll(termAgreements) + } +} diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcClient.kt b/src/main/kotlin/com/moa/service/auth/oidc/OidcClient.kt new file mode 100644 index 0000000..42d90e6 --- /dev/null +++ b/src/main/kotlin/com/moa/service/auth/oidc/OidcClient.kt @@ -0,0 +1,54 @@ +package com.moa.service.auth.oidc + +import com.moa.common.exception.ErrorCode +import com.moa.common.exception.UnauthorizedException +import org.springframework.stereotype.Component +import org.springframework.web.client.RestClient +import java.math.BigInteger +import java.security.KeyFactory +import java.security.interfaces.RSAPublicKey +import java.security.spec.RSAPublicKeySpec +import java.util.* + +@Component +class OidcClient( + private val restClient: RestClient = RestClient.create(), +) { + fun fetchPublicKeys(jwksUri: String): Map { + val response = try { + restClient.get() + .uri(jwksUri) + .retrieve() + .body(JwksResponse::class.java) + } catch (ex: Exception) { + throw UnauthorizedException(ErrorCode.OIDC_PROVIDER_ERROR) + } + + return response?.keys + ?.filter { it.kty == "RSA" && it.use == "sig" } + ?.associate { key -> + key.kid to createRsaPublicKey(key.n, key.e) + } ?: emptyMap() + } + + private fun createRsaPublicKey(n: String, e: String): RSAPublicKey { + val decoder = Base64.getUrlDecoder() + val modulus = BigInteger(1, decoder.decode(n)) + val exponent = BigInteger(1, decoder.decode(e)) + val spec = RSAPublicKeySpec(modulus, exponent) + val keyFactory = KeyFactory.getInstance("RSA") + return keyFactory.generatePublic(spec) as RSAPublicKey + } + + private data class JwksResponse( + val keys: List, + ) + + private data class JwkKey( + val kid: String, + val kty: String, + val use: String?, + val n: String, + val e: String, + ) +} diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcIdTokenValidator.kt b/src/main/kotlin/com/moa/service/auth/oidc/OidcIdTokenValidator.kt new file mode 100644 index 0000000..c471d56 --- /dev/null +++ b/src/main/kotlin/com/moa/service/auth/oidc/OidcIdTokenValidator.kt @@ -0,0 +1,78 @@ +package com.moa.service.auth.oidc + +import com.moa.common.exception.ErrorCode +import com.moa.common.exception.UnauthorizedException +import io.jsonwebtoken.Jwts +import org.springframework.stereotype.Component +import tools.jackson.databind.ObjectMapper +import java.util.* + +@Component +class OidcIdTokenValidator( + private val config: OidcProviderConfig, + private val publicKeyCache: OidcPublicKeyCache, + private val objectMapper: ObjectMapper +) { + fun validate(provider: ProviderType, idToken: String): OidcUserInfo { + val providerProperties = getProviderProperties(provider) + + return validateToken(idToken, providerProperties, provider) + } + + private fun getProviderProperties(provider: ProviderType): OidcProviderConfig.ProviderProperties { + return when (provider) { + ProviderType.KAKAO -> config.kakao + else -> throw UnauthorizedException(ErrorCode.INVALID_PROVIDER) + } + } + + private fun validateToken( + idToken: String, + providerConfig: OidcProviderConfig.ProviderProperties, + provider: ProviderType + ): OidcUserInfo { + val kid = extractKid(idToken) + + val publicKey = publicKeyCache.getPublicKey( + jwksUri = providerConfig.jwksUri, + kid = kid, + ttlSeconds = providerConfig.cacheTtlSeconds, + ) + + val claims = try { + Jwts.parser() + .verifyWith(publicKey) + // TODO: oauth계정 선정후 주석 해제 + // .requireIssuer(providerConfig.issuer) + // .requireAudience(providerConfig.audience) + .build() + .parseSignedClaims(idToken) + .payload + } catch (ex: Exception) { + throw UnauthorizedException(ErrorCode.INVALID_ID_TOKEN) + } + + return OidcUserInfo( + subject = claims.subject, + email = claims["email"] as? String, + nickname = claims["nickname"] as? String, + provider = provider + ) + } + + + private fun extractKid(idToken: String): String { + try { + val headerPart = idToken.split(".")[0] + val decodedHeader = String(Base64.getUrlDecoder().decode(headerPart)) + + val headerMap = objectMapper.readValue(decodedHeader, Map::class.java) + + return headerMap["kid"] as? String + ?: throw UnauthorizedException(ErrorCode.INVALID_ID_TOKEN) + + } catch (e: Exception) { + throw UnauthorizedException(ErrorCode.INVALID_ID_TOKEN) + } + } +} diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcProviderConfig.kt b/src/main/kotlin/com/moa/service/auth/oidc/OidcProviderConfig.kt new file mode 100644 index 0000000..b81275e --- /dev/null +++ b/src/main/kotlin/com/moa/service/auth/oidc/OidcProviderConfig.kt @@ -0,0 +1,15 @@ +package com.moa.service.auth.oidc + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "oidc") +data class OidcProviderConfig( + val kakao: ProviderProperties, +) { + data class ProviderProperties( + val jwksUri: String, + val issuer: String, + val audience: String, + val cacheTtlSeconds: Long = 3600, + ) +} diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcPublicKeyCache.kt b/src/main/kotlin/com/moa/service/auth/oidc/OidcPublicKeyCache.kt new file mode 100644 index 0000000..9ef736c --- /dev/null +++ b/src/main/kotlin/com/moa/service/auth/oidc/OidcPublicKeyCache.kt @@ -0,0 +1,45 @@ +package com.moa.service.auth.oidc + +import com.moa.common.exception.BadRequestException +import com.moa.common.exception.ErrorCode +import org.springframework.stereotype.Component +import java.security.interfaces.RSAPublicKey +import java.time.Instant +import java.util.concurrent.ConcurrentHashMap + +@Component +class OidcPublicKeyCache( + private val oidcClient: OidcClient, +) { + private data class CacheEntry( + val keys: Map, + val expiresAt: Instant, + ) + + private val cache = ConcurrentHashMap() + + fun getPublicKey(jwksUri: String, kid: String, ttlSeconds: Long): RSAPublicKey { + val entry = cache[jwksUri] + val now = Instant.now() + + if (entry != null && entry.expiresAt.isAfter(now)) { + entry.keys[kid]?.let { return it } + } + + return refreshAndGetKey(jwksUri, kid, ttlSeconds) + } + + private fun refreshAndGetKey(jwksUri: String, kid: String, ttlSeconds: Long): RSAPublicKey { + val keys = oidcClient.fetchPublicKeys(jwksUri) + val expiresAt = Instant.now().plusSeconds(ttlSeconds) + cache[jwksUri] = CacheEntry(keys, expiresAt) + + return keys[kid] + ?: throw BadRequestException(ErrorCode.INVALID_ID_TOKEN); + } + + //TODO : 혹시 강제 초기화 필요할 때를 대비해 남겨둠 + fun clear() { + cache.clear() + } +} diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcUserInfo.kt b/src/main/kotlin/com/moa/service/auth/oidc/OidcUserInfo.kt new file mode 100644 index 0000000..b6d4cc3 --- /dev/null +++ b/src/main/kotlin/com/moa/service/auth/oidc/OidcUserInfo.kt @@ -0,0 +1,8 @@ +package com.moa.service.auth.oidc + +data class OidcUserInfo( + val subject: String, + val email: String?, + val nickname: String?, + val provider: ProviderType, +) diff --git a/src/main/kotlin/com/moa/service/auth/oidc/ProviderType.kt b/src/main/kotlin/com/moa/service/auth/oidc/ProviderType.kt new file mode 100644 index 0000000..f2dbfb5 --- /dev/null +++ b/src/main/kotlin/com/moa/service/auth/oidc/ProviderType.kt @@ -0,0 +1,6 @@ +package com.moa.service.auth.oidc + +enum class ProviderType { + KAKAO, + APPLE, +} diff --git a/src/main/kotlin/com/moa/service/dto/KaKaoSignInUpRequest.kt b/src/main/kotlin/com/moa/service/dto/KaKaoSignInUpRequest.kt new file mode 100644 index 0000000..bace07a --- /dev/null +++ b/src/main/kotlin/com/moa/service/dto/KaKaoSignInUpRequest.kt @@ -0,0 +1,7 @@ +package com.moa.service.dto + +data class KaKaoSignInUpRequest( + val idToken: String, + val fcmDeviceToken: String? = null, +) { +} diff --git a/src/main/kotlin/com/moa/service/dto/KakaoSignInUpResponse.kt b/src/main/kotlin/com/moa/service/dto/KakaoSignInUpResponse.kt new file mode 100644 index 0000000..34cef7f --- /dev/null +++ b/src/main/kotlin/com/moa/service/dto/KakaoSignInUpResponse.kt @@ -0,0 +1,5 @@ +package com.moa.service.dto + +data class KakaoSignInUpResponse( + val accessToken: String? = null, +) From 3d79b52ba600a3639850d69f3ef183bd0f522b13 Mon Sep 17 00:00:00 2001 From: hod Date: Tue, 3 Feb 2026 00:19:38 +0900 Subject: [PATCH 05/10] Refactor separate auth infrastructure from business logic --- .../com/moa/common/auth/JwtTokenProvider.kt | 16 +++++++---- .../common/filter/JwtAuthenticationFilter.kt | 2 -- .../auth => common}/oidc/OidcClient.kt | 2 +- .../oidc/OidcIdTokenValidator.kt | 10 +++---- .../oidc/OidcProviderConfig.kt | 2 +- .../oidc/OidcPublicKeyCache.kt | 7 +---- .../auth => common}/oidc/OidcUserInfo.kt | 6 ++--- .../com/moa/controller/AuthController.kt | 5 ++-- src/main/kotlin/com/moa/entity/Member.kt | 1 - .../auth/oidc => entity}/ProviderType.kt | 2 +- src/main/kotlin/com/moa/entity/Term.kt | 3 --- .../com/moa/repository/MemberRepository.kt | 2 +- .../com/moa/repository/TermRepository.kt | 4 +-- .../com/moa/service/{auth => }/AuthService.kt | 27 +++---------------- .../moa/service/OnboardingStatusService.kt | 2 +- .../kotlin/com/moa/service/TermsService.kt | 6 ++--- src/main/resources/application-local.yml | 1 - src/main/resources/application-prod.yml | 1 - 18 files changed, 32 insertions(+), 67 deletions(-) rename src/main/kotlin/com/moa/{service/auth => common}/oidc/OidcClient.kt (98%) rename src/main/kotlin/com/moa/{service/auth => common}/oidc/OidcIdTokenValidator.kt (86%) rename src/main/kotlin/com/moa/{service/auth => common}/oidc/OidcProviderConfig.kt (91%) rename src/main/kotlin/com/moa/{service/auth => common}/oidc/OidcPublicKeyCache.kt (88%) rename src/main/kotlin/com/moa/{service/auth => common}/oidc/OidcUserInfo.kt (50%) rename src/main/kotlin/com/moa/{service/auth/oidc => entity}/ProviderType.kt (60%) rename src/main/kotlin/com/moa/service/{auth => }/AuthService.kt (63%) diff --git a/src/main/kotlin/com/moa/common/auth/JwtTokenProvider.kt b/src/main/kotlin/com/moa/common/auth/JwtTokenProvider.kt index 98e2215..bfe14f8 100644 --- a/src/main/kotlin/com/moa/common/auth/JwtTokenProvider.kt +++ b/src/main/kotlin/com/moa/common/auth/JwtTokenProvider.kt @@ -7,7 +7,9 @@ import jakarta.servlet.http.HttpServletRequest import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import java.nio.charset.StandardCharsets -import java.time.Instant +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneId import java.util.* @Component @@ -22,13 +24,13 @@ class JwtTokenProvider( private val accessKey = Keys.hmacShaKeyFor(accessTokenSecretKey.toByteArray(StandardCharsets.UTF_8)) fun createAccessToken(userId: Long): String { - val now = Instant.now() - val expiryDate = now.plusMillis(accessTokenExpirationInMilliseconds) + val now = LocalDateTime.now() + val expiryDate = now.plus(Duration.ofMillis(accessTokenExpirationInMilliseconds)) return Jwts.builder() .subject(userId.toString()) - .issuedAt(Date.from(now)) - .expiration(Date.from(expiryDate)) + .issuedAt(toDate(now)) + .expiration(toDate(expiryDate)) .signWith(accessKey) .compact() } @@ -61,3 +63,7 @@ class JwtTokenProvider( .payload } } + +fun toDate(localDateTime: LocalDateTime): Date { + return Date.from(localDateTime.atZone(ZoneId.of("Asia/Seoul")).toInstant()) +} diff --git a/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt index 4cfc141..0ce2472 100644 --- a/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt @@ -18,9 +18,7 @@ class JwtAuthenticationFilter( companion object { private val EXCLUDED_PATHS = listOf( "/api/v1/auth", - "/health", "/h2-console", - "/actuator", ) } diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcClient.kt b/src/main/kotlin/com/moa/common/oidc/OidcClient.kt similarity index 98% rename from src/main/kotlin/com/moa/service/auth/oidc/OidcClient.kt rename to src/main/kotlin/com/moa/common/oidc/OidcClient.kt index 42d90e6..bc00b43 100644 --- a/src/main/kotlin/com/moa/service/auth/oidc/OidcClient.kt +++ b/src/main/kotlin/com/moa/common/oidc/OidcClient.kt @@ -1,4 +1,4 @@ -package com.moa.service.auth.oidc +package com.moa.common.oidc import com.moa.common.exception.ErrorCode import com.moa.common.exception.UnauthorizedException diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcIdTokenValidator.kt b/src/main/kotlin/com/moa/common/oidc/OidcIdTokenValidator.kt similarity index 86% rename from src/main/kotlin/com/moa/service/auth/oidc/OidcIdTokenValidator.kt rename to src/main/kotlin/com/moa/common/oidc/OidcIdTokenValidator.kt index c471d56..994ce0d 100644 --- a/src/main/kotlin/com/moa/service/auth/oidc/OidcIdTokenValidator.kt +++ b/src/main/kotlin/com/moa/common/oidc/OidcIdTokenValidator.kt @@ -1,7 +1,8 @@ -package com.moa.service.auth.oidc +package com.moa.common.oidc import com.moa.common.exception.ErrorCode import com.moa.common.exception.UnauthorizedException +import com.moa.entity.ProviderType import io.jsonwebtoken.Jwts import org.springframework.stereotype.Component import tools.jackson.databind.ObjectMapper @@ -42,9 +43,6 @@ class OidcIdTokenValidator( val claims = try { Jwts.parser() .verifyWith(publicKey) - // TODO: oauth계정 선정후 주석 해제 - // .requireIssuer(providerConfig.issuer) - // .requireAudience(providerConfig.audience) .build() .parseSignedClaims(idToken) .payload @@ -54,8 +52,6 @@ class OidcIdTokenValidator( return OidcUserInfo( subject = claims.subject, - email = claims["email"] as? String, - nickname = claims["nickname"] as? String, provider = provider ) } @@ -71,7 +67,7 @@ class OidcIdTokenValidator( return headerMap["kid"] as? String ?: throw UnauthorizedException(ErrorCode.INVALID_ID_TOKEN) - } catch (e: Exception) { + } catch (ex: Exception) { throw UnauthorizedException(ErrorCode.INVALID_ID_TOKEN) } } diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcProviderConfig.kt b/src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt similarity index 91% rename from src/main/kotlin/com/moa/service/auth/oidc/OidcProviderConfig.kt rename to src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt index b81275e..fecf6fd 100644 --- a/src/main/kotlin/com/moa/service/auth/oidc/OidcProviderConfig.kt +++ b/src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt @@ -1,4 +1,4 @@ -package com.moa.service.auth.oidc +package com.moa.common.oidc import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcPublicKeyCache.kt b/src/main/kotlin/com/moa/common/oidc/OidcPublicKeyCache.kt similarity index 88% rename from src/main/kotlin/com/moa/service/auth/oidc/OidcPublicKeyCache.kt rename to src/main/kotlin/com/moa/common/oidc/OidcPublicKeyCache.kt index 9ef736c..6022c46 100644 --- a/src/main/kotlin/com/moa/service/auth/oidc/OidcPublicKeyCache.kt +++ b/src/main/kotlin/com/moa/common/oidc/OidcPublicKeyCache.kt @@ -1,4 +1,4 @@ -package com.moa.service.auth.oidc +package com.moa.common.oidc import com.moa.common.exception.BadRequestException import com.moa.common.exception.ErrorCode @@ -37,9 +37,4 @@ class OidcPublicKeyCache( return keys[kid] ?: throw BadRequestException(ErrorCode.INVALID_ID_TOKEN); } - - //TODO : 혹시 강제 초기화 필요할 때를 대비해 남겨둠 - fun clear() { - cache.clear() - } } diff --git a/src/main/kotlin/com/moa/service/auth/oidc/OidcUserInfo.kt b/src/main/kotlin/com/moa/common/oidc/OidcUserInfo.kt similarity index 50% rename from src/main/kotlin/com/moa/service/auth/oidc/OidcUserInfo.kt rename to src/main/kotlin/com/moa/common/oidc/OidcUserInfo.kt index b6d4cc3..8457a7d 100644 --- a/src/main/kotlin/com/moa/service/auth/oidc/OidcUserInfo.kt +++ b/src/main/kotlin/com/moa/common/oidc/OidcUserInfo.kt @@ -1,8 +1,8 @@ -package com.moa.service.auth.oidc +package com.moa.common.oidc + +import com.moa.entity.ProviderType data class OidcUserInfo( val subject: String, - val email: String?, - val nickname: String?, val provider: ProviderType, ) diff --git a/src/main/kotlin/com/moa/controller/AuthController.kt b/src/main/kotlin/com/moa/controller/AuthController.kt index 9611ae1..9ba0f0e 100644 --- a/src/main/kotlin/com/moa/controller/AuthController.kt +++ b/src/main/kotlin/com/moa/controller/AuthController.kt @@ -1,7 +1,7 @@ package com.moa.controller import com.moa.common.response.ApiResponse -import com.moa.service.auth.AuthService +import com.moa.service.AuthService import com.moa.service.dto.KaKaoSignInUpRequest import com.moa.service.dto.KakaoSignInUpResponse import org.springframework.http.ResponseEntity @@ -16,7 +16,6 @@ class AuthController( @PostMapping("/api/v1/auth/kakao") fun kakaoSignInUp(@RequestBody kaKaoSignInUpRequest: KaKaoSignInUpRequest): ResponseEntity> { - val response = authService.kakaoSignInUp(kaKaoSignInUpRequest) - return ResponseEntity.ok(ApiResponse.Companion.success(response)) + return ResponseEntity.ok(ApiResponse.success(authService.kakaoSignInUp(kaKaoSignInUpRequest))) } } diff --git a/src/main/kotlin/com/moa/entity/Member.kt b/src/main/kotlin/com/moa/entity/Member.kt index 8144180..c7e5a99 100644 --- a/src/main/kotlin/com/moa/entity/Member.kt +++ b/src/main/kotlin/com/moa/entity/Member.kt @@ -1,6 +1,5 @@ package com.moa.entity -import com.moa.service.auth.oidc.ProviderType import jakarta.persistence.* @Entity diff --git a/src/main/kotlin/com/moa/service/auth/oidc/ProviderType.kt b/src/main/kotlin/com/moa/entity/ProviderType.kt similarity index 60% rename from src/main/kotlin/com/moa/service/auth/oidc/ProviderType.kt rename to src/main/kotlin/com/moa/entity/ProviderType.kt index f2dbfb5..79a11aa 100644 --- a/src/main/kotlin/com/moa/service/auth/oidc/ProviderType.kt +++ b/src/main/kotlin/com/moa/entity/ProviderType.kt @@ -1,4 +1,4 @@ -package com.moa.service.auth.oidc +package com.moa.entity enum class ProviderType { KAKAO, diff --git a/src/main/kotlin/com/moa/entity/Term.kt b/src/main/kotlin/com/moa/entity/Term.kt index c2bf434..7a1b341 100644 --- a/src/main/kotlin/com/moa/entity/Term.kt +++ b/src/main/kotlin/com/moa/entity/Term.kt @@ -17,7 +17,4 @@ class Term( @Column(nullable = false) val contentUrl: String, - - @Column(nullable = false) - val active: Boolean = true, ) diff --git a/src/main/kotlin/com/moa/repository/MemberRepository.kt b/src/main/kotlin/com/moa/repository/MemberRepository.kt index 820dcf2..293be2b 100644 --- a/src/main/kotlin/com/moa/repository/MemberRepository.kt +++ b/src/main/kotlin/com/moa/repository/MemberRepository.kt @@ -1,7 +1,7 @@ package com.moa.repository import com.moa.entity.Member -import com.moa.service.auth.oidc.ProviderType +import com.moa.entity.ProviderType import org.springframework.data.jpa.repository.JpaRepository interface MemberRepository : JpaRepository { diff --git a/src/main/kotlin/com/moa/repository/TermRepository.kt b/src/main/kotlin/com/moa/repository/TermRepository.kt index cacf944..7dd2a90 100644 --- a/src/main/kotlin/com/moa/repository/TermRepository.kt +++ b/src/main/kotlin/com/moa/repository/TermRepository.kt @@ -3,6 +3,4 @@ package com.moa.repository import com.moa.entity.Term import org.springframework.data.jpa.repository.JpaRepository -interface TermRepository : JpaRepository { - fun findAllByActiveTrue(): List -} +interface TermRepository : JpaRepository diff --git a/src/main/kotlin/com/moa/service/auth/AuthService.kt b/src/main/kotlin/com/moa/service/AuthService.kt similarity index 63% rename from src/main/kotlin/com/moa/service/auth/AuthService.kt rename to src/main/kotlin/com/moa/service/AuthService.kt index f1e932d..d8eb64c 100644 --- a/src/main/kotlin/com/moa/service/auth/AuthService.kt +++ b/src/main/kotlin/com/moa/service/AuthService.kt @@ -1,13 +1,10 @@ -package com.moa.service.auth +package com.moa.service import com.moa.common.auth.JwtTokenProvider +import com.moa.common.oidc.OidcIdTokenValidator import com.moa.entity.Member -import com.moa.entity.TermAgreement +import com.moa.entity.ProviderType import com.moa.repository.MemberRepository -import com.moa.repository.TermAgreementRepository -import com.moa.repository.TermRepository -import com.moa.service.auth.oidc.OidcIdTokenValidator -import com.moa.service.auth.oidc.ProviderType import com.moa.service.dto.KaKaoSignInUpRequest import com.moa.service.dto.KakaoSignInUpResponse import org.springframework.stereotype.Service @@ -18,8 +15,6 @@ class AuthService( private val oidcIdTokenValidator: OidcIdTokenValidator, private val jwtTokenProvider: JwtTokenProvider, private val memberRepository: MemberRepository, - private val termRepository: TermRepository, - private val termAgreementRepository: TermAgreementRepository, ) { @Transactional @@ -45,8 +40,6 @@ class AuthService( ) ) - createTermAgreementsForNewMember(registeredMember.id) - val registerToken = jwtTokenProvider.createAccessToken( registeredMember.id ) @@ -55,18 +48,4 @@ class AuthService( registerToken, ) } - - private fun createTermAgreementsForNewMember(memberId: Long) { - val activeTerms = termRepository.findAllByActiveTrue() - - val termAgreements = activeTerms.map { term -> - TermAgreement( - memberId = memberId, - termCode = term.code, - agreed = false, - ) - } - - termAgreementRepository.saveAll(termAgreements) - } } diff --git a/src/main/kotlin/com/moa/service/OnboardingStatusService.kt b/src/main/kotlin/com/moa/service/OnboardingStatusService.kt index 2b0d388..8336b88 100644 --- a/src/main/kotlin/com/moa/service/OnboardingStatusService.kt +++ b/src/main/kotlin/com/moa/service/OnboardingStatusService.kt @@ -19,7 +19,7 @@ class OnboardingStatusService( @Transactional(readOnly = true) fun getStatus(memberId: Long, today: LocalDate = LocalDate.now()): OnboardingStatusResponse { // 필수 약관 동의 여부 - val requiredCodes = termRepository.findAllByActiveTrue() + val requiredCodes = termRepository.findAll() .filter { it.required } .map { it.code } .toSet() diff --git a/src/main/kotlin/com/moa/service/TermsService.kt b/src/main/kotlin/com/moa/service/TermsService.kt index b293254..89240cb 100644 --- a/src/main/kotlin/com/moa/service/TermsService.kt +++ b/src/main/kotlin/com/moa/service/TermsService.kt @@ -18,7 +18,7 @@ class TermsService( @Transactional(readOnly = true) fun getTerms(): TermsResponse { - val terms = termRepository.findAllByActiveTrue() + val terms = termRepository.findAll() .sortedWith(compareByDescending { it.required }.thenBy { it.code }) .map { TermDto( @@ -34,7 +34,7 @@ class TermsService( @Transactional fun getAgreements(memberId: Long): TermsAgreementsResponse { - val terms = termRepository.findAllByActiveTrue() + val terms = termRepository.findAll() val requiredCodes = terms.filter { it.required }.map { it.code }.toSet() val existingAgreements = termAgreementRepository.findAllByMemberId(memberId) @@ -76,7 +76,7 @@ class TermsService( @Transactional fun upsertAgreements(memberId: Long, req: TermsAgreementRequest): TermsAgreementsResponse { - val terms = termRepository.findAllByActiveTrue() + val terms = termRepository.findAll() val termByCode = terms.associateBy { it.code } val requiredCodes = terms.filter { it.required }.map { it.code }.toSet() diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 1dceafe..443279f 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -16,7 +16,6 @@ spring: driver-class-name: org.h2.Driver url: jdbc:h2:mem:core;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username: sa - pool-name: moa-db-pool sql: init: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 835a012..44fccfa 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -4,7 +4,6 @@ spring: url: jdbc:mysql://${db.prod.host}:${db.prod.port}/${db.prod.name}?serverTimezone=UTC&useUniCode=yes&characterEncoding=UTF-8 username: ${db.prod.username} password: ${db.prod.password} - pool-name: moa-db-pool jpa: database-platform: org.hibernate.dialect.MySQLDialect From 972d0262153a45841b7b39e4c1e7197d70ef4b77 Mon Sep 17 00:00:00 2001 From: hod Date: Tue, 3 Feb 2026 01:05:35 +0900 Subject: [PATCH 06/10] edit submodules import path --- src/main/resources/application.yml | 6 +++--- src/test/resources/application.yaml | 14 -------------- src/test/resources/application.yml | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 src/test/resources/application.yaml create mode 100644 src/test/resources/application.yml diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 13ccc23..a8b08f8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: config: import: - - classpath:/moa-secret/oauth.yml - - classpath:/moa-secret/jwt.yml - - classpath:/moa-secret/db.yml + - moa-secret/oauth.yml + - moa-secret/jwt.yml + - moa-secret/db.yml diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml deleted file mode 100644 index 5ef034a..0000000 --- a/src/test/resources/application.yaml +++ /dev/null @@ -1,14 +0,0 @@ -spring: - application: - name: demo - -jwt: - secret-key: moa-temporary-secret-key-for-testing-minimum-256-bits-required - expiration-milliseconds: 3600000 - -oidc: - kakao: - jwks-uri: https://kauth.kakao.com/.well-known/jwks.json - issuer: https://kauth.kakao.com - audience: test-app-key - cache-ttl-seconds: 3600 diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..e27ed31 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,14 @@ +spring: + application: + name: demo + +jwt: + secret-key: ${jwt.local.access.secret-key} + expiration-milliseconds: ${jwt.local.access.expiration} + +oidc: + kakao: + jwks-uri: ${oauth.kakao.jwks-uri} + issuer: ${oauth.kakao.issuer} + audience: ${oauth.kakao.audience} + cache-ttl-seconds: ${oauth.cache-ttl-seconds} From 3f91c9eab9c287f1029d9af60443164faa99063e Mon Sep 17 00:00:00 2001 From: hod Date: Tue, 3 Feb 2026 01:25:09 +0900 Subject: [PATCH 07/10] Rename test config to inherit main application settings --- src/test/resources/{application.yml => application-test.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/resources/{application.yml => application-test.yml} (100%) diff --git a/src/test/resources/application.yml b/src/test/resources/application-test.yml similarity index 100% rename from src/test/resources/application.yml rename to src/test/resources/application-test.yml From 4eada879cce3f5c56cfc19cbe636530c472040c3 Mon Sep 17 00:00:00 2001 From: hod Date: Tue, 3 Feb 2026 02:06:31 +0900 Subject: [PATCH 08/10] Set default spring profile to 'local' --- src/main/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a8b08f8..e9388a1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,3 +4,6 @@ spring: - moa-secret/oauth.yml - moa-secret/jwt.yml - moa-secret/db.yml + + profiles: + default: local From 6a99723bc1d37356976568b3ea433b732d4a891a Mon Sep 17 00:00:00 2001 From: hod Date: Tue, 3 Feb 2026 02:29:27 +0900 Subject: [PATCH 09/10] Fix CI test failure by adding submodule checkout token --- .github/workflows/be-pr-workflow.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/be-pr-workflow.yml b/.github/workflows/be-pr-workflow.yml index 185c1ee..a545e09 100644 --- a/.github/workflows/be-pr-workflow.yml +++ b/.github/workflows/be-pr-workflow.yml @@ -22,6 +22,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + token: ${{ secrets.BE_SUBMODULE_TOKEN }} - name: Set up JDK 21 uses: actions/setup-java@v4 From 00ac5fbae79192bb46b238598052f0ebc1273919 Mon Sep 17 00:00:00 2001 From: hod Date: Tue, 3 Feb 2026 02:29:27 +0900 Subject: [PATCH 10/10] Refactor configuration files to use secret properties for sensitive data --- .../{be-pr-workflow.yml => pr-workflow.yml} | 3 +++ .../workflows/{be-deploy.yml => prod-deploy.yml} | 3 +-- .../common/exception/GlobalExceptionHandler.kt | 12 ++++++++++++ .../moa/common/filter/JwtAuthenticationFilter.kt | 16 ++++++++-------- .../kotlin/com/moa/common/oidc/OidcClient.kt | 4 +--- .../com/moa/common/oidc/OidcProviderConfig.kt | 2 -- src/main/resources/application-local.yml | 10 ++++------ src/main/resources/application-prod.yml | 16 +++++++--------- src/main/resources/moa-secret | 2 +- src/test/kotlin/com/moa/ApplicationTests.kt | 13 ------------- src/test/resources/application-test.yml | 10 ++++------ 11 files changed, 41 insertions(+), 50 deletions(-) rename .github/workflows/{be-pr-workflow.yml => pr-workflow.yml} (91%) rename .github/workflows/{be-deploy.yml => prod-deploy.yml} (97%) delete mode 100644 src/test/kotlin/com/moa/ApplicationTests.kt diff --git a/.github/workflows/be-pr-workflow.yml b/.github/workflows/pr-workflow.yml similarity index 91% rename from .github/workflows/be-pr-workflow.yml rename to .github/workflows/pr-workflow.yml index 185c1ee..28a6e2f 100644 --- a/.github/workflows/be-pr-workflow.yml +++ b/.github/workflows/pr-workflow.yml @@ -22,6 +22,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.SUBMODULE_TOKEN }} - name: Set up JDK 21 uses: actions/setup-java@v4 diff --git a/.github/workflows/be-deploy.yml b/.github/workflows/prod-deploy.yml similarity index 97% rename from .github/workflows/be-deploy.yml rename to .github/workflows/prod-deploy.yml index 206eb92..32c5094 100644 --- a/.github/workflows/be-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -21,9 +21,8 @@ jobs: - name: Checkout (for tagging) uses: actions/checkout@v4 with: - fetch-depth: 0 submodules: recursive - token: ${{ secrets.BE_SUBMODULE_TOKEN }} + token: ${{ secrets.SUBMODULE_TOKEN }} - name: Calculate version id: calc diff --git a/src/main/kotlin/com/moa/common/exception/GlobalExceptionHandler.kt b/src/main/kotlin/com/moa/common/exception/GlobalExceptionHandler.kt index 4255a7c..e357102 100644 --- a/src/main/kotlin/com/moa/common/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/com/moa/common/exception/GlobalExceptionHandler.kt @@ -2,6 +2,7 @@ package com.moa.common.exception import com.moa.common.response.ApiResponse import com.moa.common.response.FieldError +import org.slf4j.LoggerFactory import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.HttpStatusCode @@ -15,6 +16,8 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep @RestControllerAdvice class GlobalExceptionHandler : ResponseEntityExceptionHandler() { + private val log = LoggerFactory.getLogger(this::class.java) + @ExceptionHandler(NotFoundException::class) fun handleNotFoundException(ex: NotFoundException): ResponseEntity> { return ResponseEntity @@ -43,6 +46,15 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() { .body(ApiResponse.error(ex.errorCode)) } + @ExceptionHandler(RuntimeException::class) + fun handleRuntimeException(ex: RuntimeException): ResponseEntity> { + log.error("Unhandled Exception occurred: ${ex.message}", ex) + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(ErrorCode.INTERNAL_SERVER_ERROR)) + } + override fun handleMethodArgumentNotValid( ex: MethodArgumentNotValidException, headers: HttpHeaders, diff --git a/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt index 0ce2472..4aa8827 100644 --- a/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/com/moa/common/filter/JwtAuthenticationFilter.kt @@ -9,10 +9,12 @@ import jakarta.servlet.http.HttpServletResponse import org.springframework.http.MediaType import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter +import tools.jackson.databind.ObjectMapper @Component class JwtAuthenticationFilter( private val jwtTokenProvider: JwtTokenProvider, + private val objectMapper: ObjectMapper, ) : OncePerRequestFilter() { companion object { @@ -55,14 +57,12 @@ class JwtAuthenticationFilter( response.characterEncoding = "UTF-8" val errorCode = ErrorCode.UNAUTHORIZED - val body = """ - { - "code": "${errorCode.code}", - "message": "${errorCode.message}", - "content": null - } - """.trimIndent() + val errorResponse = mapOf( + "code" to errorCode.code, + "message" to errorCode.message, + "content" to null + ) - response.writer.write(body) + objectMapper.writeValue(response.writer, errorResponse) } } diff --git a/src/main/kotlin/com/moa/common/oidc/OidcClient.kt b/src/main/kotlin/com/moa/common/oidc/OidcClient.kt index bc00b43..34b65bb 100644 --- a/src/main/kotlin/com/moa/common/oidc/OidcClient.kt +++ b/src/main/kotlin/com/moa/common/oidc/OidcClient.kt @@ -1,7 +1,5 @@ package com.moa.common.oidc -import com.moa.common.exception.ErrorCode -import com.moa.common.exception.UnauthorizedException import org.springframework.stereotype.Component import org.springframework.web.client.RestClient import java.math.BigInteger @@ -21,7 +19,7 @@ class OidcClient( .retrieve() .body(JwksResponse::class.java) } catch (ex: Exception) { - throw UnauthorizedException(ErrorCode.OIDC_PROVIDER_ERROR) + throw RuntimeException("OIDC 공개키를 가져오는데 실패했습니다.", ex) } return response?.keys diff --git a/src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt b/src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt index fecf6fd..00306d7 100644 --- a/src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt +++ b/src/main/kotlin/com/moa/common/oidc/OidcProviderConfig.kt @@ -8,8 +8,6 @@ data class OidcProviderConfig( ) { data class ProviderProperties( val jwksUri: String, - val issuer: String, - val audience: String, val cacheTtlSeconds: Long = 3600, ) } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 443279f..34093da 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -23,12 +23,10 @@ spring: data-locations: classpath:data-local.sql jwt: - secret-key: ${jwt.local.access.secret-key} - expiration-milliseconds: ${jwt.local.access.expiration} + secret-key: ${secret.jwt.local.access.secret-key} + expiration-milliseconds: ${secret.jwt.local.access.expiration} oidc: kakao: - jwks-uri: ${oauth.kakao.jwks-uri} - issuer: ${oauth.kakao.issuer} - audience: ${oauth.kakao.audience} - cache-ttl-seconds: ${oauth.cache-ttl-seconds} + jwks-uri: ${secret.oauth.kakao.jwks-uri} + cache-ttl-seconds: ${secret.oauth.cache-ttl-seconds} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 44fccfa..d7dae4e 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,9 +1,9 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${db.prod.host}:${db.prod.port}/${db.prod.name}?serverTimezone=UTC&useUniCode=yes&characterEncoding=UTF-8 - username: ${db.prod.username} - password: ${db.prod.password} + url: jdbc:mysql://${secret.db.prod.host}:${secret.db.prod.port}/${secret.db.prod.name}?serverTimezone=UTC&useUniCode=yes&characterEncoding=UTF-8 + username: ${secret.db.prod.username} + password: ${secret.db.prod.password} jpa: database-platform: org.hibernate.dialect.MySQLDialect @@ -15,12 +15,10 @@ spring: show_sql: true jwt: - secret-key: ${jwt.prod.access.secret-key} - expiration-milliseconds: ${jwt.prod.access.expiration} + secret-key: ${secret.jwt.prod.access.secret-key} + expiration-milliseconds: ${secret.jwt.prod.access.expiration} oidc: kakao: - jwks-uri: ${ouath.kakao.jwks-uri} - issuer: ${ouath.kakao.issuer} - audience: ${ouath.kakao.audience} - cache-ttl-seconds: ${oauth.cache-ttl-seconds} + jwks-uri: ${secret.oauth.kakao.jwks-uri} + cache-ttl-seconds: ${secret.oauth.cache-ttl-seconds} diff --git a/src/main/resources/moa-secret b/src/main/resources/moa-secret index c307feb..990182b 160000 --- a/src/main/resources/moa-secret +++ b/src/main/resources/moa-secret @@ -1 +1 @@ -Subproject commit c307feb71657aed46363f38cf92b8f6f3d493c03 +Subproject commit 990182b8bf35a88c417a3d7ee62c874c7e9b6a1d diff --git a/src/test/kotlin/com/moa/ApplicationTests.kt b/src/test/kotlin/com/moa/ApplicationTests.kt deleted file mode 100644 index 869f8c9..0000000 --- a/src/test/kotlin/com/moa/ApplicationTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.moa - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class ApplicationTests { - - @Test - fun contextLoads() { - } - -} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index e27ed31..2428436 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -3,12 +3,10 @@ spring: name: demo jwt: - secret-key: ${jwt.local.access.secret-key} - expiration-milliseconds: ${jwt.local.access.expiration} + secret-key: ${secret.jwt.local.access.secret-key} + expiration-milliseconds: ${secret.jwt.local.access.expiration} oidc: kakao: - jwks-uri: ${oauth.kakao.jwks-uri} - issuer: ${oauth.kakao.issuer} - audience: ${oauth.kakao.audience} - cache-ttl-seconds: ${oauth.cache-ttl-seconds} + jwks-uri: ${secret.oauth.kakao.jwks-uri} + cache-ttl-seconds: ${secret.oauth.cache-ttl-seconds}