Spring Security小教程 Vol 4. 使用用户名和密码验证身份-UsernamePasswordAuthenticationFilter

前言

上一期我们分享了Spring Security是如何通过AbstractAuthenticationProcessingFilter向Web应用向基于HTTP、浏览器的请求提供身份验证服务的。
这一次我们针对最常用,也是Spring Security默认在HTTP上使用的验证过滤器\color{red}{UsernamePasswordAuthenticationFilter}即基于用户名和密码的身份验证过滤器是如何与核心进行交互进行展开说明。目的是希望让大家对如何在Spring Security的核心上完成一个指定的身份验证协议的扩展工作,已经涉及相关主要组件及其角色职责有个初步的了解。
这一期的内容如果有了前几期对身份验证核心的背景,相对来说比较的简单,因为整个流程就是在原有的基础上更加具体化了场景:身份验证的数据来源是用户提交的请求,验证的凭证是用户名和密码。由于这样的原因,这一期更像是对前几期的一个综合性的应用总结。

第四期 UsernamePasswordAuthenticationFilter详细说明

本期的任务清单

  1. 了解UsernamePasswordAuthenticationFilter的职责和实现

1. 了解UsernamePasswordAuthenticationFilter的职责和实现

UsernamePasswordAuthenticationFilter类的说明

UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter针对使用用户名和密码进行身份验证而定制化的一个过滤器。
在一开始我们先通过下面的配图来回忆一下我们的老朋友AbstractAuthenticationProcessingFilter的在框架中的角色与职责。


AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter在整个身份验证的流程中主要处理的工作就是所有与Web资源相关的事情,并且将其封装成Authentication对象,最后调用AuthenticationManager的验证方法。所以UsernamePasswordAuthenticationFilter的工作大致也是如此,只不过在这个场景下更加明确了Authentication对象的封装数据的来源和形式——使用用户名和密码。

接着我们再对\color{red}{UsernamePasswordAuthenticationFilter}的属性和方法做一个快速的了解。UsernamePasswordAuthenticationFilter继承扩展了AbstractAuthenticationProcessingFilter,相对与AbstractAuthenticationProcessingFilter而言主要有以下几个改动:

  1. 属性中增加了username和password字段;
  2. 强制的只对POST请求应用;
  3. 重写了attemptAuthentication身份验证入口方法。
UsernamePasswordAuthenticationFilter的属性中额外增加了username和password字段

封装用户名密码的基石:UsernamePasswordAuthenticationToken

在UsernamePasswordAuthenticationFilter的属性声明中额外增加了username和password的动机很容易明白,即需要从HttpRequest中获取对应的参数字段,并将其封装进Authentication中传递给AuthenticationManager进行身份验证。这里让我们回顾下Authentication到底是什么?Authentication是一个接口声明,一个特定行为的声明,它并不是一个类,没有办法实例化为对象进行传递。所以我们首先需要对Authentication进行实现,使其可以被实例化。

Authentication接口声明

在UsernamePasswordAuthenticationFilter的身份验证设计里,我们需要验证协议用简单的语言可以描述为:给我一组用户名和密码,如果匹配,那么就算验证成功。用户名即是一个唯一可以标识不同用户的字段,而密码则是检验当前的身份验证是否正确的凭证信息。在Spring Security中便将使用username和password封装成Authentication的实现声明为了
\color{red}{UsernamePasswordAuthenticationToken}

UsernamePasswordAuthenticationToken继承了\color{red}{AbstractAuthenticationToken}抽象类,其主要与AbstractAuthenticationToken的区分就是针对使用用户名和密码验证的请求按照约定进行了一定的封装:将username赋值到了principal ,而将password赋值到了credentials。

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

通过UsernamePasswordAuthenticationToken实例化了Authentication接口,继而按照流程,将其传递给AuthenticationMananger调用身份验证核心完成相关工作。

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);

以上将来自HTTP请求中的参数按照预先约定放入赋值给Authentication指定属性,便是UsernamePasswordAuthenticationFilter部分最主要的改动。

验证核心的工作者:AuthenticationProvider

Web层的工作已经完成了,Authentication接口的实现类UsernamePasswordAuthenticationToken通过AuthenticationMananger提供的验证方法作为参数被传递到了身份验证的核心组件中。
我们曾多次强调过一个设计概念:AuthenticationManager接口设计上并不是用于完成特定的身份验证工作的,而是调用其所配发的AuthenticationProvider接口去实现的。
那么这里就有一个疑问,针对接口声明参数声明的Authentication,针对不同验证协议的AuthenticationProvider的实现类们是完成对应的工作的,并且AuthenticationManager是如何知道应该使用哪一个AuthenticationProvider才能完成对应协议的验证工作?
那么我们首先先复习下验证核心的大明星AuthenticationProvider接口的声明:


AuthenticationProvider接口声明

AuthenticationProvider只包含两个方法声明,核心验证方法入口:

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

另外一个便是让AuthenticationManager可以通过调用该方法辨别当前AuthenticationProvider是否是完成相应验证工作的supports方法:

     boolean supports(Class<?> authentication);

