From d45393def0c02cdf8ac33f4094f210b5805cea18 Mon Sep 17 00:00:00 2001 From: jisu Date: Sun, 29 Dec 2024 01:48:17 +0900 Subject: [PATCH 1/2] style: update comment in cd script --- .github/workflows/cd-backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-backend.yml b/.github/workflows/cd-backend.yml index 393ae2a..58284ed 100644 --- a/.github/workflows/cd-backend.yml +++ b/.github/workflows/cd-backend.yml @@ -38,7 +38,7 @@ jobs: docker images docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} - - name: Deploy to Prod WAS Server + - name: Deploy to Dev WAS Server uses: appleboy/ssh-action@master with: host: ${{ secrets.WAS_HOST }} From 32a94f453f8fe6e0f9d8269af7082b33b76f9704 Mon Sep 17 00:00:00 2001 From: Jisu Lim <69844138+Ji-soo708@users.noreply.github.com> Date: Mon, 30 Dec 2024 02:12:02 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[YS-48]=20feat:=20security=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style: rename fileName * feat: add AuthorizationException * feat: add security configuration * feat: add ExceptionHandler --- .../exception/AuthorizationException.kt | 7 +++ .../domain/exception/DefaultException.kt | 10 +++ .../backend/domain/exception/ErrorCode.kt | 5 ++ .../gateway/TokenGatewayImpl.kt | 4 +- .../infrastructure/token/JwtTokenProvider.kt | 2 +- .../presentation/api/config/WebConfig.kt | 17 +++++ .../api/config/WebExceptionHandler.kt | 56 +++++++++++++++++ .../api/config/WebSecurityConfig.kt | 63 +++++++++++++++++++ .../config/filter/JwtAuthenticationFilter.kt | 36 +++++++++++ .../token/JwtTokenProviderTest.kt | 2 +- 10 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/com/dobby/backend/domain/exception/AuthorizationException.kt create mode 100644 src/main/kotlin/com/dobby/backend/domain/exception/DefaultException.kt create mode 100644 src/main/kotlin/com/dobby/backend/presentation/api/config/WebConfig.kt create mode 100644 src/main/kotlin/com/dobby/backend/presentation/api/config/WebExceptionHandler.kt create mode 100644 src/main/kotlin/com/dobby/backend/presentation/api/config/WebSecurityConfig.kt create mode 100644 src/main/kotlin/com/dobby/backend/presentation/api/config/filter/JwtAuthenticationFilter.kt diff --git a/src/main/kotlin/com/dobby/backend/domain/exception/AuthorizationException.kt b/src/main/kotlin/com/dobby/backend/domain/exception/AuthorizationException.kt new file mode 100644 index 0000000..ec75780 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/domain/exception/AuthorizationException.kt @@ -0,0 +1,7 @@ +package com.dobby.backend.domain.exception + +open class AuthorizationException( + errorCode: ErrorCode, +) : DomainException(errorCode) + +class PermissionDeniedException : AuthorizationException(ErrorCode.PERMISSION_DENIED) diff --git a/src/main/kotlin/com/dobby/backend/domain/exception/DefaultException.kt b/src/main/kotlin/com/dobby/backend/domain/exception/DefaultException.kt new file mode 100644 index 0000000..b9a4582 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/domain/exception/DefaultException.kt @@ -0,0 +1,10 @@ +package com.dobby.backend.domain.exception + +open class DefaultException( + errorCode: ErrorCode, +) : DomainException(errorCode) + +class UnknownErrorException : DefaultException(ErrorCode.UNKNOWN_SERVER_ERROR) +class UnauthorizedException : DefaultException(ErrorCode.UNAUTHORIZED) +class InvalidInputException : DefaultException(ErrorCode.INVALID_INPUT) +class UnknownResourceException : DefaultException(ErrorCode.UNKNOWN_RESOURCE) diff --git a/src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt b/src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt index d554c13..0e3a291 100644 --- a/src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt +++ b/src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt @@ -24,6 +24,11 @@ enum class ErrorCode( TOKEN_EXPIRED("AU0003", "Authentication token has expired.", HttpStatus.UNAUTHORIZED), INVALID_TOKEN_TYPE("AU0004", "Invalid token type", HttpStatus.UNAUTHORIZED), + /** + * Authorization error codes + */ + PERMISSION_DENIED("AZ0001", "Permission denied", HttpStatus.FORBIDDEN), + /** * Member error codes */ diff --git a/src/main/kotlin/com/dobby/backend/infrastructure/gateway/TokenGatewayImpl.kt b/src/main/kotlin/com/dobby/backend/infrastructure/gateway/TokenGatewayImpl.kt index 0d2a06f..ee90d0f 100644 --- a/src/main/kotlin/com/dobby/backend/infrastructure/gateway/TokenGatewayImpl.kt +++ b/src/main/kotlin/com/dobby/backend/infrastructure/gateway/TokenGatewayImpl.kt @@ -2,13 +2,13 @@ package com.dobby.backend.infrastructure.gateway import com.dobby.backend.domain.gateway.TokenGateway import com.dobby.backend.domain.model.Member -import com.dobby.backend.infrastructure.token.JWTTokenProvider +import com.dobby.backend.infrastructure.token.JwtTokenProvider import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.stereotype.Component @Component class TokenGatewayImpl( - private val tokenProvider: JWTTokenProvider, + private val tokenProvider: JwtTokenProvider, ) : TokenGateway { override fun generateAccessToken(member: Member): String { val authentication = UsernamePasswordAuthenticationToken( diff --git a/src/main/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProvider.kt b/src/main/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProvider.kt index 80af8e6..cc77876 100644 --- a/src/main/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProvider.kt +++ b/src/main/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProvider.kt @@ -13,7 +13,7 @@ import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec @Component -class JWTTokenProvider( +class JwtTokenProvider( private val tokenProperties: TokenProperties ) { private final val signKey: SecretKey = SecretKeySpec(tokenProperties.secretKey.toByteArray(), "AES") diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/config/WebConfig.kt b/src/main/kotlin/com/dobby/backend/presentation/api/config/WebConfig.kt new file mode 100644 index 0000000..b43c4f4 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/presentation/api/config/WebConfig.kt @@ -0,0 +1,17 @@ +package com.dobby.backend.presentation.api.config + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig : WebMvcConfigurer { + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedMethods("*") + .allowedOrigins( + "http://localhost:3000", + "http://localhost:3300", + ) + } +} diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/config/WebExceptionHandler.kt b/src/main/kotlin/com/dobby/backend/presentation/api/config/WebExceptionHandler.kt new file mode 100644 index 0000000..dd5eeb9 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/presentation/api/config/WebExceptionHandler.kt @@ -0,0 +1,56 @@ +package com.dobby.backend.presentation.api.config + +import com.dobby.backend.domain.exception.AuthenticationException +import com.dobby.backend.domain.exception.AuthorizationException +import com.dobby.backend.domain.exception.DomainException +import com.dobby.backend.domain.exception.PermissionDeniedException +import com.dobby.backend.presentation.api.dto.payload.ApiResponse +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.access.AccessDeniedException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class WebExceptionHandler { + + @ExceptionHandler(value = [DomainException::class]) + fun handleDomainException( + e: DomainException, + request: HttpServletRequest, + ): ResponseEntity> { + return ResponseEntity + .badRequest() + .body(e.toErrorResponse()) + } + + @ExceptionHandler(value = [AuthenticationException::class]) + fun handleAuthenticationException( + exception: AuthenticationException, + request: HttpServletRequest, + ): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(exception.toErrorResponse()) + } + + @ExceptionHandler(value = [AuthorizationException::class]) + fun handleAuthorizationException( + exception: AuthorizationException, + ): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body(exception.toErrorResponse()) + } + + @ExceptionHandler(value = [AccessDeniedException::class]) + fun handleAccessDeniedException() = handleAuthorizationException(PermissionDeniedException()) + + private fun DomainException.toErrorResponse(): ApiResponse { + return ApiResponse.onFailure( + code = this.code, + message = this.errorMessage + ) + } +} diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/config/WebSecurityConfig.kt b/src/main/kotlin/com/dobby/backend/presentation/api/config/WebSecurityConfig.kt new file mode 100644 index 0000000..5554d90 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/presentation/api/config/WebSecurityConfig.kt @@ -0,0 +1,63 @@ +package com.dobby.backend.presentation.api.config + +import com.dobby.backend.domain.exception.PermissionDeniedException +import com.dobby.backend.domain.exception.UnauthorizedException +import com.dobby.backend.infrastructure.token.JwtTokenProvider +import com.dobby.backend.presentation.api.config.filter.JwtAuthenticationFilter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.security.config.Customizer +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.web.servlet.HandlerExceptionResolver + +@Configuration +@EnableMethodSecurity +class WebSecurityConfig { + @Bean + @Order(0) + fun securityFilterChain( + httpSecurity: HttpSecurity, + jwtTokenProvider: JwtTokenProvider, + handlerExceptionResolver: HandlerExceptionResolver, + ): SecurityFilterChain = httpSecurity + .securityMatcher( "/v1/members/**") + .csrf { it.disable() } + .cors(Customizer.withDefaults()) + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authorizeHttpRequests { + it.anyRequest().authenticated() + } + .addFilterBefore( + JwtAuthenticationFilter(jwtTokenProvider, handlerExceptionResolver), + UsernamePasswordAuthenticationFilter::class.java + ) + .exceptionHandling { + it.accessDeniedHandler { request, response, exception -> + handlerExceptionResolver.resolveException(request, response, null, PermissionDeniedException()) + }.authenticationEntryPoint { request, response, authException -> + handlerExceptionResolver.resolveException(request, response, null, UnauthorizedException()) + } + } + .build() + + @Bean + @Order(1) + fun authSecurityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = httpSecurity + .securityMatcher("/v1/auth/**") + .csrf { it.disable() } + .cors(Customizer.withDefaults()) + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .authorizeHttpRequests { + it.anyRequest().permitAll() + } + .build() +} diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/config/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/com/dobby/backend/presentation/api/config/filter/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..54785c6 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/presentation/api/config/filter/JwtAuthenticationFilter.kt @@ -0,0 +1,36 @@ +package com.dobby.backend.presentation.api.config.filter + +import com.dobby.backend.domain.exception.AuthenticationTokenNotFoundException +import com.dobby.backend.domain.exception.AuthenticationTokenNotValidException +import com.dobby.backend.infrastructure.token.JwtTokenProvider +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.servlet.HandlerExceptionResolver + +class JwtAuthenticationFilter( + private val jwtTokenProvider: JwtTokenProvider, + private val handlerExceptionResolver: HandlerExceptionResolver, +) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + try { + val authenticationHeader = + request.getHeader("Authorization") ?: throw AuthenticationTokenNotFoundException() + val accessToken = if (authenticationHeader.startsWith("Bearer ")) + authenticationHeader.substring(7) + else throw AuthenticationTokenNotValidException() + + val authentication = jwtTokenProvider.parseAuthentication(accessToken) + SecurityContextHolder.getContext().authentication = authentication + return filterChain.doFilter(request, response) + } catch (e: Exception) { + handlerExceptionResolver.resolveException(request, response, null, e) + } + } +} diff --git a/src/test/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProviderTest.kt b/src/test/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProviderTest.kt index e564460..d410396 100644 --- a/src/test/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProviderTest.kt +++ b/src/test/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProviderTest.kt @@ -14,7 +14,7 @@ import kotlin.test.assertNotNull class JwtTokenProviderTest : BehaviorSpec() { @Autowired - lateinit var jwtTokenProvider: JWTTokenProvider + lateinit var jwtTokenProvider: JwtTokenProvider init { given("회원 정보가 주어지고") {