花了两天时间认真、重点走读 Apache Shiro安全框架 身份认证的源码, 访问控制和前者是结构对应的,架构之美体现在对称和简易上。这个框架也让我想起业界优秀的网络框架 Netty,优雅地描述了网络模型,它的优雅不仅体现在 ServerSocket和 Socket的对称之美,还体现在简化了一系列配置极简之美。
①,先上一张Shiro框架手绘图
从图中能够清晰地看出,该框架能够提供的服务类型有哪些:
- 实现应用程序账户的认证和授权
- 对会话进行管理
- 提高性能进行缓存管理
- 整个流程安全加密进行
- 委派一个大总管负责以上所有服务的提供
②,七大对象类
1.默认Web安全管理类——DefaultWebSecurityManager
凡是用到HTTP连接的程序就用该类进行管理,通过该类进行一揽子的配置完成框架的应用,该类可以看做是Shiro框架的大总管。类图如下所示,可见其复杂度,实现了手绘图中设计的各个部分都归属该类的管理。
2.默认会话安全管理类——DefaultWebSessionManager
一个HTTP连接就是一个会话,将其理解为TCP连接即可通过该类能够对会话进行配置,设置检测会话有效性的周期、超时时间的设置、Cookie的设置。设置Cookie就是给会话分配一个UUID号,让浏览器下次访问请求时在HTTP头部设置以下属性,系统能够根据该标识判断用户的登录状态及权限信息。
cookie:JSESSIONID=a7f3083b-4a47-4a56-a734-072a886edcfc
3.授权域——AuthorizingRealm
Realm,译文为“域”,可理解为存储账户信息的数据库或者文件系统。
该类继承了认证类,因此具备认证和访问控制的双重责任,也是和数据库或文件系统进行打交道的类。通过查询会话中的token获取身份信息,进而进行将登录密码和数据库中的密码进行比对认证,认证通过后再进行权限的查询。
在系统对接中,如SSO(Single Signed On)对接,就是重写该类的认证方法doGetAuthenticationInfo(AuthenticationToken authenticationToken)和授权方法doGetAuthorizationInfo(PrincipalCollection principals),赋值给外来系统以本地系统的用户身份信息,完成系统的集成。
4. 缓存管理类——CacheManager
该类负责会话信息的缓存管理,较为简单,一般地用EhCacheManager进行缓存管理,即主机内存,较于每次数据库IO性能低,直接从内存中读取出信息能够提高IO性能。当然这种情况在程序重启的情况下会失去信息,因此可以通过实现CacheManager接口完成Redis对会话信息的缓存。
5. 身份认证秘钥——AuthenticationToken
该类拥有用户的用户名和密码信息,当然除了以此作为身份标识外,你想用的任何信息都可以对该接口进行实现。一般来说,框架提供的UsernamePasswordToken类足以满足需求,该类还实现了记住我的功能。
6. 秘钥匹配器——CredentialsMatcher
该接口用于声明秘钥匹配的方式,如果是明文秘钥就直接比较密码相等与否,如果是使用加密算法对秘钥进行处理后的,就是用对应的加密算法对输入的秘钥进行加密后处理再与数据库里的密码进行比对,相等则认证通过。
7. 用户实体——Subject
该类表示程序当前用户的状态和操作行为,包括认证(登入/登出)和访问控制(角色和权限)以及会话的访问。
③,认证过程详解
查看整个源码发现大量的委派模式/代理模式。
整个过程分为几个阶段。
当用户有登录行为时,调用Subject的login(AuthenticationToken token)
方法,委派类DelegatingSubject
实现该方法,在该方法内委托安全管理器类进行登录操作。
关键的就是管理器中的认证函数authenticate(token),通过该函数来构造认证信息类AuthenticationInfo。
接着调用安全管理类的认证器的认证函数doAuthenticate(token)
来获取认证信息类。
代码来到了ModularRealmAuthenticator
,该类是框架的灵活类,各个存储账户信息的数据库被看做是可插拔的,即模块化的(modular),账户信息可以来自多个库。
因为调试代码使用了一个账户信息库,因此调用单个账户认证函数doSingleRealmAuthentication
。
此时就可以看到从账户信息库里提取信息。
整个代码如下:先从缓存中获取账户信息,因为是首次登录,信息还没有放到缓存中。
所以就进到doGetAuthenticationInfo(AuthenticationToken authcToken)
中,也就来到了关键地方,我们自己继承了授权域类实现的账户信息库。在该方法中,我们构造账户信息。
至此我们完成了账户信息的获取。
账户信息拿到后,我们开始将登录信息和账户信息进行比对。进入到assertCredentialsMatch(token, info)
方法中,接着到doCredentialsMatch(token, info)
。
OK,在这里通过Hash算法对密码进行加盐加密完成密码的校验比对。
开始一步步退栈回到认证器的authenticate函数,最后是通知有关任何注册监听认证成功的监听器以成功的信号。
继续退栈来到安全管理器的登录函数中完成用户Subject对象的构造。
继续退栈,来到Subject的login(AuthenticationToken token)方法下,至此完成整个账户登录过程。