Spring Security 中的身份认证

原文连接:https://blog.gaoyuexiang.cn/2020/06/07/spring-security-authentication/,内容无差异。java

本文介绍 Spring Security 的身份认证的内容,研究 Spring Security 自带的身份认证方式和添加本身的身份认证方式的方法。spring

身份认证相关组件

上一篇文章中,咱们了解到了 Spring Security 会将 DelegatingFilterProxy 插入到 Servlet Filter Chain 中,而后将要过滤的请求经过 FilterChainProxy 代理给匹配的 SecurityFilterChain;这些 SecurityFilterChain 中包含着真正作安全相关工做的 Filter数据库

后面提到的 Filter 都是红色方框中的

这些 Filter 中的一部分,他们的职责就是进行身份验证,好比 UsernamePasswordAuthenticationFilter;而他们中的大多数都有一个共同的父类 AbstractAuthenticationProcessingFiltersegmentfault

AbstractAuthenticationProcessFilter

这个类是不少身份认证的 Filter 的父类,它已经实现了 doFilter 方法,流程以下:安全

本文不涉及其中的 sessionStrategy 部分

doFilter 已经帮咱们搭好了这个流程,咱们只须要关心其中的几个被调用的方法(红绿蓝三个颜色框)就能够了。session

attemptAuthentication

这是一个抽象方法。子类实现的时候,须要从 HttpServletRequest 中获取须要的信息,构建出一个 Authentication 实例,而后调用父类中的 AuthenticationManager.authenticate() 方法,对 Authentication 对象进行认证。mvc

unsuccessfulAuthentication

