史上最简单的Spring Security教程(二十五):UsernamePasswordAuthenFilter详解

本文详细介绍了Spring Security框架中表单登录Filter——UsernamePasswordAuthenticationFilter的工作原理,包括认证流程、Session策略、认证成功与失败处理,以及如何自定义AuthenticationFilter。

​在 Spring Security 框架中,最常用的 Filter 便是表单登录Filter,即 UsernamePasswordAuthenticationFilter。

下面我们就来一起详细了解一下这个 Filter 的具体功用。

首先,既然是Filter,势必要实现 Filter 接口吧,不过,确实实现了 Filter 接口。

从上图中,能清晰的了解到 UsernamePasswordAuthenticationFilter 的继承关系,也确实实现了 Filter 接口。

那么,我们便从 Filter 的核心方法 doFilter 开始看起,然后,内部贯穿说明其它涉及的逻辑。

 

判断

 

 

首先,便是是否需要认证的判断。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
​
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;
​
    if (!requiresAuthentication(request, response)) {
      chain.doFilter(request, response);
​
      return;
    }

如不需要认证,则直接执行下一个 Filter;如果需要认证,再向下继续进行。按照 Spring Security 框架的默认配置,此处只拦截 /login 的action,也即表单登录提交action,不会拦截其它请求。因此,这也就是解释了为何其它请求不会被 UsernamePasswordAuthenticationFilter 处理

此一节是通过 requiresAuthentication 方法来判断的。

protected boolean requiresAuthentication(HttpServletRequest request,
      HttpServletResponse response) {
    return requiresAuthenticationRequestMatcher.matches(request);
}

而 requiresAuthenticationRequestMatcher,看着不熟悉,其实,我们一开始就为其赋了值。

比如构造方法(基类AbstractAuthenticationProcessingFilter)。

protected AbstractAuthenticationProcessingFilter(
      RequestMatcher requiresAuthenticationRequestMatcher) {
    Assert.notNull(requiresAuthenticationRequestMatcher,
                   "requiresAuthenticationRequestMatcher cannot be null");
    this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}

同时,UsernamePasswordAuthenticationFilter 默认的无参构造方法,也对此进行了赋值(Ant风格路径匹配)。

public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login", "POST"));
}

注意,正是此处,默认了 /login(老版本为 /j_spring_security_check)为用户名密码验证路径,即 loginProcessingUrl(老版本为 filterProcessingUrl

基类 AbstractAuthenticationProcessingFilter 的无参构造方法为:

protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
    setFilterProcessesUrl(defaultFilterProcessesUrl);
}

基类的 setFilterProcessesUrl 方法和 requiresAuthenticationRequestMatcher 的 setter 方法一起,实现了对 requiresAuthenticationRequestMatcher 的赋值。

public void setFilterProcessesUrl(String filterProcessesUrl) {
    setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(
        filterProcessesUrl));
}
​
public final void setRequiresAuthenticationRequestMatcher(
    RequestMatcher requestMatcher) {
    Assert.notNull(requestMatcher, "requestMatcher cannot be null");
    this.requiresAuthenticationRequestMatcher = requestMatcher;
}

同时,表单登录配置器同时也默认了 UsernamePasswordAuthenticationFilter 作为表单登录验证的Filter,同时,提供了相关API以供相关配置属性的修改。

/**
 * Creates a new instance
 * @see HttpSecurity#formLogin()
 */
public FormLoginConfigurer() {
    super(new UsernamePasswordAuthenticationFilter(), null);
    usernameParameter("username");
    passwordParameter("password");
}
​
......
​
@Override
public FormLoginConfigurer<H> loginPage(String loginPage) {
    return super.loginPage(loginPage);
}
​
......
​
public FormLoginConfigurer<H> usernameParameter(String usernameParameter) {
    getAuthenticationFilter().setUsernameParameter(usernameParameter);
    return this;
}
​
......

 

尝试认证

 

 

如果需要进行认证,也即就是表单提交操作,此时,便需要进行用户名、密码验证。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
​
    ......
​
    try {
      authResult = attemptAuthentication(request, response);
      ......
    }

具体的验证逻辑,在 UsernamePasswordAuthenticationFilter 中的 attemptAuthentication方法。

public Authentication attemptAuthentication(HttpServletRequest request,
      HttpServletResponse response) throws AuthenticationException {
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
    }
