总的参考文档:http://shiro.apache.org/reference.html
本文参考文档:http://shiro.apache.org/authentication.html
本文介绍Shiro功能特性中的身份认证部分,上图中绿色的一块。身份认证就是根据用户提供的身份以及凭证验证它的合法性。有两个概念:
前文说过,在具体的应用中用Subject代表一个用户,认证的过程可以分成三步:
示例代码:
//Example using most common scenario of username/password pair:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//"Remember Me" built-in:
token.setRememberMe(true);
首先username与password怎么来的,代码中并没有写。实际上Shiro并不关心它们是怎么来的,常见的情况是用户通过浏览器提交了表单,应用从表单中拿到的数据。拿到数据后,将它们转换成UsernamePasswordToken实例。UsernamePasswordToken是 org.apache.shiro.authc.AuthenticationToken接口的具体实现。实际上这个UsernamePasswordToken实例最终会被传递给Realm,Realm要认识这个类才能知道怎么解析里边的用户名与密码。
假如这样一种情况,用户在提交数据时,除了用户名、密码外,还包括防止机器人的随机码,就像我们平时登录网站时会先刷出打了噪点的小图片,上边有一串数字、字母、汉字什么的。这个时候就需要开发者实现自己的org.apache.shiro.authc.AuthenticationToken类,将用户、密码、随机验证码一起打包。当然也要设置或者自定义Realm,让它知道如何解释打包进来的东西,如何验证这些东西。另外注意最后一行代码token.setRememberMe(true);,这个是记住我功能,目前搞不清楚它是怎么实现的,但我想应该与会话管理有关。这个东西等看完SessionManager后再说。
示例代码:
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
前文中说过了,SecurityUtils代表了一个SecurityManager实例,并且它是单例,全应用只有一个实例。从它获取Subject实例,这样Subject实例就与SecurityUtils代表的SecurityManager实例关联起来了。
一旦从SecurityUtils获取了Subject实例,那么这个实例就会与当前线程绑定,它是线程级别的对象。比如你在某个方法中执行了上述代码,那么在另一个方法中调用SecurityUtils.getSubject()时,返回的仍然是示例代码中的Subject,是同一个,而不是新创建。如果上述代码通过了验证,那么在后续的方法中调用SecurityUtils.getSubject().isAuthenticated()返回的依然是true。
currentUser.login(token);这一行表示向SecurityManager发起身份认证。
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?
}
上述异常是由Realm抛出的,当然开发者可以自定义Realm并抛出自定义异常。
示例代码:
currentUser.logout(); //removes all identifying information and invalidates their session too.
执行logout操作后,Subject将会清空自己所持有的所有信息,并且将相关的session失效,效果等同于它是刚从SecurityUtils.getSubject();获取到一样。
上图中一共有五个步骤,本质上是层层调用,最后将token传给Realm。
第一步:用户调用Subject.login(token)并将token传入。
第二步:因为Subject是与SecurityManager绑定的,Subject.login(token)实际会调用securityManager.login(token),这个时候token到了SecurityManager。
第三步:securityManager.login(token)又调用authenticator.login(token),数据token传给了Authenticator组件。Authenticator一般是ModularRealmAuthenticator类实例,它支持多Realm。
第四步:这一步只有在多个Realm时才会发生。如果有多个Realm,Authenticator会初始化一个AuthenticationStrategy实例,AuthenticationStrategy负责根据配置的策略协调多个Realm的认证过程,后边会详细介绍。
第五步:这时token终于到了具体的Realm,在这里最终进行身份验证,后边会详细介绍Realm。
上述五个步骤,每个涉及到的组件实例用户都可以自定义。
认证策略,当有多个Realm时负责协调整个认证过程。比如当一个Realm通过认证,其它的Realm都没有通过认证,这个时候整个认证应该是成功还是不成功呢?是否需要所有Realm都认证成功才算整个认证成功?在认证的时候,如果第一个Reaml就已经认证通过,这个时候是不是应该整个认证过程已经成功了,后边的Realm不需要再认证了呢?AuthenticationStrategy就是做这个事情的。
AuthenticationStrategy的四次交互:
在最后一步的时候,如果有不止一个Realm通过了验证,那么AuthenticationStrategy会将全部的结果都聚合到单一的AuthenticationInfo实例中,这个实例会通过Authenticator传递给SecurityManager,再由SecurityManager返回给具体的Subject,这样Subject就知道自己通过了那个Realm的认证。
AuthenticationStrategy本身是Shiro提供的接口,同时Shiro也提供了三种具体实现。当然如果Shiro提供的具体实现无法需求,开发者也可以自定义。
AuthenticationStrategy class |
Description |
---|---|
AtLeastOneSuccessfulStrategy |
向所有Realm发起认证,只有要一个认证通过,整个认证过程就被认为成功。只有所有Realm认证全部失败,才认为整个认证过程失败。如果有多个Realm通过认证,相关的信息全部聚合并返回给Subject。 |
FirstSuccessfulStrategy |
依次向Realm发起认证,只要有一个成功,整个认证被认为成功,其它Realm将不再认证。所有Realm都失败,则整个认证失败。只返回给Subject认证成功的Realm信息。 |
AllSuccessfulStrategy |
这个很明确,所有Realm必需全部成功,一个失败都不行。所有Realm的认证成功信息聚合后返回给Subject。 |
默认Authenticator是ModularRealmAuthenticator类实例,它默认使用AtLeastOneSuccessfulStrategy谁策略。可通过如下方式配置:
[main]
...
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authcStrategy
...
FirstSuccessfulStrategy认证策略中,多个Realm是依次进行的,因此Realm的顺序会影响到认证结果。在配置SecurityManager时有两种反序方式。
一各旧隐式排序,如:
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
这种的话,SecurityManager就按它们在配置文件中定义的顺序发起认证。
另一种是显示排序,如:
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
securityManager.realms = $fooRealm, $barRealm, $blahRealm
最后一行代码,明确定义了三个Realm的顺序,隐式排序就失效。