Authentication,认证,即身份验证,证明一个用户是谁。用户想要证明其身份,就需要提供身份信息,以及系统可以识别的标识证明。这需要向Shiro提供用户的principals和credentials来验证其是否和系统匹配。
1.1 Principals
即Subject的识别属性。 Principals可以是任何可以识别Subject的事物。比如,名,姓,用户名,身份证号等等。当然了,姓不能很好唯一的识别一个Subject。因此,对于应用中,最好的principals就是用户名或者email。
1.1.1 Primary Principal
虽然可以使用任意数量的principals,对于一个应用,最好有一个主要的principal,一个简易的值来唯一的标识Subject。通常使用用户名,emial或者唯一的userid。
1.2 Credentials
即加密的数值,用来证明其对Subject所声明的身份。通常,为密码,生物识别数据如指纹,视网膜扫描等。
一般来说,principal/credential即指用户名和密码。用户名指明其身份,密码进行身份验证。如果提交的密码符合系统的验证,系统就可以认为这个用户是合法的。
2. Authenticating Subjects
验证Subject分3步。
a. 收集提交的principals和credentials
b. 提交principals和credentials进行验证
c. 如果提交成功,允许访问,否则重新提交或不允许访问
以下代码演示了使用Shiro的API对于这三部分的操作。
Step 1: Collect the Subject's principals and credentials
//Example using most common scenario of username/password pair: UsernamePasswordToken token = new UsernamePasswordToken(username, password); //”Remember Me” built-in: token.setRememberMe(true);
这个例子中,我们使用UsernamePasswordToken,它支持通常的用户名和密码的验证。它是org.apache.shiro.authc.AuthenticationToken的一个实现。AuthenticationToken接口表示Shiro系统认证中提交的principals和credentials.
需要注意的是,Shiro不关心你如何获取的这些信息:可以从用户的表单提交,HTTP的头信息,Swing,Flex GUI表单,或者命令行。也可以任意构建AuthenticationToken,因为它是协议无关的。
从例子中可以看出,在认证请求时,我们想使用Shiro的"Remember Me"服务。它表示Shiro记住了当前的用户身份。
Step 2: Submit the principals and credentials
收集了principals和credentials,并构建为AuthenticationToken实例,我们需要提交这个token(令牌)让Shiro进行认证。
Subject currentUser = SecurityUtils.getSubject(); currentUser.login(token);
获取了当前操作的Subject后,我们传入token,调用了login方法。一次login方法的调用,就代表尝试一次认证。
Step 3: Handling Success or Failure
如果login方法无任何返回,表示我们的认证成功了。SecurityUtils.getSubject()将返回一个认证过的Subject,对
subject.isAuthenticated()方法的调用都将返回true。
但是,如果登陆失败,将会怎样呢?比如,终端用户输入了错误的密码,或者请求访问太多次,异或账户被锁定?
Shiro提供了丰富的运行时AuthenticationException,以准确表示认证失败的原因。你可以把login方法封装在try/catch语句块中,并且捕捉你期望的异常。
try { currentUser.login(token); } catch ( UnknownAccountException uae ) { ... } catch ( IncorrectCredentialsException ice ) { ... } catch ( LockedAccountException lae ) { ... } catch ( ExcessiveAttemptsException eae ) { ... } ... catch your own ... } catch ( AuthenticationException ae ) { //unexpected error? } //No problems, continue on as expected...
如果这些异常类不能满足你的需求,可以自定义AuthenticationExceptions。
2.1 Login Failure Tip
虽然这些异常类可以很好的反映出业务逻辑,但实践中,只需要给终端用户显示简单的提示消息即可,比如"用户名或密码错误",以防止被试图带有攻击倾向的黑客所获取。
2.2 Remembered vs. Authenticated
如上面的例子,Shiro除了登陆,还支持 "remember me"的概念。这里需要指出的是,Shiro的remembered Subject和authenticated Subject有明显的区别。
Remembered:
一个remembered Subject,不能是匿名的,其被上一次会话所认证并记忆,subject.getPrincipals方法返回不为空。只有subject.isRemembered()返回true时,才被认为是remembered subject。
Authenticated:
一个authenticated Subject表示,其被当前会话成功的认证过(调用login方法没有任何异常信息)。只有当subject.isAuthenticated()返回true时,才被认为是authenticated subject。
2.3 Mutually Exclusive
Remembered和authenticated两种状态是互斥的。一个true,另一个即为false,反之亦然。
2.4 Why the distinction?
authentication一词,有强烈的证明寓意。即,必须保证Subject被认证过。当一个用户被上一次的应用会话所记忆,认证的状态已不存在了,被记忆的用户标识只能告诉系统,这个用户可能是谁,但实质上并不能完全保证它就是那个用户。然而,一个authenticated subject,不单单被认为是记忆着的,而且必须保证在当前会话中被认证过。尽管应用的有些功能可以使用记忆着的principals来执行一些用户的逻辑操作,比如自定义视图,但是在用户被合法性认证前,最好不要执行一些敏感的操作。比如,对于金融信息的访问,使用subject的isAuthenticated()以保证它的安全和合法性,而非isRemembered()。
2.5 Logging Out
当subject和应用交互完成后,需要销毁这些认证的信息。
currentUser.logout(); //removes all identifying information and invalidates their session too.
当调用logout方法后,session失效,所有的认证信息都无效了(web应用中,RememberMe的cookie也被删除),subject实例变为匿名的(在web应用中,其还可以重用。)
2.6 Web Application Notice
由于web应用中,remembered的标识保存在cookie中,而cookie只有在response提交前才能被删除,强烈建议在调用logout方法后,立刻让终端用户跳转到一个新的视图或页面,这样,才能保证和信息安全相关的cookie被删除掉。这是由HTTP的cookie所限而非Shiro。
2.7 Authentication Sequence
直到现在,我们只看到了应用中认证subject,那么,接下来我们要看一下Shiro内部的认证流程。下面架构图左边的部分,就是和认证相关的,数字代表了验证的步骤。
step1:从终端用户获取principals和credentials来构建AuthenticationToken实例,调用subject.login方法。
step2: subject实现类DelegatingSubject(或子类)委托给应用的SecurityManager,调用securityManager的login(token)方法。
step3: SecurityManager接受到token后,委托其内部的Authenticator,调用authenticator的authenticate(token)方法。
step4: 如果配置多个Realm,ModularRealmAuthenticator(Realm的默认实现)利用其配置的AuthenticationStrategy决定如何对Realm进行认证。如果应用中只配置了一个Realm,就不需要AuthenticationStrategy。
step5: 每个Realm都要检查其是否支持提交的token,如果支持,getAuthenticationInfo方法将被调用,返回封装了token的AuthenticationInfo实例。
3. Authenticator
SecurityManager默认实现中都使用了ModularRealmAuthenticator。 ModularRealmAuthenticator同时支持单Realm和多Realm。
如果想自定义的Authenticator,可以在shiro.ini中这样做:
[main] ... authenticator = com.foo.bar.CustomAuthenticator securityManager.authenticator = $authenticator
3.1 AuthenticationStrategy
在单Realm应用中,ModularRealmAuthenticator直接调用Reaml。在多Realm中,它使用AuthenticationStrategy来决定如何判断认证的成功与否。比如,如果只有一个Realm验证成功,其他的全部失败,认证是成功么?还是所有的Realm认证都必须要成功才算认证成功?亦或一个Realm验证成功,是否还需要调用其他Realm?AuthenticationStrategy会根据应用的需求来决定如何进行验证。AuthenticationStrategy为无状态组件,每次认证请求,需要调用它四次。
1.在Realms被调用前。
2.在每个Realm的getAuthenticationInfo之前
3.在每个Realm的getAuthenticationInfo之后
4.在所有Realms被调用后。
AuthenticationStrategy汇集成功认证的Realm结果,并将它们封装为AuthenticationInfo。AuthenticationInfo就是Authenticator实例所最终返回的结果,也就是Subject的标识(或Principals)。
Shiro中有3个AuthenticationStrategy实现:
AuthenticationStrategy class | Description |
AtLeastOneSuccessfulStrategy | If one (or more) Realms authenticate successfully, the overall attempt is considered successful. If none authenticate succesfully, the attempt fails. |
FirstSuccessfulStrateg | Only the information returned from the first successfully authenticated Realm will be used. All further Realms will be ignored. If none authenticate successfully, the attempt fails. |
AllSuccessfulStrategy | All configured Realms must authenticate successfully for the overall attempt to be considered successful. If any one does not authenticate successfully, the attempt fails. |
AtLeastOneSuccessfulStrategy是ModularRealmAuthenticator中AuthenticationStrategy的默认实现。
[main] ... authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy securityManager.authenticator.authenticationStrategy = $authcStrategy
3.2 Custom AuthenticationStrategy
如果想自定义AuthenticationStrategy,可以继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy。
3.3 Implicit Ordering
如果使用Shiro的ini配置方式,需要指定Realm对于AuthenticationToken的处理顺序。在shiro.ini中,Realm的处理顺序就是其在文件中定义的顺序。
blahRealm = com.company.blah.Realm ... fooRealm = com.company.foo.Realm ... barRealm = com.company.another.Realm
在认证处理时,blahRealm,fooRealm,barRealm会依次被调用。
等同于:
securityManager.realms = $blahRealm, $fooRealm, $barRealm
3.4 Explicit Ordering
如果想显示定义Realms的顺序,可以设置securityManager的realms属性。还使用上面的例子,但是这次,把blahRealm放在最后:
blahRealm = com.company.blah.Realm ... fooRealm = com.company.foo.Realm ... barRealm = com.company.another.Realm securityManager.realms = $fooRealm, $barRealm, $blahRealm
在认证处理时,fooRealm,barRealm,blahRealm会依次被调用。
3.5 Explicit Realm Inclusion
如果显式配置了securityManager.realms属性,只有配置的realms才有效。也就是说,如果你在ini文件中定义了5个realms,而只有3个被securityManager的realms属性引用,那么只有这三个才是有效的。这和隐式定义realm不同,隐式的realm将全部有效。