​
    String username = obtainUsername(request);
    String password = obtainPassword(request);
​
    if (username == null) {
        username = "";
    }
​
    if (password == null) {
        password = "";
    }
​
    username = username.trim();
​
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);
​
    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);
​
    return this.getAuthenticationManager().authenticate(authRequest);
}

其实,此处的验证逻辑比较简单,最主要的便是调用 AuthenticationManager 进行身份认证

attemptAuthentication 方法主要是搜集参数(username、password),封装为验证请求(UsernamePasswordAuthenticationToken),然后调用AuthenticationManager 进行身份认证。关于认证的详细逻辑,后续会专门细讲,此处暂且不提。

关于此处设计,我们可以想象一下。为何核心逻辑都在基类 AbstractAuthenticationProcessingFilter 中,而尝试认证却需要子类 UsernamePasswordAuthenticationFilter 来实现?

其实,细品之后,不难发现其奥义。

无论哪一种认证方式,都需要先判断是否需要进行认证,然后认证成功了怎么样,失败了怎么样等等(诶,好像剧透了!!!)。

但是,认证需要的这些参数数据怎么来,甚至怎么认证,我就不管了,开发者可根据具体业务场景自行实现。

如 UsernamePasswordAuthenticationFilter,通过获取表单提交的参数,封装为表单登录认证特有的 UsernamePasswordAuthenticationToken 进行认证(后续还会讲解其它类型的认证方式哦,也就是不同的Token)。

 

Session策略

 

 

认证完成后(不发生异常),会有一个 Session 策略处理器处理 Session 的过程。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
​
    ......
​
    try {
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
        // return immediately as subclass has indicated that it hasn't completed
        // authentication
        return;
      }
      sessionStrategy.onAuthentication(authResult, request, response);
    }

具体怎么处理的,此处暂且不谈。

 

认证成功

 

 

如果认证过程中没有发生异常,那么即为认证成功。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
​
    ......
​
    successfulAuthentication(request, response, chain, authResult);
  }

具体的认证成功逻辑如下:

protected void successfulAuthentication(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, Authentication authResult)
      throws IOException, ServletException {
​
    if (logger.isDebugEnabled()) {
        logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                     + authResult);
    }
​
    SecurityContextHolder.getContext().setAuthentication(authResult);
​
    rememberMeServices.loginSuccess(request, response, authResult);
​
    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
            authResult, this.getClass()));
    }
​
    successHandler.onAuthenticationSuccess(request, response, authResult);
}

 

暂且先不管 rememberMeServices。单看其它逻辑,也不复杂。首先把认证后的结果(UsernamePasswordAuthenticationToken )放到了 SecurityContextHolder 属于当前线程的 SecurityContext 中;然后,使用 rememberMeServices 处理记住我逻辑;接着,如果事件发布器不为空,则发布认证成功消息,供其他 Bean 接收并处理;最后,调用 AuthenticationSuccessHandler 进行认证成功的相关操作。

此处简单说明一下,为何把认证后的结果(UsernamePasswordAuthenticationToken )放到了 SecurityContextHolder 属于当前线程的 SecurityContext 中。

一切皆因 SecurityContextHolder 的初始化方法和 strategyName 参数。

private static void initialize() {
    if (!StringUtils.hasText(strategyName)) {
        // Set default
        strategyName = MODE_THREADLOCAL;
    }
​
    if (strategyName.equals(MODE_THREADLOCAL)) {
        strategy = new ThreadLocalSecurityContextHolderStrategy();
    }
    else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
        strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
    }
    else if (strategyName.equals(MODE_GLOBAL)) {
        strategy = new GlobalSecurityContextHolderStrategy();
    }
    else {
        // Try to load a custom strategy
        try {
            Class<?> clazz = Class.forName(strategyName);
            Constructor<?> customStrategy = clazz.getConstructor();
            strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
        }
        catch (Exception ex) {
            ReflectionUtils.handleReflectionException(ex);
        }
    }
​
    initializeCount++;
}

由于 strategyName 参数默认为空,所以,SecurityContextHolderStrategy 便被初始化为 ThreadLocalSecurityContextHolderStrategy。当然,也可以设置其它类型的 ThreadLocalSecurityContextHolderStrategy,只需设置 strategyName 参数即可。

 

