feat(ai-character): 관리자 API Phase 1 기반을 추가한다
This commit is contained in:
@@ -2,13 +2,19 @@ package kr.co.vividnext.sodalive.configs
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.common.ExceptionHandlerFilter
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler
|
||||
import kr.co.vividnext.sodalive.jwt.JwtAuthenticationEntryPoint
|
||||
import kr.co.vividnext.sodalive.jwt.JwtFilter
|
||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||
import kr.co.vividnext.sodalive.v2.api.admin.aicharacter.error.AiCharacterAdminErrorResponseWriter
|
||||
import kr.co.vividnext.sodalive.v2.api.admin.aicharacter.error.AiCharacterAdminExceptionHandler
|
||||
import kr.co.vividnext.sodalive.v2.api.admin.aicharacter.security.AiCharacterAdminSecurityErrorHandler
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity
|
||||
@@ -17,8 +23,16 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.access.AccessDeniedHandler
|
||||
import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher
|
||||
import org.springframework.web.HttpRequestHandler
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -35,25 +49,94 @@ class SecurityConfig(
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun webSecurityCustomizer(): WebSecurityCustomizer {
|
||||
fun webSecurityCustomizer(
|
||||
aiCharacterAdminSecurityErrorHandler: AiCharacterAdminSecurityErrorHandler
|
||||
): WebSecurityCustomizer {
|
||||
return WebSecurityCustomizer { web: WebSecurity ->
|
||||
web
|
||||
.requestRejectedHandler(aiCharacterAdminSecurityErrorHandler)
|
||||
.ignoring()
|
||||
.antMatchers("/h2-console/**", "/favicon.ico", "/error")
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
fun aiCharacterAdminErrorResponseWriter(messageSource: SodaMessageSource): AiCharacterAdminErrorResponseWriter {
|
||||
return AiCharacterAdminErrorResponseWriter(objectMapper, messageSource)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun aiCharacterAdminSecurityErrorHandler(
|
||||
responseWriter: AiCharacterAdminErrorResponseWriter
|
||||
): AiCharacterAdminSecurityErrorHandler {
|
||||
return AiCharacterAdminSecurityErrorHandler(
|
||||
responseWriter,
|
||||
AntPathRequestMatcher(AI_CHARACTER_ADMIN_PATH),
|
||||
WebConfig.createAiCharacterAdminCorsConfiguration()
|
||||
)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun aiCharacterAdminExceptionHandler(
|
||||
responseWriter: AiCharacterAdminErrorResponseWriter
|
||||
): AiCharacterAdminExceptionHandler {
|
||||
return AiCharacterAdminExceptionHandler(
|
||||
responseWriter,
|
||||
AntPathRequestMatcher(AI_CHARACTER_ADMIN_PATH)
|
||||
)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun aiCharacterAdminFallbackHandlerMapping(
|
||||
responseWriter: AiCharacterAdminErrorResponseWriter
|
||||
): SimpleUrlHandlerMapping {
|
||||
val notFoundHandler = HttpRequestHandler { request, response ->
|
||||
responseWriter.write(
|
||||
request,
|
||||
response,
|
||||
HttpStatus.NOT_FOUND,
|
||||
"common.error.invalid_request"
|
||||
)
|
||||
}
|
||||
return SimpleUrlHandlerMapping(
|
||||
mapOf(AI_CHARACTER_ADMIN_PATH to notFoundHandler),
|
||||
Ordered.LOWEST_PRECEDENCE - 2
|
||||
).apply {
|
||||
setCorsConfigurations(
|
||||
mapOf(AI_CHARACTER_ADMIN_PATH to WebConfig.createAiCharacterAdminCorsConfiguration())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun filterChain(
|
||||
http: HttpSecurity,
|
||||
aiCharacterAdminSecurityErrorHandler: AiCharacterAdminSecurityErrorHandler,
|
||||
aiCharacterAdminExceptionHandler: AiCharacterAdminExceptionHandler
|
||||
): SecurityFilterChain {
|
||||
val jwtFilter = JwtFilter(tokenProvider)
|
||||
val aiCharacterAdminRequestMatcher = AntPathRequestMatcher(AI_CHARACTER_ADMIN_PATH)
|
||||
val authenticationEntryPoints = linkedMapOf<RequestMatcher, AuthenticationEntryPoint>(
|
||||
aiCharacterAdminRequestMatcher to aiCharacterAdminSecurityErrorHandler
|
||||
)
|
||||
val delegatingAuthenticationEntryPoint = DelegatingAuthenticationEntryPoint(authenticationEntryPoints).apply {
|
||||
setDefaultEntryPoint(authenticationEntryPoint)
|
||||
}
|
||||
val accessDeniedHandlers = linkedMapOf<RequestMatcher, AccessDeniedHandler>(
|
||||
aiCharacterAdminRequestMatcher to aiCharacterAdminSecurityErrorHandler
|
||||
)
|
||||
val delegatingAccessDeniedHandler = RequestMatcherDelegatingAccessDeniedHandler(
|
||||
accessDeniedHandlers,
|
||||
accessDeniedHandler
|
||||
)
|
||||
|
||||
return http
|
||||
.cors()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.exceptionHandling()
|
||||
.authenticationEntryPoint(authenticationEntryPoint)
|
||||
.accessDeniedHandler(accessDeniedHandler)
|
||||
.authenticationEntryPoint(delegatingAuthenticationEntryPoint)
|
||||
.accessDeniedHandler(delegatingAccessDeniedHandler)
|
||||
.and()
|
||||
.headers()
|
||||
.frameOptions()
|
||||
@@ -63,7 +146,15 @@ class SecurityConfig(
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
|
||||
.addFilterBefore(ExceptionHandlerFilter(objectMapper), JwtFilter::class.java)
|
||||
.addFilterBefore(
|
||||
ExceptionHandlerFilter(
|
||||
objectMapper,
|
||||
aiCharacterAdminRequestMatcher,
|
||||
aiCharacterAdminSecurityErrorHandler,
|
||||
aiCharacterAdminExceptionHandler
|
||||
),
|
||||
JwtFilter::class.java
|
||||
)
|
||||
.authorizeRequests()
|
||||
.antMatchers("/member/check/email").permitAll()
|
||||
.antMatchers("/member/check/nickname").permitAll()
|
||||
@@ -108,10 +199,20 @@ class SecurityConfig(
|
||||
.antMatchers(HttpMethod.GET, "/api/v2/home/rankings/creators").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/api/v2/home/following").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/api/v2/home/on-air-lives").authenticated()
|
||||
.antMatchers(AI_CHARACTER_ADMIN_PATH)
|
||||
.access(
|
||||
"hasRole('ADMIN') and " +
|
||||
"principal instanceof T(kr.co.vividnext.sodalive.member.MemberAdapter) and " +
|
||||
"principal.member.role == T(kr.co.vividnext.sodalive.member.MemberRole).ADMIN"
|
||||
)
|
||||
// 페이지네이션 하위 경로(/lives, /debut-creators 등)는 인증 필수
|
||||
.antMatchers(HttpMethod.GET, "/api/v2/home/recommendations/**").authenticated()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AI_CHARACTER_ADMIN_PATH = "/api/v2/admin/ai-characters/**"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user