这个方法已经被实现,子类也能够选择重写。根据父类的实现,这个方法将完成一下步骤:ide

  1. 清理 SecurityContextHolder
  2. 清除 RememberMeService 中的信息(默认使用 NullRememberMeService
  3. 调用 AuthenticationFailureHandler.onAuthenticationFailure() 方法

默认使用的 AuthenticationFailureHandlerSimpleUrlAuthenticationFailureHandler,它的逻辑是:spring-boot

  1. 若是没有配置 defaultFailureUrl (默认没有)网站

    1. 发送 401 响应
  2. 根据配置的布尔值 forwardToDestination (默认为 false)判断

    1. 使用 Servlet forward 到配置的 defaultFailureUrl
    2. 使用 HTTP redirect 到配置的 defaultFailureUrl

successfulAuthentication

unsuccessfulAuthentication 方法同样,这个方法也已经实现,而且能够被重写,但其中的逻辑却刚好相反:

  1. attemptAuthentication 返回的 Authentication 对象保存到 SecurityContextHolder
  2. 保存登录信息到 RememberMeService
  3. 发布 InteractiveAuthenticationSuccessEvent 事件,这样能够被配置的 EventListener 处理
  4. 调用 AuthenticationSuccessHandler.onAuthenticationSuccess() 方法

默认使用的 AuthenticationSuccessHandlerSavedRequestAwareAuthenticationSuccessHandler,其实现就是一次重定向。咱们能够看看它重定向到哪里:

  • 当配置了 alwaysUseDefaultTargetUrl 或指定了 targetUrlParameter 且此参数存在的时候

    • 若是配置了 alwaysUseDefaultTargetUrl 则重定向到 defaultTargetUrl,默认是 /
    • 若是存在 targetUrlParameter(好比 redirect_uri 之类的比较常见的参数),则重定向到这个路径
    • 若是存在 Referer,则重定向到这个地址
    • 重定向到 /
  • RequestCache 中找到了保存的请求

    • 重定向到请求中设置的重定向地址
  • 若是仍是没有知足条件,则进行第一步里的逻辑
关于 RequestCache:想象你正在访问一个须要认证的资源,这个时候网站会把你重定向到登录页面;在你登录成功后,又会重定向回刚才的资源。 RequestCache 就是为了保存登录以前的请求而设计的。在这里,默认使用基于 session 的实现。

AuthenticationManager

AbstractAuthenticationProcessingFilter 中保存了一个 AuthenticationManager,它会在子类的 attemptAuthentication 方法中被使用。其职责是对 Filter 建立的 Authentication 对象进行身份验证,好比查询数据库匹配用户名密码、携带的 token 是否合法等。

ProviderManager 与 AuthenticationProvider

这是 AuthenticationManager 经常使用的实现。它没有实现任何认证逻辑,而是管理了一些 AuthenticationProvider,经过这些 provider 来实现真正的认证功能。

每一个 AuthenticationProvider 实现一种特定类型的身份认证方式,好比用户名密码登录、OIDC 登录等。他们能够经过 Authentication 的具体类型来判断是否支持这种 Authentication 须要的认证方式。

其余的一些 Filter

除了 AbstractAuthenticationProcessFilter,还有一些进行身份验证的 Filter,它们并无继承这个类,而是基于 OncePerRequestFilter 本身实现了一套逻辑。这些 Filter 包括 AuthenticationFilterBasicAuthenticationFilterOAuth2AuthorizationCodeGrantFilter 等等。

因为它们再也不是 AbstractAuthenticationProcessFilter,因此不会再被要求使用 AuthenticationManager。尽管这样,当咱们选择使用 OncePerReuqestFilter 来实现自定义的身份认证时,仍然能够考虑使用 AuthenticationManager 这种方式。

我的以为 AuthenticationManager 还算是个不错的设计,由于作到了职责分离。

甚至还有更加放飞自个人 DigestAuthenticationFilter,直接继承 GenericFilterBean,在实现上也是我行我素,这里就不探究了。

ExceptionTranslationFilter

这个 Filter 不是用来进行身份验证的,而是用来处理认证受权过程当中产生的异常的。它能够处理 AuthenticationExceptionAccessDeniedException,分别表示认证失败和受权失败。这篇文章只关心如何处理 AuthenticationException

可是这个 Filter 默认被安排在 SecurityFilterChain 的倒数第二位,因此前面的 Filter 抛出的异常并不能被它捕获。但自定义的 Filter 能够加到它后面,这样就能够利用它来处理这两种异常。

最后一位是 FilterSecurityInterceptor,可能会抛出 AccessDeniedException

处理 AuthenticationException

ExceptionTranslationFilterAuthenticationException 的处理分三步:

  1. 清理 SecurityContextHolder 中的身份信息
  2. 将当前的 request、response 保存到 RequestCache 中(用途能够回顾一下 successfulAuthentication 方法
  3. 调用 AuthenticationEntryPoint.commence() 方法

其中的 AuthenticationEntryPoint 具体实例取决于你的配置,默认会用到 BasicAuthenticationEntryPoint。这个接口的职责就是经过 WWW-Authenticate header 告诉客户端使用哪一种方式进行身份验证。

处理其余异常

对于 AuthenticationExceptionAccessDeniedException 以外的异常,ExceptionTranslationFilter 会将其转换成 ServletExceptionRuntimeException 抛出。

若是想要处理这些异常,须要本身添加 Filter 实现。

Spring Security 自动配置的 FilterChainProxy

当咱们启动 Spring 应用以后,会在日志里看到打印全部配置的 FilterChainProxy

默认状况下,咱们会看到这样的一条链:

这是引入 spring-boot-starter-security 以后自动配置的 FilterChainProxy,在引入更多的 security 相关的依赖和编写了相关配置以后,这个 filter chain 也会相应变化。

几种内置的身份认证方式

接下来,咱们以 UsernamePawwrodAuthenticationFilterBasicAuthenticationFilter 为例,看看他们是如何实现身份认证的。

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 是一个 AbstractAuthenticationProcessingFilter 的子类,实现了 attemptAuthentication 方法,没有重写其余方法。因此用户认证成功后,会被重定向到一个地址,具体逻辑参考上面的 successfulAuthentication 方法

attemptAuthentication

attemptAuthentication 方法会从 HttpServletRequest.getParameter() 方法中获取用户名密码,从而进行身份验证。具体从哪里获取用户名密码,则能够被子类经过重写 obtainUsername()obtainPassword() 方法修改。

以后,UsernamePasswordAuthenticationFilter 会构建出一个 UsernameAuthenticationToken,交给 AuthenticationManager 进行认证。

DaoAuthenticationProvider

这是 UsernamePasswordAuthenticationFilter 对应的 AuthenticationProvider,负责对 UsernameAuthenticationToken 进行认证。

它使用一个 UserDetailsService 来加载用户信息,使用 PasswordEncoder 来匹配用户的密码。

这两个接口具体使用哪个实现,取决于具体的配置。好比 UserDetailsService 就有 in memory 和 JDBC 的实现。

UsernamePasswordAuthenticationFilter 是用于单独处理登陆的 Filter,它不是用来在请求业务 API 时进行身份认证的 Filter

事实上,全部继承了 AbstractAuthenticationProcessFilter 但没有重写 successfulAuthentication 方法的 Filter 都是这样的,它们会在登录成功后重定向到登陆前的地址或默认的地址。这也符合它的语义:进行身份认证流程,而不是业务请求的一部分。

BasicAuthenticationFilter

UsernamePasswordAuthenticationFilter 不一样,BasicAuthenticationFilter 没有继承 AbstractAuthenticationProcessingFilter,而是直接继承 OncePerRequestFilter。由于它是被使用在请求业务 API 的请求上,而不是进行身份认证流程。

BasicAuthenticationFilter 的实现并不复杂,无非是从 Authorization header 中取出用户名密码,而后建立出 UsernameAuthenticationToken,接着调用 AuthenticationManager.authenticate() 方法。

之因此它也会使用 AuthenticationManager,应该是出于复用的考虑。这样它就可使用和 UsernamePasswordAuthenticationFilter 同样的 AuthenticationProvider

它与 UsernamePasswordAuthenticationFilter 的区别在于认证以后的行为。

不管认证成功与否,BasicAuthenticationFilter 都不会作出重定向的响应。

  • 若是认证失败,则经过默认的 BasicAuthenticationEntryPoint 返回 401 响应
  • 若是认证成功,则继续执行 filter chain,这样就能执行到真正的业务方法

如何添加本身的身份认证方式

前面介绍了两种不一样的 Filter 实现,以及它们被使用的场景,如今咱们知道了该选择哪种方式去实现自定义的 Filter。可是,如何把它们加入到 SecurityFilterChain 中去处理身份认证呢?

配置 SecurityFilterChain

咱们若是须要任何对 SecurityFilterChain 的配置,都须要扩展 WebSecurityConfigurerAdapter,实现本身的一个配置类。每建立这样的一个实现,都会建立一个 SecurityFilterChain 加入到 FilterChainProxy 中。

配置 requestMathcer

咱们在前一篇文章提到过,FilterChainProxy 须要根据 url 来判断选择哪个 SecurityFilterChain。咱们须要将这个配置写到这个实现类中,好比:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.requestMatchers(matcher -> matcher.mvcMatchers("/hello"));
}

这样,FilterChainProxy 就知道了对 /hello 的请求须要使用这个 SecurityFilterChain

向 SecurityFilterChain 加入 Filter

如今有了对应的 SecurityFilterChain,咱们就能够将自定义的 Filter 加入到这个 chain 中:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.addFilter(HelloFilter.class);
}

