Files
sodalive-backend-spring-boot/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt

219 lines
10 KiB
Kotlin

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
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
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
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig(
private val objectMapper: ObjectMapper,
private val tokenProvider: TokenProvider,
private val accessDeniedHandler: JwtAccessDeniedHandler,
private val authenticationEntryPoint: JwtAuthenticationEntryPoint
) {
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
@Bean
fun webSecurityCustomizer(
aiCharacterAdminSecurityErrorHandler: AiCharacterAdminSecurityErrorHandler
): WebSecurityCustomizer {
return WebSecurityCustomizer { web: WebSecurity ->
web
.requestRejectedHandler(aiCharacterAdminSecurityErrorHandler)
.ignoring()
.antMatchers("/h2-console/**", "/favicon.ico", "/error")
}
}
@Bean
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(delegatingAuthenticationEntryPoint)
.accessDeniedHandler(delegatingAccessDeniedHandler)
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
.addFilterBefore(
ExceptionHandlerFilter(
objectMapper,
aiCharacterAdminRequestMatcher,
aiCharacterAdminSecurityErrorHandler,
aiCharacterAdminExceptionHandler
),
JwtFilter::class.java
)
.authorizeRequests()
.antMatchers("/member/check/email").permitAll()
.antMatchers("/member/check/nickname").permitAll()
.antMatchers("/member/signup").permitAll()
.antMatchers("/member/signup/v2").permitAll()
.antMatchers("/member/login").permitAll()
.antMatchers("/member/login/google").permitAll()
.antMatchers("/member/login/kakao").permitAll()
.antMatchers("/member/login/apple").permitAll()
.antMatchers("/member/login/line").permitAll()
.antMatchers("/admin/member/login").permitAll()
.antMatchers("/creator-admin/member/login").permitAll()
.antMatchers("/member/forgot-password").permitAll()
.antMatchers("/stplat/terms_of_service").permitAll()
.antMatchers("/stplat/privacy_policy").permitAll()
.antMatchers("/charge/ads").permitAll()
.antMatchers("/v2/audio-content/main/home").permitAll()
.antMatchers("/v2/audio-content/main/home/popular-content-by-creator").permitAll()
.antMatchers("/v2/audio-content/main/home/content/ranking").permitAll()
.antMatchers("/api/home").permitAll()
.antMatchers("/api/home/latest-content").permitAll()
.antMatchers("/api/home/day-of-week-series").permitAll()
.antMatchers("/api/home/content-ranking").permitAll()
.antMatchers(HttpMethod.GET, "/api/live").permitAll()
.antMatchers(HttpMethod.GET, "/faq").permitAll()
.antMatchers(HttpMethod.GET, "/faq/category").permitAll()
.antMatchers("/audition").permitAll()
.antMatchers("/live/recommend/channel").permitAll()
.antMatchers(HttpMethod.GET, "/live/room").permitAll()
.antMatchers(HttpMethod.GET, "/event").permitAll()
.antMatchers(HttpMethod.GET, "/live/recommend").permitAll()
.antMatchers("/ad-tracking/app-launch").permitAll()
.antMatchers(HttpMethod.GET, "/notice/latest").permitAll()
.antMatchers(HttpMethod.GET, "/api/chat/character/main").permitAll()
.antMatchers(HttpMethod.GET, "/api/chat/room/list").permitAll()
.antMatchers(HttpMethod.GET, "/api/chat/original/list").permitAll()
.antMatchers(HttpMethod.POST, "/charge/payverse/webhook").permitAll()
.antMatchers(HttpMethod.GET, "/api/v2/home/recommendations").permitAll()
.antMatchers(HttpMethod.GET, "/api/v2/audio/recommendations").permitAll()
.antMatchers(HttpMethod.GET, "/api/v2/audio/contents").permitAll()
.antMatchers(HttpMethod.GET, "/api/v2/audio/rankings").permitAll()
.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/**"
}
}