1.认证(Authentication)
在Shiro中,认证指的是识别和证明操作者是一个合法用户。“用户”这个概念在框架中抽象为主体Subject的概念。用户如果想要通过认证,需要提供Principal(身份)和Credentials(凭证),从而应用能验证用户身份。这些身份和凭证信息,在Shiro框架中以Token口令的概念进行封装。Shiro中的认证Token接口AuthenticationToken中封装了这俩个信息。
Principal: 身份,既主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/手机号。
Credentials: 凭证/证明,既只有主体知道的安全值,如密码/数字证书等。
身份和凭证是俩个比较抽象的概念,如果按简单项目中的概念说通俗一点,其实角色用户名/密码。以下是Token类的层次结构图
1.1基本的认证代码步骤
1.2记住我 vs 认证
Shiro框架支持记住我,也就是短期内免登陆(或者说自动登陆)。虽然俩种都表示用户在认证环境被“放行了”,但我们要明确区分“记住我主体”和“认证主体”还是有一些区别的。
- “记住我主体”并不是匿名的,而是具有已知身份ID的,也就是subject.getPrincipals()不为空,但这个身份ID是来自于记住的上次认证的身份ID,如果是通过“记住我”实现的自动登陆,那么调用subject.isRemembered()返回true。
- "认证主体"指的是本次会话通过subject.login()等方法成功认证而且没有抛异常的Subejct对象的。一个真正的"认证主体"调用subject.isAuthenticated()返回true。
注意,Subject的isRemembered()和isAuthenticated()是互斥的,一个为true意味着另外一个必然为fasle。为什么要区分这俩者呢?因为通过"记住我"通过认证环节,用户本次并没有提供凭证信息,并不是真正严格的认证。在很多软件系统中支持这个功能是为了大部分时候方便。但一些高度敏感的功能比如财务数据、付款等操作,还是会要求提供再次输入密码。比如手机上的支付宝和微信支付,都是自动登陆的,但付款时还是要输入支付密码。
1.3注销Log Out
用户的注销需要调用Subject.logout()方法。该操作会让Shiro清楚身份信息并让当前Session失效。对于Web项目而言,同时还会通过响应Header让浏览器删除记住的C ookie。在注销后,当前主体又处于匿名状态,可以再次执行登陆操作。
注意:由于Web项目的“记住我”通常基于浏览器Cookie实现,而注销后的删除Cookie命令又是通过响应给浏览器的HTTP Header来传达,因此在注销后强烈建议重定向到一个新的页面,这样失效的Cookie就真的失效了,否则用户可能从当前页面继续提交无效的Cookie,导致引发一些认证异常。
1.4认证的内部流程
上图是框架内部认证的流程示意图。具体流程为:
- 应用程序代码调用Subject.login(token)
- Subject实例–通常是DelegatingSubject类型或其子类–在login的内部会调用securityManager.login(token),委托安全管理器 进行实例的认证。
- 安全管理器进一步调用authenticator.authenticate(token),把认证工姓委托给内部的认证器authenticator,它一般是
ModularRealmAuthenticator
类型的实例,该类型的认证器支持多个域(Realm)的认证。 - 如果应用程序配置了多个Realm,该认证器会启用认证策略对象
AuthenticationStrategy
。通俗一点说,AuthenticationStrategy。
就是在多个Realm都被执行后,对认证结果进行综合判断,决定认证结果,如果程序只有一个Realm,那么认证器会注解执行Realm,不会使用认证策略对象。 - 对于每个配置的Realm(都是一个认证域AuthenticatingRealm类或子类),在认证环节,Shiro都会判断是否支持当前Token,如果支持就会执行该Realm的
getAuthenticationInfo
方法,该方法中封装了真正的认证逻辑
2.域(Realm)
Realm是Shiro框架安全方面的数据源,用于提供用户、角色、资源和权限等方面的数据。打开框架源码中Shiro的jar包中realm的package,可以看到这一系列的Realm,在IDEA中转换为类图如下
在最顶层的是一个抽象的Realm接口,该接口有一个最重要的getAuthenticationInfo抽象方法,就是规定了任何域都必须要实现认证
这个类层次结果是逐层(增强) 的。如果你的项目只需要认证,而不需要针对功能、数据进行权限检查,那么使用认证域
AuthenticatingRealm即可。如果需要授权,那么要从AuthorizaingRealm类或它的子类选一个进行使用或扩展。比如前面的示例我们基础了AuthorizatingRealm,重写了认证、授权的方法。
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.RealmsecurityManager.realms = $ fooRealm,$ barRealm,$ bazRealm
如果你采用ini文件进行Realm的配置,可以怎么写。这里配置了三个自定义的域对象,并按顺序分配给了Shiro的securityManager对象。
认证器受安全管理器的委托执行认证时,先通过认证域的supports(token)方法,判断该Realm是否支持提交过来的Token对象,如果返回true,则继续。举个例子,你的软件 系统为了支持生物识别(指纹、虹膜、声音)的认证,自定义了一个 Realm,那么这个 Realm 明显是不能支持 UsernamePasswordToken 这种类型的 Token 的,因此认证器不会调用这个自 定义 Realm。由此可以看出,Realm 才是认证的真正执行者,而不是认证器。
认证 Realm 的 getAuthenticationInfo(token) 方法是真正执行认证逻辑的方法,一般而言 逻辑应该是这样的:
- 检查 Token 信息
- 根据身份信息(通常是 User Name)从数据源中查找账户数据
- 确认 Token 中封装的凭证(通常是 Password)和数据源中保存的一致
- 如果凭证一致,把账户数据封装为 Shiro 可识别的 AuthenticationInfo 对象返回出去
- 如果凭证不一致,抛出 AuthenticationException
当然,以上是一个通常的抽象流程,具体的实现代码可以不同,或者做些其它事情比如 更新数据库、保存文件等等,都可以自由实现。
2.1凭证匹配与加密
从认证域 AuthenticatingRealm 的类图可以看到,该类中有个 credentialsMatcher 属性, 也就是凭证匹配器对象。在 Realm 中获取到了账户数据后,会和用户提交的认证数据 Token 会一起,提交给凭证匹配器进行对比,这一般决定了认证是否成功。Shiro 中提供了常用的 凭证匹配器类型,看 org.apache.shiro.authc.credential 包中的相关类层次结构:
如果你的系统需要实现特殊的匹配规则,可以继承这些匹配器中的某一个并重写。重写 的匹配器可以通过 ini 的方式配置、Spring XML 中或 Java 中注入给 Realm 对象。
Java 方式:
Realm myRealm = new com.company.shiro.realm.MyRealm();
CredentialsMatcher customMatcher = new company.shiro.realm.CustomCredentialsMatcher();
myRealm.setCredentialsMatcher(customMatcher);
或者ini方式:
[main]
…
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credntialsMatcher = $customMatcher
…
Shiro 的所有开箱即用的 Realm 实现默认使用简单凭证匹配器 SimpleCredentialsMatcher, 这意味着默认情况下密码的匹配只是简单的字符串比较。例如 Realm 的认证方法收到一个 UsernamePasswordToken,简单凭证匹配器会直接用 Token 中用户提交的明文密码和数据库 的密码进行比较。当然,该匹配器除了支持 String 类型的比较,还支持 char[]、byte[]、File 和 InputStream 等其它类型。
2.2哈希与盐
相比于在数据库中存储明文密码,更好的方式是在把密码保存到数据源之前先进行单向 散列(Hash 算法)。散列值是不可逆的,即无法通过密文反推出明文,安全性较高,因此 Hash 算法在当前的软件安全领域被大量地使用。
要想使用 Hash 加密,可以用 Shiro 提供的 HashedCredentialsMatcher。要在认证时使用 该匹配器,进行基于 Hash 加密的密码对比,需要将 Realm 中默认的 SimpleCredentialsMatcher 替换为 HashedCredentialsMatcher。
加密领域的盐 Salt,不是炒菜的调料,而是为了提高加密的安全性。不管是对称加密, 还是非对称加密,在用户信息和算法可能被泄漏的情况下,都存在密码被反推出来的可能。 在加密环节如果加盐,等于多了一重安全因素。一般来说,盐就是一个不被外界知道的随机 字符串,把用户的明文密码加上盐,再进行加密得到密文(密码的加密后的形式)。
也就是说最终的密文是以下两个内容的函数:
1.用户输入的密码明文
2.盐值
用户进行注册时,第一次填写的密码,需要经过这个步骤进行加密,然后要将以下内容 存入数据库,以便后续登录时用来对比:
1.用户名
2.最终的密文
3.加密所用的盐值
3.基于 IniRealm 进行认证
3.1.Shiro 的 ini 配置格式
ini 文件是 Shiro 中比较常用的一种格式,他和 Java 的 properties 文件差不多,都是键值 对形式,主要一个优点就是多了 [节点] 的语法。Shiro 中规定的几个主要 ini 节点有 main、 users、roles、urls 等。
接下来,我们先以 ini 配置文件作为数据源,来编写一个基本的认证和授权测试。
- 编写 ini 配置文件
在 resources/目录下新建一个 File,名为 shiro.ini。内容如下图。
在 users 节点下编写一个键值对,[jack=1234,admin] 的意思是身份为 jack 的用户, 第 154 页
密码是 1234,属于 admin 角色。 在[roles]节点下编写一个键值对,[admin=user:delete,user:update]的意思是 admin 角色的用户具有 user:delete,user:update 这两个权限。
- 编写单元测试代码
3.2基于 JdbcRealm 进行认证
前面提到 Realm 的层次结构和 IniRealm,其实对于以 RDBMS 作为数据源的软件项目来 说,使用 JdbcRealm 可能更加方便。打开 JdbcRealm 类的结构看看:
从上面类图可以看到,使用 JdbcRealm 需要注入一个 DataSource,代表数据库。开发者还需要注入一些 SQL 语句给这个域对象,其中
setAuthenticationQuery 方法用来注入一个认证的 SQL,Shiro 据此查询用户数据,
setUserRolesQuery 方法用来注入一个查询用户、角色关联数据的 SQL,
setPermissionsQuery 方法用来注入一个查询权限数据的 SQL
如果你不想这么麻烦,那么在设计这方面的数据库表时,可以继续遵循“约定大于 配置”的理念,都按 Shiro 默认表名、字段来设计。该类内置了一些列默认 SQL 语句, 例如你数据库中的用户表名为“users”且用户名字段名为“username”,那么就可以 不需要注入认证的 SQL。其它几个 SQL 同理。
接下来,我们使用 JdbcRealm 实现从数据库查询,实现认证和基于角色的授权。
- 首先按照Shiro默认命名建立几个表
- 编写shiro.ini配置并导入依赖
这里我们采用了业界很受欢迎的高性能数据库连接池——阿里巴巴的 druid 作 为 DataSource 的具体实现,因此需要导入 maven 依赖。
- 编写测试代码