认证失败

 

 

如果认证过程中发生异常,即代表认证失败。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
​
    ......
​
    catch (InternalAuthenticationServiceException failed) {
      logger.error(
          "An internal error occurred while trying to authenticate the user.",
          failed);
      unsuccessfulAuthentication(request, response, failed);
​
      return;
    }
    catch (AuthenticationException failed) {
      // Authentication failed
      unsuccessfulAuthentication(request, response, failed);
​
      return;
    }
​
    ......
  }

认证过程中发生异常,一共有分为两种情况,一是不好区分具体的认证失败原因、场景的内部异常,即 InternalAuthenticationServiceException;另一种就是父异常 AuthenticationException。这个异常是所有认证异常的父类。除了内部异常打印了一句日志,其它也没有明显的区别。

认证失败的具体处理逻辑如下:

protected void unsuccessfulAuthentication(HttpServletRequest request,
      HttpServletResponse response, AuthenticationException failed)
      throws IOException, ServletException {
    SecurityContextHolder.clearContext();
​
    if (logger.isDebugEnabled()) {
        logger.debug("Authentication request failed: " + failed.toString(), failed);
        logger.debug("Updated SecurityContextHolder to contain null Authentication");
        logger.debug("Delegating to authentication failure handler " + failureHandler);
    }
​
    rememberMeServices.loginFail(request, response);
​
    failureHandler.onAuthenticationFailure(request, response, failed);
}

首先,清除放到了 SecurityContextHolder 属于当前线程的 SecurityContext 中的认证后的结果(UsernamePasswordAuthenticationToken ,然后,就是 rememberMeServices 的处理逻辑(暂时不谈);最后,便是调用认证失败handler,即 AuthenticationFailureHandler 来处理失败后的逻辑。

 

自定义AuthenticationFilter

 

 

既然我们搞懂了表单登录认证Filter UsernamePasswordAuthenticationFilter 的相关认证逻辑,那么我们不妨自己实现一个 AuthenticationFilter

就以CA登录认证为例吧。

首先,我们创建一个适用于CA登录认证的 CertificateAuthorityAuthenticationToken(类似于表单登录认证的UsernamePasswordAuthenticationToken )。由于CA密码在前端验证,所以无需 credentials 属性。

public class CertificateAuthorityAuthenticationToken extends AbstractAuthenticationToken {
​
  private final Object principal;
​
    // ~ Constructors
    // ===================================================================================================
​
    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>CertificateAuthorityAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     *
     */
    public CertificateAuthorityAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }
    
  ......
}

基本逻辑同 UsernamePasswordAuthenticationToken ,但是,CA登录最显著的特点在于其只需输入密码,无需输入账号(与账号提前绑定)。所以,免去了记忆的烦恼,同时也增加了安全性。当然,KEY丢失了不算哈~~。

然后,自定义 CertificateAuthorityAuthenticationFilter 以处理CA登录请求,默认 filterProcessesUrl 为 /certificate_authority_login

public class CertificateAuthorityAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
​
  ......
​
    public CertificateAuthorityAuthenticationFilter() {
        super(new AntPathRequestMatcher("/certificate_authority_login", "POST"));
    }
​
    // ~ Methods
    // ========================================================================================================
​
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
​
    ......
​
        CertificateAuthorityAuthenticationToken authRequest = new CertificateAuthorityAuthenticationToken(
                signature, password);
​
    ......
    }
​
    ......
}

至于CA登录的演示,这里就不做了。一是还需要自定义相应的 AuthenticationManagerAuthenticationProvider、登录页面等,这些都还没有讲解到;另外,最重要的是,我们没有CA KEY,也演示不了[尴尬][尴尬]。

其它详细源码,请参考文末源码链接,可自行下载后阅读。

我是银河架构师,十年饮冰,难凉热血,愿历尽千帆,归来仍是少年! 

如果文章对您有帮助,请举起您的小手,轻轻【三连】,这将是笔者持续创作的动力源泉。当然,如果文章有错误,或者您有任何的意见或建议,请留言。感谢您的阅读!

 

源码

 

 

github

https://github.com/liuminglei/SpringSecurityLearning/tree/master/25

 

gitee

https://gitee.com/xbd521/SpringSecurityLearning/tree/master/25

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值