简单的描述便是AuthenticationProvider只有两个方法,一个是它不能验证当前的Authentication,还有便是让他去验证当前的Authentication。
对于AuthenticationProvider整个体系能说的非常多,本期只对我们“需要了解”的AuthenticationProvider中两个接口声明的方法做个最简单的说明。其他部分在以后单独对AuthenticationProvider体系介绍的时候再进一步展开。

是否支持当前验证协议:boolean supports(Class<?> authentication

在Spring Security中唯一AuthenticationManager的实现类\color{red}{ProviderManager},在处理authenticate身份验证入口方法的时,首先第一解决的问题便是:我手下哪个AuthenticationProvider能验证当前传入的Authentication?为此ProviderManager便会对其所有的AuthenticationProvider做supports方法检测,直到有AuthenticationProvider能在supports方法被调用后返回true。

我们了解了框架上的设计逻辑:先要知道知道谁能处理当前的身份验证信息请求再要求它进行验证工作。
回到我们的场景上来:UsernamePasswordAuthenticationFilter已经封装好了一个UsernamePasswordAuthenticationToken传递给了ProviderMananger。紧接着当前ProviderMananger正焦头烂额的询问哪个AuthenticationProvider能支持这个Authentication的实现类。此时ProviderMananger所处的情况大概就跟下图一般困惑:


ProviderMananger的炼狱生活

在ProviderMananger的视角里,所有的Authentication实现类都不具名,它不仅不能通过自身完成验证工作也不能独立完成判断是否支持的工作,而是统统交给AuthenticationProvider去完成。而不同的AuthenticationProvider开发初衷本就是为了支持指定的某种验证协议,所以在特定的AuthenticationProvider的视角中,他只关心当前Authentication是不是他预先设计处理的类型即可。
在使用用户名和密码的验证场景中,验证使用的用户名和密码被封装成了UsernamePasswordAuthenticationToken对象。Spring Security便为了向UsernamePasswordAuthenticationToken对象在核心层提供相关的验证服务便继承AuthenticationProvider开发了使用用户名和密码与UserDetailsService交互并且验证密码的\color{red}{DaoAuthenticationProvider}
DaoAuthenticationProvider是\color{red}{AbstractUserDetailsAuthenticationProvider}的实现类,DaoAuthenticationProvider针对UsernamePasswordAuthenticationToken的大部分逻辑都是通过AbstractUserDetailsAuthenticationProvider完成的。比如针对ProviderManager询问是否支持当前Authentication的supports方法:

    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
    }

可能有些同学对isAssignableFrom方法比较陌生,这是一个判断两个类之间是否存在继承关系使用的判断方法,DaoAuthenticationProvider会判断当前的Authentication的实现类是否是UsernamePasswordAuthenticationToken它本身,或者是扩展了UsernamePasswordAuthenticationToken的子孙类。返回true的场景只有一种,便是当前的Authentication是UsernamePasswordAuthenticationToken实现,换言之便是DaoAuthenticationProvider设计上需要进行处理的某种特定的验证协议的信息载体的实现。

核心验证逻辑:Authentication authenticate(Authentication authentication)

完成了是否支持的supports验证后,ProviderMananger便会全权将验证工作交由DaoAuthenticationProvider进行处理了。与ProviderMananger最不同一点是,在DaoAuthenticationProvider的视角里,当前的Authentication最起码一定是UsernamePasswordAuthenticationToken的形式了,不用和ProviderMananger一样因为匮乏信息而不知道干什么。
在DaoAuthenticationProvider分别会按照预先设计一样分别从principal和credentials获取用户名和密码进行验证。

String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

String presentedPassword = authentication.getCredentials().toString();

接着便是按照我们熟悉的预先设计流程,通过UserDetailsService使用username获取对应的UserDetails,最后通过对比密码是否一致,向PrivoderManager返回最终的身份验证结果与身份信息。这样一个特定场景使用用户名和密码的验证流程就完成了。

小结

我们先来总结下,当前出现过的针对用户名和密码扩展过的类与其为何被扩展的原因。

  1. UsernamePasswordAuthenticationFilter扩展AbstractAuthenticationProcessingFilter,因为需要从HTTP请求中从指定名称的参数获取用户名和密码,并且传递给验证核心;
  2. UsernamePasswordAuthenticationToken扩展Authentication,因为我们设计了一套约定将用户名和密码放入了指定的属性中以便核心读取使用;
  3. DaoAuthenticationProvider 扩展AuthenticationProvider,因为我们需要在核心中对UsernamePasswordAuthenticationToken进行处理,并按照约定读出用户名和密码使其可以进行身份验证操作。
客制化验证协议过程中涉及扩展的类

结尾

本章的重点是介绍特定场景下框架是如何通过扩展指定组件来完成预设验证逻辑的交互过程。其实整个验证工作核心部分是在DaoAuthenticationProvider中进行完成的,但是这部分内容涉及到具体的验证协议的实现逻辑非常复杂,本期就暂时略过,在一下期中我们将对验证核心最重要的组件AuthenticationProvider其依赖的组件和对应职责做一个全面的讲解。
我们下期再见。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容