Skip to content

Commit 32a94f4

Browse files
authored
[YS-48] feat: security 관련 설정 추가 (#6)
* style: rename fileName * feat: add AuthorizationException * feat: add security configuration * feat: add ExceptionHandler
1 parent d45393d commit 32a94f4

File tree

10 files changed

+198
-4
lines changed

10 files changed

+198
-4
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.dobby.backend.domain.exception
2+
3+
open class AuthorizationException(
4+
errorCode: ErrorCode,
5+
) : DomainException(errorCode)
6+
7+
class PermissionDeniedException : AuthorizationException(ErrorCode.PERMISSION_DENIED)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.dobby.backend.domain.exception
2+
3+
open class DefaultException(
4+
errorCode: ErrorCode,
5+
) : DomainException(errorCode)
6+
7+
class UnknownErrorException : DefaultException(ErrorCode.UNKNOWN_SERVER_ERROR)
8+
class UnauthorizedException : DefaultException(ErrorCode.UNAUTHORIZED)
9+
class InvalidInputException : DefaultException(ErrorCode.INVALID_INPUT)
10+
class UnknownResourceException : DefaultException(ErrorCode.UNKNOWN_RESOURCE)

src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ enum class ErrorCode(
2424
TOKEN_EXPIRED("AU0003", "Authentication token has expired.", HttpStatus.UNAUTHORIZED),
2525
INVALID_TOKEN_TYPE("AU0004", "Invalid token type", HttpStatus.UNAUTHORIZED),
2626

27+
/**
28+
* Authorization error codes
29+
*/
30+
PERMISSION_DENIED("AZ0001", "Permission denied", HttpStatus.FORBIDDEN),
31+
2732
/**
2833
* Member error codes
2934
*/

src/main/kotlin/com/dobby/backend/infrastructure/gateway/TokenGatewayImpl.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package com.dobby.backend.infrastructure.gateway
22

33
import com.dobby.backend.domain.gateway.TokenGateway
44
import com.dobby.backend.domain.model.Member
5-
import com.dobby.backend.infrastructure.token.JWTTokenProvider
5+
import com.dobby.backend.infrastructure.token.JwtTokenProvider
66
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
77
import org.springframework.stereotype.Component
88

99
@Component
1010
class TokenGatewayImpl(
11-
private val tokenProvider: JWTTokenProvider,
11+
private val tokenProvider: JwtTokenProvider,
1212
) : TokenGateway {
1313
override fun generateAccessToken(member: Member): String {
1414
val authentication = UsernamePasswordAuthenticationToken(

src/main/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import javax.crypto.SecretKey
1313
import javax.crypto.spec.SecretKeySpec
1414

1515
@Component
16-
class JWTTokenProvider(
16+
class JwtTokenProvider(
1717
private val tokenProperties: TokenProperties
1818
) {
1919
private final val signKey: SecretKey = SecretKeySpec(tokenProperties.secretKey.toByteArray(), "AES")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.dobby.backend.presentation.api.config
2+
3+
import org.springframework.context.annotation.Configuration
4+
import org.springframework.web.servlet.config.annotation.CorsRegistry
5+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
6+
7+
@Configuration
8+
class WebConfig : WebMvcConfigurer {
9+
override fun addCorsMappings(registry: CorsRegistry) {
10+
registry.addMapping("/**")
11+
.allowedMethods("*")
12+
.allowedOrigins(
13+
"http://localhost:3000",
14+
"http://localhost:3300",
15+
)
16+
}
17+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.dobby.backend.presentation.api.config
2+
3+
import com.dobby.backend.domain.exception.AuthenticationException
4+
import com.dobby.backend.domain.exception.AuthorizationException
5+
import com.dobby.backend.domain.exception.DomainException
6+
import com.dobby.backend.domain.exception.PermissionDeniedException
7+
import com.dobby.backend.presentation.api.dto.payload.ApiResponse
8+
import jakarta.servlet.http.HttpServletRequest
9+
import org.springframework.http.HttpStatus
10+
import org.springframework.http.ResponseEntity
11+
import org.springframework.security.access.AccessDeniedException
12+
import org.springframework.web.bind.annotation.ExceptionHandler
13+
import org.springframework.web.bind.annotation.RestControllerAdvice
14+
15+
@RestControllerAdvice
16+
class WebExceptionHandler {
17+
18+
@ExceptionHandler(value = [DomainException::class])
19+
fun handleDomainException(
20+
e: DomainException,
21+
request: HttpServletRequest,
22+
): ResponseEntity<ApiResponse<Unit>> {
23+
return ResponseEntity
24+
.badRequest()
25+
.body(e.toErrorResponse())
26+
}
27+
28+
@ExceptionHandler(value = [AuthenticationException::class])
29+
fun handleAuthenticationException(
30+
exception: AuthenticationException,
31+
request: HttpServletRequest,
32+
): ResponseEntity<ApiResponse<Unit>> {
33+
return ResponseEntity
34+
.status(HttpStatus.UNAUTHORIZED)
35+
.body(exception.toErrorResponse())
36+
}
37+
38+
@ExceptionHandler(value = [AuthorizationException::class])
39+
fun handleAuthorizationException(
40+
exception: AuthorizationException,
41+
): ResponseEntity<ApiResponse<Unit>> {
42+
return ResponseEntity
43+
.status(HttpStatus.FORBIDDEN)
44+
.body(exception.toErrorResponse())
45+
}
46+
47+
@ExceptionHandler(value = [AccessDeniedException::class])
48+
fun handleAccessDeniedException() = handleAuthorizationException(PermissionDeniedException())
49+
50+
private fun DomainException.toErrorResponse(): ApiResponse<Unit> {
51+
return ApiResponse.onFailure(
52+
code = this.code,
53+
message = this.errorMessage
54+
)
55+
}
56+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.dobby.backend.presentation.api.config
2+
3+
import com.dobby.backend.domain.exception.PermissionDeniedException
4+
import com.dobby.backend.domain.exception.UnauthorizedException
5+
import com.dobby.backend.infrastructure.token.JwtTokenProvider
6+
import com.dobby.backend.presentation.api.config.filter.JwtAuthenticationFilter
7+
import org.springframework.context.annotation.Bean
8+
import org.springframework.context.annotation.Configuration
9+
import org.springframework.core.annotation.Order
10+
import org.springframework.security.config.Customizer
11+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
12+
import org.springframework.security.config.annotation.web.builders.HttpSecurity
13+
import org.springframework.security.config.http.SessionCreationPolicy
14+
import org.springframework.security.web.SecurityFilterChain
15+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
16+
import org.springframework.web.servlet.HandlerExceptionResolver
17+
18+
@Configuration
19+
@EnableMethodSecurity
20+
class WebSecurityConfig {
21+
@Bean
22+
@Order(0)
23+
fun securityFilterChain(
24+
httpSecurity: HttpSecurity,
25+
jwtTokenProvider: JwtTokenProvider,
26+
handlerExceptionResolver: HandlerExceptionResolver,
27+
): SecurityFilterChain = httpSecurity
28+
.securityMatcher( "/v1/members/**")
29+
.csrf { it.disable() }
30+
.cors(Customizer.withDefaults())
31+
.sessionManagement {
32+
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
33+
}
34+
.authorizeHttpRequests {
35+
it.anyRequest().authenticated()
36+
}
37+
.addFilterBefore(
38+
JwtAuthenticationFilter(jwtTokenProvider, handlerExceptionResolver),
39+
UsernamePasswordAuthenticationFilter::class.java
40+
)
41+
.exceptionHandling {
42+
it.accessDeniedHandler { request, response, exception ->
43+
handlerExceptionResolver.resolveException(request, response, null, PermissionDeniedException())
44+
}.authenticationEntryPoint { request, response, authException ->
45+
handlerExceptionResolver.resolveException(request, response, null, UnauthorizedException())
46+
}
47+
}
48+
.build()
49+
50+
@Bean
51+
@Order(1)
52+
fun authSecurityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = httpSecurity
53+
.securityMatcher("/v1/auth/**")
54+
.csrf { it.disable() }
55+
.cors(Customizer.withDefaults())
56+
.sessionManagement {
57+
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
58+
}
59+
.authorizeHttpRequests {
60+
it.anyRequest().permitAll()
61+
}
62+
.build()
63+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.dobby.backend.presentation.api.config.filter
2+
3+
import com.dobby.backend.domain.exception.AuthenticationTokenNotFoundException
4+
import com.dobby.backend.domain.exception.AuthenticationTokenNotValidException
5+
import com.dobby.backend.infrastructure.token.JwtTokenProvider
6+
import jakarta.servlet.FilterChain
7+
import jakarta.servlet.http.HttpServletRequest
8+
import jakarta.servlet.http.HttpServletResponse
9+
import org.springframework.security.core.context.SecurityContextHolder
10+
import org.springframework.web.filter.OncePerRequestFilter
11+
import org.springframework.web.servlet.HandlerExceptionResolver
12+
13+
class JwtAuthenticationFilter(
14+
private val jwtTokenProvider: JwtTokenProvider,
15+
private val handlerExceptionResolver: HandlerExceptionResolver,
16+
) : OncePerRequestFilter() {
17+
override fun doFilterInternal(
18+
request: HttpServletRequest,
19+
response: HttpServletResponse,
20+
filterChain: FilterChain,
21+
) {
22+
try {
23+
val authenticationHeader =
24+
request.getHeader("Authorization") ?: throw AuthenticationTokenNotFoundException()
25+
val accessToken = if (authenticationHeader.startsWith("Bearer "))
26+
authenticationHeader.substring(7)
27+
else throw AuthenticationTokenNotValidException()
28+
29+
val authentication = jwtTokenProvider.parseAuthentication(accessToken)
30+
SecurityContextHolder.getContext().authentication = authentication
31+
return filterChain.doFilter(request, response)
32+
} catch (e: Exception) {
33+
handlerExceptionResolver.resolveException(request, response, null, e)
34+
}
35+
}
36+
}

src/test/kotlin/com/dobby/backend/infrastructure/token/JwtTokenProviderTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import kotlin.test.assertNotNull
1414
class JwtTokenProviderTest : BehaviorSpec() {
1515

1616
@Autowired
17-
lateinit var jwtTokenProvider: JWTTokenProvider
17+
lateinit var jwtTokenProvider: JwtTokenProvider
1818

1919
init {
2020
given("회원 정보가 주어지고") {

0 commit comments

Comments
 (0)