You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用OncePerRequestFilter自定义认证致Spring异常映射为403的解决方法

Spring Security自定义过滤器异常响应码被映射为403的解决方法

问题背景

  • 自定义Telegram认证过滤器TelegramAuthenticationFilter,从请求头获取认证数据并将认证对象放入安全上下文完成用户认证
  • 当业务逻辑抛出异常(如数据库未找到对应记录)时,响应码被映射为403而非预期的500
  • 调试发现ExceptionTranslationFilter先捕获到业务逻辑异常,但后续AuthorizationFilter的doFilterMethod()抛出AccessDeniedException,最终导致响应码变为403

现有代码实现

自定义认证过滤器

package com.fnot.backend.config.spring.security

import com.fnot.backend.config.external.telegram.TelegramConfig
import com.fnot.backend.domain.external.telegram.bot.service.TelegramBotService
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class TelegramAuthenticationFilter(
    telegramConfig: TelegramConfig,
    private val telegramBotService: TelegramBotService
): OncePerRequestFilter() {

    private val botToken = telegramConfig.botToken

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val authHeader = request.getHeader("TG-Authorization")
        if(authHeader == null)  {
            filterChain.doFilter(request, response)
            return
        }

        val tmaAuthData = TmaAuthData(authHeader, botToken)

        if (tmaAuthData.isValid()) {
            val telegramUserData = tmaAuthData.getTelegramUserData()
            val user = telegramBotService.registerOrUpdateUserTma(telegramUserData)

            val authToken = TelegramAuthenticationToken(user.id!!)
            SecurityContextHolder.getContext().authentication = authToken
        }

        filterChain.doFilter(request, response)
    }
}

Security配置类

package com.fnot.backend.config.spring.security

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import org.springframework.web.filter.CorsFilter

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val telegramAuthenticationFilter: TelegramAuthenticationFilter
) {
    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .cors {  } // Enable CORS
            .authorizeHttpRequests { auth ->
                auth.requestMatchers("/").permitAll()
                .anyRequest().authenticated()
            }
            .addFilterBefore(telegramAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)

        return http.build()
    }

    @Bean
    fun corsFilter(): CorsFilter {
        val source = UrlBasedCorsConfigurationSource()
        val config = CorsConfiguration()

        config.allowCredentials = true
        config.addAllowedOriginPattern("*") // Allow all origins
        config.addAllowedHeader("*") // Allow all headers
        config.addAllowedMethod("*") // Allow all methods

        source.registerCorsConfiguration("/**", config)
        return CorsFilter(source)
    }
}

问题原因

当自定义过滤器的认证逻辑抛出业务异常时,SecurityContextHolder可能未被设置有效认证对象,后续AuthorizationFilter检查请求认证状态时,会因无有效认证信息抛出AccessDeniedException,覆盖了原本的业务异常,最终返回403响应码。

解决方案

修改自定义过滤器

在认证逻辑块添加异常捕获,清除安全上下文后重新抛出异常,确保业务异常能被Spring异常处理器正常处理:

package com.fnot.backend.config.spring.security

import com.fnot.backend.config.external.telegram.TelegramConfig
import com.fnot.backend.domain.external.telegram.bot.service.TelegramBotService
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class TelegramAuthenticationFilter(
    telegramConfig: TelegramConfig,
    private val telegramBotService: TelegramBotService
): OncePerRequestFilter() {

    private val botToken = telegramConfig.botToken

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val authHeader = request.getHeader("TG-Authorization")
        if(authHeader == null)  {
            filterChain.doFilter(request, response)
            return
        }

        val tmaAuthData = TmaAuthData(authHeader, botToken)

        if (tmaAuthData.isValid()) {
            try {
                val telegramUserData = tmaAuthData.getTelegramUserData()
                val user = telegramBotService.registerOrUpdateUserTma(telegramUserData)

                val authToken = TelegramAuthenticationToken(user.id!!)
                SecurityContextHolder.getContext().authentication = authToken
            } catch (e: Exception) {
                // 清除安全上下文,避免后续过滤器误判
                SecurityContextHolder.clearContext()
                // 抛出异常交由全局处理器处理
                throw e
            }
        }

        filterChain.doFilter(request, response)
    }
}

添加全局异常处理器

创建全局异常处理器,统一处理所有未捕获异常,返回500响应码:

package com.fnot.backend.config.exception

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(Exception::class)
    fun handleAllExceptions(e: Exception): ResponseEntity<Map<String, String>> {
        val errorResponse = mapOf(
            "message" to e.message ?: "服务器内部错误",
            "status" to HttpStatus.INTERNAL_SERVER_ERROR.value().toString()
        )
        return ResponseEntity(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR)
    }
}

关键说明

  1. 捕获认证逻辑异常后先清除安全上下文,避免后续安全过滤器因无效上下文抛出权限异常
  2. 重新抛出异常,让全局异常处理器统一捕获并返回500响应
  3. 全局异常处理器确保所有未处理异常都能得到一致的错误响应格式和状态码

内容的提问来源于stack exchange,提问作者Arthur Klezovich

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 23:22:24