Spring Security小教程 Vol 5.核心组件AuthenticationManager专题

前言

我们在某一期其实已经对Authentication身份验证中的主要组件进行过介绍,并且通过几期的分享让大家大致了解了Web应用大致利用核心进行身份验证的流程和相关的扩展点。
这一期我们用一期的篇章把关注点放在Authentication身份验证核心最主要的几个服务AuthenticationMananger、AuthenticationProvider和UserDetailsService,三个顶层接口进行展开。通过Spring Security对这些接口服务的实现进行说明讲解。目的是为了将来客制化扩展核心服务做好知识储备。

第五期 核心组件AuthenticationManager专题

本期的任务清单

  1. AuthenticationMananger与ProviderMananger
  2. Authentication与AuthenticationProvider
  3. UserDetailsService接口和它的实现类

零、整体概述

本期的重点是Authentication身份验证几个核心服务接口:

  1. AuthenticationMananger
  2. AuthenticationProvide
  3. UserDetailsServic

额外的还包括两个负责封装用户身份信息的接口与类:

  1. Authentication
  2. UserDetails

我们回顾下前几期我们在分享Spring Security核心组件时候曾用到过以下这张图比较重要的核心组件:


Spring Security核心组件

这一期的重点显而易见便是红框中身份验证部分的三个核心组件以及其相关的组件。

一、AuthenticationMananger与ProviderMananger

AuthenticationMananger作为整个身份验证核心最外层的封装负责与外部使用者进行交互。
AuthenticationMananger接口有且仅有一个对外的服务便是“身份验证”。这样是整个身份验证服务对外提供的服务接口。

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

外部使用者通过将身份验证的必要信息,比如用户名和密码封装一个Authentication传递、调用AuthenticationMananger的authenticate方法。如果没有返回异常和null值,那么验证服务便是完成。完成身份验证的Authentication不仅包含了用户的身份验证信息,比如用户名,额外还会将该用户身份下所有对应的权限列表也一并封装返回。


AuthenticationMananger的authenticate是整个验证服务的入口

身份信息交互的纽带:Authentication

在整个与外部使用交互的过程中Authentication的职责有两个,第一个是封装了验证请求的参数,第二个便是封装了用户的权限信息。结合Authentication的接口设计便更加清晰了这样的设计意图:principal用于存放用户的身份标识信息,比如用户名,credentials用于存放用户的验证凭证比如密码,authorities用于存放用户的权限列表。而details则存放了除了用户名和密码其他可能会被用于身份验证的信息,比如应用限定用户的使用ip范围场景下,ip信息可能便会被存放在details做辅助的验证信息使用。


Authentication设计

唯一的实现类:ProviderMananger

为了向外部提供身份验证服务,Spring Security中通过\color{red}{ProviderMananger}实现了Authentication。作为实现类ProviderMananger便不能和AuthenticationManager一样只关心唯一的抽象核心服务authenticate。在ProviderMananger为了管理外部输入与像外部返回的Authentication,ProviderMananger内部大致的工序如下:

  1. 首先,寻找可以进行验证当前外部输入Authentication形式的AuthenticationProvider;如果自身的providers中无法处理验证并且当前层次的Mananger还有父级的Mananger则向上传递,交由父层Mananger进行处理;
  2. 然后,因为details的信息是外部传入的,内部身份验证后的Authentication并不会从持久化或者其他数据源中携带,在返回前将details写入返回给外部的Authentication;
  3. 最后,如果有必要则将外部身份验证请求中的敏感擦除,比如讲请求验证的密码置空。
    了解了ProviderMananger完成的三件工作,大致明白了虽然整个验证框架只有一个ProviderManager暴露在外部,但是其内部可能是有多个AuthenticationMananger和AuthenticationProvider组成的网络,并且最终进行核心身份验证的还是AuthenticationProvider。核心在叶子节点中依次寻找对验证当前Authentication形式的AuthenticationProvider。如果存在支持便将验证请求的Authentication传递给AuthenticationProvider,委托其进行验证。在处理输入的验证请求Authentication,ProviderMananger并不对其进行任何的处理,而是指在处理完后进行必要的加工和处理。


    一个验证服务可能的组件结构

二、 Authentication与AuthenticationProvider

相对AuthenticationMananger而言AuthenticationProvider的工作更加明确:针对特定的验证数据,提供特定的验证行为。在这个语境下,Authentication的设计目的是解决验证什么(What)的问题,而authenticate方法更像是在回答怎么验证的问题(How)。

