FilterChain

Spring Security 核心是一些列的过滤链,请求和响应就是检查每个过滤链,其中的登录和验证只是其中的两个过滤器。

事实上重写SecurityFilterChain,完成你想要的功能可以非常简单,如果能明白其中原理,那么接下来的内容会非常简单。

    @Bean
    fun springSecurityFilterChain() = object : SecurityFilterChain {
        val log: Log = LogFactory.getLog(this.javaClass)
        override fun matches(request: HttpServletRequest) = true.also {
            log.debug("request path:${request.pathInfo}")
        }

        override fun getFilters(): MutableList<Filter> = mutableListOf(
            Filter { request, response, chain ->
                if (request.getParameter("passed") == "yes") {
                    chain.doFilter(request, response)
                }
                throw HttpClientErrorException.create(
                    HttpStatus.UNAUTHORIZED,
                    "unauthorized request",
                    HttpHeaders(),
                    "".toByteArray(),
                    null
                )
            })
    }

Filter接口传入当前请求与响应接口,还有下一条链,如果没有什么要做的,就执行next.doFilter(request, response)完成后续过滤。这个组件在fiber里面叫middleware,其他Go编写的Http框架都是中间件。如果不明白,可以看看责任链模式

Authentication

Authentication表示我是谁,我能做什么,算是请求的安全上下文。这个信息一般由前面的链完成创建,这只是一个声明,无关操作或是状态,登陆不登陆都可以生成Authentication信息。生成的信息包装成 SecurityContext 存储在 ThreadLocal 里面,所以你可以在任何地方调用 SecurityContextHolder.getContext() 获取安全上下文信息,即使不是http请求也是合理的。

interface Authentication : Principal, Serializable {
    val authorities: Collection<GrantedAuthority>

    val credentials: Any?

    val details: Any?

    val principal: Any?
    
    var isAuthenticated: Boolean
}

认证

这一步对应 Authentication 对象如何生成,用户登录可以是用户名和密码,可以是站内的token,可以是第三方平台的回调。生成的过程是在上述过滤链里面完成。

这个步骤就是输入认证需要的信息,并且输出一个凭证,即使是匿名登录,也需要返回一个代表游客的身份凭证。

鉴权

鉴权是把 Authentication.authorities 作为根据,判断操作是否能通过。鉴权的地方非常多,基础的包括根据请求地址判断,其他的注解在 Controller、Repository 都可以有,原理因为上下文存储在 ThreadLocal 里面。

无关过滤链

如果仔细观察 Authentication 就会发现,认证是写入 Authentication 到 SecurityContext,鉴权就是读取 SecurityContext 里面的 Authentication 的信息。这个和过滤链是独立开来的两个部分,组合在一起使用只是因为刚好 Http 请求先通过的是过滤链,如果使用自己的 rpc 那么对应的就是 rpc 的前置钩子。

// 认证过程
SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken("admin", "")
// 鉴权过程
log.debug("name: ${SecurityContextHolder.getContext()?.authentication?.name}")

其他

我觉得上面的内容差不多就可以解释 Spring Security 如何工作的,如果还有什么可以讲的那么就是那个授权管理器。HttpBasic/FormLogin/OAuth2/X509 这几个鉴权模式是开箱即用,他们都使用到一个AuthenticationManager这个接口。

fun interface AuthenticationManager {
    fun authenticate(authentication: Authentication): Authentication
}

用户认证只是 Spring Security 其中的一个部分,其他的部分还包括 cors 策略,请求拦截,缓存信息等等。过滤链注册顺序可以参考 SecurityWebFiltersOrder 定义的顺序。这些都是使用Spring Web组件的情况下使用 ThreadLocal ,如果使用的是Spring WebFlux,相关的类和接口存在差异,最重要的是请求穿过过滤链的时候没法保证都是在同一个线程上运行,因此并不能直接使用 ThreadLocal。

20220508

在新版本的中WebSecurityConfigurerAdapter已经被标记为弃用,所以我也将原先的代码进行了迁移,并没有改动多少,新的配置取消继承WebSecurityConfigurerAdapter,编写一个方法依赖 HttpSecurity,然后使用HttpSecurity生成SecurityFilterChain注入到容器里面。我们最终的目的是生成过滤链,实际过程中过滤链可能非常长,含有非常多的东西,HttpSecurity就是这么一个工具,后面的调用build方法间接调用到performBuild,就是排序所有用到的过滤器组合起来完成过滤链。

    @Bean
    fun securityFilterChain (http: HttpSecurity): SecurityFilterChain {
        http.csrf()
            .disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        return http.build()
    }