Apache Shiro 认证
身份验证,即证明用户证明确实是他们自己。为了让用户证明自己的身份,需要他们提供一些身份信息以及系统理解和信任的身份证明。这是通过向Shiro提交用户的principals
和credentials
,以查看它们是否与应用程序的期望相匹配来完成的
-
Principals:是用户的身份标识,可以是姓名,社工号,但最好是唯一表示用户的某个东西,如邮箱地址等等,这种能唯一标识用户的
Principal
称为Primary Principal
- Credentials:一个只有用户才知道或拥有的安全凭据,常见的就是密码,指纹等等
最常见的Principal/Credential
就是用户名和密码了,只有用户提交密码在应用程序比对通过后,才能认证成功
认证中的Subject
对Subject
进行身份验证的过程可以有效地分为三个不同的步骤:
- 收集
Subject
中的principals
和credentials
- 提交这些
principals
和credentials
进行认证 - 如果认证通过,则允许访问,否则重新进行认证或者不允许访问
下面详细介绍这些步骤
步骤1:将principals和credentials表示为AuthenticationToken
//Example using most common scenario of username/password pair:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//"Remember Me" built-in:
token.setRememberMe(true);
这里我们使用的是UsernamePasswordToken
,这是最常用的通过用户名密码认证的方法,UsernamePasswordToken
实现了Shiro的org.apache.shiro.authc.AuthenticationToken
接口,这个接口是Shiro专门用来表示用户提交的principals
和credentials
的
需要注意的是,Shiro并不关心你如何获取这些用户提交的信息,不管是从HTML表单提交还是其他方式,Shiro并不关心,这样处理是为了通过AuthenticationToken
这个概念,把从终端用户收集信息这个过程与认证解耦
你可以按照自己的喜好来构造和表示AuthenticationToken
,它是与协议无关的
这个例子的最后一行代码展示了如何通过Shiro执行“记住我
”服务
步骤2:提交principals和credentials
在收集principals
和credentials
并将其表示为AuthenticationToken
实例之后,我们需要将这个Token提交给Shiro以执行认证
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
首先我们先从Shiro中获取一个Subject
,然后调用login
方法,传入AuthenticationToken
的实例,即完成了一次认证请求
步骤3:处理认证结果
如果认证通过,那么login
方法不会有任何回应,现在这个Subject就是已经通过认证了的,应用程序的线程可以继续往下执行,并且之后调用SecurityUtils.getSubject()
方法都会返回这个已经认证过的Subject,任何调用subject.isAuthenticated()
方法的返回值都是true
如果认证失败,login方法将会抛出一个运行时的AuthenticationException
或其子类的异常,通过抛出异常的类型可以判断处认证失败的原因
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...
如果这些已经存在的异常类型不能满足你的需求,可以自定义一个AuthenticationException
来表示一个特定的登录失败的场景
如果你的程序需要给用户展示一个提示信息,告诉用户登录失败,最好是使用同一个提示信息,如“用户名或密码错误”,这样可以避免给想要攻击你的应用程序的黑客提供有价值的信息
已记住与已认证
正如上面的例子,Shiro支持“记住我
”的概念,注意,在Shiro中,已记住和已认证有着非常明确的区别
已记住:一个已记住的
Subject
是一个已知身份的,即调用subject.getPrincipals()
会返回一个非空值,但这个身份是从来源于之前的会话中的认证,只有当调用subject.isRemembered()
方法返回true
时,这个Subject
才是Remembered
Authenticated:一个Authenticated Subject是一个在当前会话中通过了认证的Subject,即调用了
login()
方法而没有抛出异常,只有当调用subject.isAuthenticated()
方法返回true
时,这个Subject
才是Authenticated
一个Subject的Remembered状态和Authenticated状态是互斥的
为什么有这个区别
“认证”这个术语有很强的证明的含义,即保证Subject
是它所声明的用户。当一个用户仅仅是已记住状态时,只能说明这个用户很大可能是真正的用户,但不一定是。而一旦认证通过,也就不再认为这个用户是已记住的了,因为已经在当前会话中验证通过了
所以当执行一些安全性不高的操作时(如获取用户自定义的设置),只要Subject
是已记住的状态即可,但是,如果涉及到一些比较敏感的操作(如查看账户余额),则需要Subject
是已认证的状态
举个例子
就像你登录到淘宝,添加了一些东西到你的购物车里面,这时候你临时有点事离开了,但是你并没有登出系统。
几天后你再一次进入淘宝,此时淘宝“记住了”你是谁,会给你个性化推荐一些书籍,此时对于淘宝来说,subject.isRemembered()
就是true
如果你这时候想要修改你的银行卡或者修改身份认证信息,这时候“已记住”的状态就是不够的了,因为它不能保证你是你,可能是其他的人在使用你的设备。
所以在执行敏感操作的时候,淘宝会要求你登录,以确保你的真实性,在你登录之后,subject.isAuthenticated()
就是true
了
登出
当Subject
完成了所有和应用的交互后,可以通过subject.logout()
方法来使得认证信息失效
currentUser.logout(); //removes all identifying information and invalidates their session too.
当调用logout
后,当前的Session
和身份将会失效,在Web应用中,RememberMe
的cookie
也会被删除
当登出后,执行logout
方法的Subject
实例将会变成匿名的,如果需要,可以再次用于登录,Web应用程序除外
因为web应用程序中“记住我”的功能通常是通过cookie来持久化的,而cookie的删除只能在响应之后(详情可查后端如何删除Cookie),因此强烈建议在调用
subject.logout()
之后立即将终端用户重定向到一个新的视图或页面。这保证了任何与安全相关的cookie都会按照预期被删除。这是HTTP cookie功能的限制,而不是Shiro的限制
认证的流程
现在来详细介绍Shiro是如何进行认证的。下面的架构图中,与认证相关的组件都进行了高亮显示,每一个序号代表认证过程中的每一步
第一步:应用程序代码调用Subject.login
方法,并传入构造好的AuthenticationToken
实例,代表终端用户的principals
和credentials
第二步:Subject
实例(通常是DelegatingSubject
或其子类)委托应用程序的SecurityManager
,调用SecuirtyManager.login()
方法进行真正的认证工作
第三步:SecurityManager
接收token
,将其交给它内部的Authenticator
实例,调用authenticator.authenticate(token)
方法,通常这个Authenticator
实例是ModularRealmAuthenticator
的实例,它支持在认证期间协调多个Realm
的实例共同工作。ModularRealmAuthenticator
实质上为Apache Shiro提供了一种PAM风格的范式(其中每个Realm
在PAM术语中都是一个模块)
第四步:如果在应用中配置了多个Realm
,则ModularRealmAuthenticator
将会利用配置的AuthenticationStrategy
来进行一次multi-Realm
的认证。在调用Realm
进行身份验证之前、期间和之后,AuthenticationStrategy
将会被调用来响应每个Realm
的结果
如果只有一个Realm,它将会被直接调用,在单Realm的应用程序中,不需要
AnthenticationStrategy
第五步:轮询每一个Realm
,检查它是否支持被提交的AuthenticationToken
,如果支持,则会调用Realm
的getAuthenticationInfo()
方法,getAuthenticationInfo
方法表示了对单个Realm
的认证
Authenticator
如前所述,Shiro的SecurityManager
实现默认使用的是ModularRealmAuthenticator
实例,ModularRealmAuthenticator
支持单Realm应用,也支持多Realm应用
在单Realm应用中,ModularRealmAuthenticator
将直接调用这个Realm,而在多Realm中,将会使用AuthenticationStrategy
来协调这些Realm工作
如果希望使用自定义的Authenticator,可以做如下配置
[main]
...
authenticator = com.foo.bar.CustomAuthenticator
securityManager.authenticator = $authenticator
但是在实践中,ModularRealmAuthenticator
已经可以满足大多数需求了
AuthenticationStrategy
当应用中有多个Realm时,ModularRealmAuthenticator
依赖于它内部的AuthenticationStrategy
组件来决定一次认证请求是否通过
例如,如果仅仅只有一个Realm认证通过,其他认证失败,那这次认证是成功了还是失败了?是否需要所有的Realm全部认证通过,这次认证才算通过?如果一个Realm认证通过了,是否还需要继续通过其他的Realm进行认证。AuthenticationStrategy
根据应用程序的需要做出适当的决策
一个AuthenticationStrategy
是一个无状态的组件,它将会在认证中被调用4次(在这4次的交互中,任何必要的状态都会被以方法参数的形式给出):
- 在任何Realm被调用之前
- 在每个Realm的
getAuthenticationInfo
方法被调用之前 - 在每个Realm的
getAuthenticaitonInfo
方法被调用之后 - 所有的Realm都处理之后
一个AuthenticationStrategy
也负责汇总每个认证成功的Realm的结果,并将结果绑定到一个AuthenticationInfo
中,最后汇总而成的AuthenticationInfo
实例即为Authenticator
的返回值,Shiro将会用它的信息作为Subject
的数据来源,如principals
Shiro中有3个可用的AuthenticationStrategy
的实现:
AuthenticationStrategy类 | 描述 |
---|---|
AtLeastOneSuccessfulStrategy | 只要有至少一个Realm认证成功了,则认为这次认证通过了。如果全都失败了,则认证失败 |
FirstSuccessfulStrategy | 只有第一个认证成功的Realm的信息会被使用,之后的所有Realm将会被忽略。如果全部都失败了,则认证失败 |
AllSuccessfulStrategy | 所有的Realm都认证成功,这次认证才算成功,只要有一个认证失败的,都会使得这次认证失败 |
ModularRealmAuthenticator
默认使用的是AtLeastOneSuccessfulStrategy
,这符合大多数的需求,当然你也可以配置其他策略
[main]
...
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authcStrategy
...
如果想要自定义
AuthenticationStrategy
,可以通过继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy
类,该类实现了汇总每个Realm返回的AuthenticationInfo
的功能
Realm 认证的顺序
注意,ModularRealmAuthenticator
与Realm交互是通过迭代的方式,当进行认证时,ModularRealmAuthenticator
将会迭代访问每个Realm,调用他们getAuthenticationInfo
()方法,传入AuthenticationToken
参数
隐式声明顺序
在INI配置中,Realm的顺序时按照它们在INI文件中出现的顺序来的
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
如上面的配置,ModularRealmAuthenticator
在迭代Realm时,顺序是blahRealm
,fooRealm
,barRealm
也可以通过下面的方式,来达到同样的效果
securityManager.realms = $blahRealm, $fooRealm, $barRealm
这种隐式声明的方式,你可以不需要显式的指定securityManager
的realms
的属性
显示声明顺序
如果你想显式声明这个realm的顺序,而不管它们在shiro.ini
中出现的顺序,则可以通过设置securityManager
的realms
属性来完成
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
securityManager.realms = $fooRealm, $barRealm, $blahRealm
...
这样,ModularAuthenticationStrategy
的迭代顺序就是fooRealm
,barRealm
,blahRealm
如果采用显式声明
SecurityManager.realms
属性值的方式,则只有被设置的realm会被使用,比如,如果在INI文件中定义了5个realm,但是你只使用了其中3个设置realms属性值,那么只有这3个会生效。这和隐式声明是不一样的