AuthenticationProvider视角中的Authentication

那么我们先对验证数据也就是Authentication的设计进行展开讨论。Authentication主要职责就是封装身份验证时候需要的信息数据,比如用户名场景下的用户名和密码,短信验证码下的手机号码和验证码,OAuth2场景下的ID和Code。总之每个不同验证协议使用的验证信息都需要被被封装成Authentication,更准确说在Spring Security把这种封装了用户身份验证信息的Authentication具体为了\color{red}{AuthenticationToken}的概念,毕竟一说token更容易理解。所有Spring Security中提供的各种协议的身份验证数据的封装都继承\color{red}{AbstractAuthenticationToken},基于用户名和密码的UsernamePasswordAuthenticationToken,基于OAuth2的OAuth2AuthorizationCodeAuthenticationToken,基于CAS的CasAssertionAuthenticationToken。
通常我们使用用户名和密码的场景是最多,无论是使用基于数据库持久化的用户名密码方案还是基于LDAP的用户名和密码方法。虽然验证在验证协实现有细微差别,但是无论使用验证LDAP还是数据库进行身份验证比对,因为用户提交的验证身份信息几乎一致,我们便可以复用通用结构的AuthenticationToken——将username赋值到principal属性并将password赋值到cencredentials属性中。这样就意味着我们在AuthenticationToken设计上最需要考虑是数据的封装,而不是身份验证行为的实现。

各式各样的AuthenticationToken

我们已经解决了第一个问题,在AuthenticationProvider验证数据放都可以通过传入Authentication的各种实现类AuthenticationToken进行获取。下一个问题便是AuthenticationProvider是如何进行身份验证的。
我们假设的场景是需要对使用用户名和密码的UsernamePasswordAuthenticationToken进行验证。
在Spring Security针对UsernamePasswordAuthenticationToken进行身份验证的有主要有两个AuthenticationProvider:一个是基于Dao模型与数据层用户信息对比验证的DaoAuthenticationProvider,另外一个是虽然同样使用用户名和密码,但是验证流程更加复杂,且用户数据是通过与LDAP服务进行用户验证的LdapAuthenticationProvider。在这里使用最广泛使用的基于Dao的DaoAuthenticationProvider进行说明,如果有对Ldap实现有兴趣的相信在看完对DaoAuthenticationProvider的分析之后再阅读LdapAuthenticationProvider部分的代码就会轻松许多。


DaoAuthenticationProvider与LdapAuthenticationProvider都基于UsernamePasswordAuthenticationToken的形式进行验证

DaoAuthenticationProvider

DaoAuthenticationProvider为了实现外部的验证请求便需要对外部传递身份信息——用户名和密码进行验证。我们把这个任务进一步分解成两个独立的任务:

  1. 从数据层获取对应用户名在数据层的数据记录;
  2. 对外部的用户名、密码与数据层的用户名、密码进行比对。

为什么在DaoAuthenticationProvider会将验证任务再分解成这两个独立任务,最大原因便是,这两个任务一个感知外部资源,另一个感知验证算法,两种都是不同用户可能存在不同的使用场景,框架并无法控制具体的实现。
第一个任务,从数据层获取对应用户名在数据层的数据记录,我们的目标是从数据层中查找到我们需要比对的用户身份数据,但是在这个场景下我们无非控制的是数据层的实现具体是什么?是通过JDBC访问Mysql还是通过JPA访问Oracle,更或是直接通过内存访问一个存储了用户信息键值对的Map?
第二个任务,对外部的用户名、密码与数据层的用户名、密码进行比对,具体的加密算法是什么?如何实现的?
这两个问题在DaoAuthenticationProvider中都无法给出明确的实现。Spring Security便将这两种在DaoAuthenticationProvider无法确定、存在变化的行为分别委托给了DaoAuthenticationProvider两个重要组件去完成:

  1. 通过用户名返回数据层中的用户信息的UserDetailsService;
  2. 通过特定加密算法处理用户密码的PasswordEncoder。


    DaoAuthenticationProvider中的两个组件

使用UserDetailsService获取内部用户身份信息

UserDetailsService接口定位从他的接口方法就可以明白,就是向核心组件们提供数据层的用户信息,而用户信息在这里被封装成了UserDetails。


UserDetails与UserDetailsService

UserDetailsService中只有一个方法便是loadUserByUsername方法,通过传入用户名返回数据层的用户身份记录。

    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