addFilter 方法也有一些变体,能够控制 Filter 在 chain 中的位置,这里就不赘述了。

添加 AuthenticationProvider

Filter 同样,AuthenticationProvider 也是被安排到单独的 FilterChainProxy 中的,而且须要本身配置。若是你的自定义 Filter 须要 AuthenticationProvider 的话,一样须要配置:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth.authenticationProvider(HelloAuthenticationProvider.class);
}

总结

这篇文章比较详细的梳理了 AbstractAuthenticationProcessingFilter 及其子类 UsernamePasswordAuthenticationFilter 的实现和 BasicAuthenticationFilter 的实现,了解了须要实现自定义身份验证的 Filter 时应该选择哪一种方式:

  • 只是进行身份验证,完成后进行重定向,而不调用业务方法,那么就继承 AbstractAuthenticationProcessFilter
  • 须要调用业务方法,身份验证是为了保护业务,那么就继承 OncePerRequestFilter,彻底控制认证的流程
固然,这不是一个强制的限制,你仍然能够经过重写 AbstractAuthenticationProcessFilter.successfulAuthentication() 方法来修改重定向的行为。

另外,也了解到了实现完 Filter 后,须要实现 WebSecurityConfigurerAdapter,将 Filter 加入到 SecurityFilterChain 中。

👉 查看系列文章
相关文章
相关标签/搜索