上篇讲述了Shiro的组成部分,以及Subject交互等的概念。那么开始身份认证学习~
一、结合代码分析
Controller
public String login(String username, String password, Model model){
String exMessage = "";
if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
// 获取subject
Subject subject = SecurityUtils.getSubject();
// 创建表单获取用户名密码
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
try {
// 登陆
subject.login(usernamePasswordToken);
return "redirect:index.do";
} catch (UnknownAccountException uae) {
// 帐号不存在
exMessage = "帐号不存在";
logger.debug("UnknownAccountException---->user:{},password:{}",username,password);
}catch (IncorrectCredentialsException ie) {
exMessage = "用户名或密码错误,错误信息:" + ie.getMessage();
logger.debug("IncorrectCredentialsException---->user:{},password:{}",username,password);
} catch (LockedAccountException le) {
exMessage = "该账号已锁定,错误信息:" + le.getMessage();
logger.debug("LockedAccountException---->user:{},password:{}",username,password);
} catch (DisabledAccountException de) {
exMessage = "该账号已禁用,错误信息:" + de.getMessage();
logger.debug("DisabledAccountException---->user:{},password:{}",username,password);
} catch (ExcessiveAttemptsException eae) {
exMessage = "该账号登录失败次数过多,错误信息:" + eae.getMessage();
logger.debug("ExcessiveAttemptsException---->user:{},password:{}",username,password);
} catch (Exception e){
exMessage = "未知错误,错误信息:" + e.getMessage();
logger.debug("Exception---->user:{},password:{}",username,password);
}
}
model.addAttribute("exMessage", exMessage);
return "/login";
}
1、首先整理身份验证步骤
- 获取表单username,password
- 获取shiro的Subject对象 SecurityUtils.getSubject()
- 创建身份验证的Token,调用subject.login()方法
- 最后根据返回信息来判断身份验证结果,通常根据返回的异常来判断身份验证状态
- 如果身份验证失败请捕获AuthenticationException或其子类
捕获AuthenticationException或其子类,常见的如:DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库
- 最后可以调用subject.logout退出,其会自动委托给SecurityManager.logout方法退出
2、总结身份验证细节流程
- 首先调用Subject.login(token) 进行登录,其会自动委托给 Security Manager
- SecurityManager负责身份验证逻辑,它会委托给Authenticator认证器进行身份验证,可以自定义实现
- Authenticator可能会委托给相应的AuthenticationStrategy,进行ModularRealmAuthenticator中Realm身份验证,可以选择 单realm 和多realm认证
- Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问
3、分析源码
-
由Subject.login(token) 进入接口实现类,发现真正调用的是Security Manager
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); //省略其他 }
-
然后继续跟进DefaultSecurityManager
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = authenticate(token); } catch (AuthenticationException ae) { //省略其他 } return loggedIn; }
-
继续跟进就会发现进入到 Authenticator接口进行验证,点击进入实现到 authenticate() 方法
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { if (token == null) { throw new IllegalArgumentException("Method argument (authentication token) cannot be null."); } info = doAuthenticate(token); // 省略其他 return info; }
-
再跟进就会进入到 ModularRealmAuthenticator,从源码中会发现,首先获取的是 Realms集合,然后会根据集合大小决定选择 多Realm(按照相应的顺序及策略进行访问)还是单Realm进行验证。这里我们选择单realm验证doSingleRealmAuthentication()
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
-
继续跟进其实现类,发现进入到realm验证器进行验证,跟踪进入
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { //省略其他 AuthenticationInfo info = realm.getAuthenticationInfo(token); //省略其他 return info; }
-
这是我们最终目的,到达验证方法,再继续跟踪时,会发现各种Realm子类对其实现,也是我们进行自定义realm进行实现的地方,如果用于用户身份校验,我们只要继承这个类就可以,可以看到继承树,这个类也集成了CachingRealm,可以使用缓存。
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //otherwise not cached, perform the lookup: info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
-
然后自定义UserRealm来重写doGetAuthenticationInfo(AuthenticationToken)来实现认证
以上就是身份验证流程、源码跟踪,以及自己实现身份验证的方法的原理
2019.12.08 更新
突然发现少说了**认证策略(AuthenticationStrategy)**这个知识点
AuthenticationStrategy:认证策略,目前shiro只给出三种策略方式(默认是第二种):
- FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略
- AtLeatOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将返回所有Realm身份校验成功的认证信息
- AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份认证成功的认证信息,如果有一个失败就失败了
继续从源码出发,源码第四步,我们可以看到单Realm时,我跟进到doSingleRealmAuthentication()方法里,现在我们查看多Realm时,我们进入到 doMultiRealmAuthentication() 方法
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isWarnEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.warn(msg, t);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
首先我们会看到获取认证策略:
AuthenticationStrategy strategy = getAuthenticationStrategy()
然后会遍历多个Realm,调用getAuthenticationInfo()去认证
AuthenticationStrategy 我觉得比较牛批的一点是,不但能根据想要的策略去认证,而且可以将没一个成功的结果会统一处理后再返回。
引自官方文档
Also an AuthenticationStrategy is responsible for aggregating the results from each successful Realm and ‘bundling’ them into a single AuthenticationInfo representation. This final aggregate AuthenticatinoInfo instance is what is returned by the Authenticator instance and is what Shiro uses to represent the Subject’s final identity (aka Principals).
好了,多Realm配置相关的配置信息
<!-- 多Realm时需要配置,同时指定认证策略。-->
<bean id="modularRealmAuthenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<!-- 认证策略 -->
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
</property>
<property name="realms">
<list>
<ref bean="userRealm"/>
<ref bean="secordRealm"/>
</list>
</property>
</bean>
<!-- 同时securityManager中的单Realm引用的配置更换即可,不做代码测试了。 -->
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="authenticator" ref="modularRealmAuthenticator"/>
<!-- 设置securityManager安全管理器的rememberMeManger -->
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>