当我们确定我们获取用户身份信息的方法之后,我们便可以自行扩展UserDetailsService方法,告知框架如何获取用户身份信息。通常这个步骤是使用Spring Security中是必须完成的工作。
我们在第一个章节中曾经写过以下代码用于配置我们使用的UserDetailsService:

public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {
    //注入新的UserDetailsServiceBean
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        return manager;
    }
}

那一次我们使用了基于内存键值对的形式来存储和获取用户信息记录。同样的我们也可以通过JDBC和JPA来获取用户身份信息,而Spring Security很贴心的已经提供了一个基于JDBC的UserServiceDetails实现和对应的模板DDL:


JBCD的UserServiceDetails实现JdbcImpl
create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

而通过UserDetailsService返回的数据类型是UserDetails,UserDetails中封装了用户名、密码和授权信息同时还额外包括了一些过期和锁定的标识属性。我们不难发现UserDetails封装的数据和Authentication非常的相似。没错,在身份验证成功后,DaoAuthenticationProvider便会将内部的UserDetails抽离必要的数据对应赋值到UsernamePasswordAuthenticationToken最终返回给外部调用者进行使用。我们可以简单的把UserDetails理解为用户身份信息在数据层的封装。在客制化的过程中,如果用户信息的数据结构是比较特殊的结构,比如Ldap,那么便可以自行扩展UserDetails客制化一个特殊的结构用于获取用户数据记录。
说到这里基本上我们已经了解了DaoAuthenticationProvider两个组件中用于获取用户身份记录的UserDetailsService部分,下面我们介绍下处理密码验证的PasswordEncoder部分。

使用PasswordEncoder来进行密码的比对

我们继续追踪上面的场景来说下关于验证算法部分。DaoAuthenticationProvider收到了外部提交的用户名和密码,同样的DaoAuthenticationProvider也查找到了对应用户名在数据库中的用户名和密码。通常情况下虽然都是密码,数据库中存储的密码通常会进行过一定的加密。DaoAuthenticationProvider便需要将外部提交的用户名和密码进行一次加密流程并进行比对。举个例子我们当前使用的算法比如是MD5,加密明文的样本是用户的用户名拼接用户名。如有当前需要身份验证的请求中用户名是admin,密码是password。同样的在数据库中加密后的密码是9b02edfbc208a538。我们便需要对外部传递的用户名和密码做一个MD5("passwordadmin")得到9b02edfbc208a538,再与数据库中的password字段进行对比,如果一致则认定验证成功。
在Spring Security中这种针对处理称为\color{red}{PasswordEncoder},PasswordEncoder接口主要的作用就是对明文密码进行加密与比对。

PasswordEncoder接口

通过PasswordEncoder比对密码的示意图

Spring Security中默认向DaoAuthenticationProvider提供的PasswordEncoder是BCryptPasswordEncoder。如果对BCrypt可以额外通过谷歌去了解加密流程。
如果我们需要客制化自己的加密算法,只要实现PasswordEncoder接口,并重新通过Spring注入DaoAuthenticationProvider便可以了。

@Bean
public PasswordEncoder passwordEncoder() {
    //通过修改注入的实例,客制化自己的PasswordEncoder
    return new BCryptPasswordEncoder();
}

PasswordEncoder 部分的功能相对较少,通常情况下使用默认提供的BCryptPasswordEncoder就足够完成任务。

结尾

在本期我们花了很大篇幅介绍了身份验证核心中最主要个几个组件和基于一个使用用户名和密码验证场景下对应接口的实现类的具体职责:

  1. AuthenticationManager负责核心验证前后的处理,并且负责与外部调用者进行交互;
  2. AuthenticationProvider是验证服务的核心实现,其验证的数据形式是AuthenticationToken其中封装了验证使用的用户标识和用户验证凭证信息;
  3. AuthenticationProvider中获取内部用户身份信息是通过UserDetailsService完成的。如需要进行密码处理,则引入了PasswordEncoder;
  4. UserDetails封装了用户信息在内部的结构,在向外部返回Authentication之前,AuthenticationProvider通常会将UserDetails必要的数据复制到向外部返回的AuthenticationToken中。

通过几期的说明,整个Spring Security关于身份验证的组件、流程和特定场景的实现基本我们都了解了一遍。从下一期开始,我们会开始讲解访问控制部分、框架配置部分的设计与概念。同时也将不定期通过一些场景的实战强相关框架概念和设计理念。
谢谢大家,我们下期再见。

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

推荐阅读更多精彩内容