在上一篇文章当中,咱们经过一个简单的例子,简单地认识了一下shiro。在这篇文章当中,咱们将经过阅读源码的方式了解shiro的认证流程。java
建议你们边读文章边动手调试代码,这样效果会更好。数据库
shiro中的异常主要分为两类,一类是AuthenticationException
认证异常,一类是AuthorizationException
权限异常。分别对应http响应状态码中的401
和403
缓存
当认证不经过时将根据具体状况抛出AuthenticationException的子类,当鉴权不经过时将会抛出AuthorizationException的子类。安全
咱们经过检验shiro是否抛出异常,从而判断登陆对象是否经过认证、是否具有相关保护资源的访问权限。session
这也是咱们在上一节的例子中,须要捕获相关异常的缘由。app
接下来,咱们经过阅读源码的方式来分析一下shiro框架的认证流程。框架
/**认证器 * @author 赖柄沣 bingfengdev@aliyun.com * @version 1.0 * @date 2020/9/21 0:50 */ public class Authenticator { private DefaultSecurityManager securityManager; public Authenticator(){ //1. 建立安全管理器 this.securityManager = new DefaultSecurityManager(); //2. 给安全管理器设置问题域 //由于权限信息从ini文件中读取,因此是IniRealm this.securityManager.setRealm(new IniRealm("classpath:shiro.ini")); //3. 注入安全管理器,并使用SecurityUtils全局安全工具类完成认证 SecurityUtils.setSecurityManager(securityManager); } /**认证 * @author 赖柄沣 bingfengdev@aliyun.com * @date 2020-09-23 16:22:11 * @param username 用户名 * @param password 密码 * @return void * @version 1.0 */ public void authenticate(String username,String password){ //4. 获取当前主题 Subject subject = SecurityUtils.getSubject(); //5.根据登陆对象身份凭证信息建立登陆令牌 UsernamePasswordToken token = new UsernamePasswordToken(username,password); //6.认证 //若是认证经过,则不抛出异常,不然抛出AuthenticationExceptixon异常子类 //正式项目建议直接抛出,统一异常处理 try { subject.login(token); }catch (IncorrectCredentialsException e) { e.printStackTrace(); }catch (ConcurrentAccessException e){ e.printStackTrace(); }catch (UnknownAccountException e){ e.printStackTrace(); }catch (ExcessiveAttemptsException e){ e.printStackTrace(); }catch (ExpiredCredentialsException e){ e.printStackTrace(); }catch (LockedAccountException e){ e.printStackTrace(); } } }
这是上一个例子当中的认证器的代码。 咱们在上述代码的44行,shiro认证的入口处打个断点,以便跟踪其认证流程。ide
而后在idea中以debug的形式启动程序。工具
咱们发现咱们进入了DelegatingSubject.login方法当中;学习
public class DelegatingSubject implements Subject { //省略了其余不影响理解的代码 public void login(AuthenticationToken token) throws AuthenticationException { this.clearRunAsIdentitiesInternal(); // 1. 真正作认证的仍是securityManager对象 Subject subject = this.securityManager.login(this, token); String host = null; PrincipalCollection principals; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject)subject; principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } if (principals != null && !principals.isEmpty()) { this.principals = principals; this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken)token).getHost(); } if (host != null) { this.host = host; } Session session = subject.getSession(false); if (session != null) { this.session = this.decorate(session); } else { this.session = null; } } else { String msg = "Principals returned from securityManager.login( token ) returned a null or empty value. This value must be non null and populated with one or more elements."; throw new IllegalStateException(msg); } } }
从上面的源码中咱们发现,虽然咱们调用了Subject对象的认证方法,可是,真正的认证操做仍是由安全管理器对象securityManager执行。
接着,咱们进入到securityManager的login方法当中去。
public class DefaultSecurityManager extends SessionsSecurityManager { //省略了其余无关代码 public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { //调用认证方法 info = this.authenticate(token); } catch (AuthenticationException var7) { AuthenticationException ae = var7; try { this.onFailedLogin(token, ae, subject); } catch (Exception var6) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an exception. Logging and propagating original AuthenticationException.", var6); } } throw var7; } Subject loggedIn = this.createSubject(token, info, subject); this.onSuccessfulLogin(token, info, loggedIn); return loggedIn; } }
当咱们进入到authenticate方法中时,发现他是AuthenticatingSecurityManager的方法
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager { //省略了其余无关代码 public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); } }
接着,他又调用了authenticator对象的authenticate方法
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware { //省略了其余无关方法 public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { if (token == null) { throw new IllegalArgumentException("Method argument (authentication token) cannot be null."); } else { log.trace("Authentication attempt received for token [{}]", token); AuthenticationInfo info; try { info = this.doAuthenticate(token); if (info == null) { String msg = "No account information found for authentication token [" + token + "] by this Authenticator instance. Please check that it is configured correctly."; throw new AuthenticationException(msg); } } catch (Throwable var8) { AuthenticationException ae = null; if (var8 instanceof AuthenticationException) { ae = (AuthenticationException)var8; } if (ae == null) { String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected error? (Typical or expected login exceptions should extend from AuthenticationException)."; ae = new AuthenticationException(msg, var8); if (log.isWarnEnabled()) { log.warn(msg, var8); } } try { this.notifyFailure(token, ae); } catch (Throwable var7) { if (log.isWarnEnabled()) { String msg = "Unable to send notification for failed authentication attempt - listener error?. Please check your AuthenticationListener implementation(s). Logging sending exception and propagating original AuthenticationException instead..."; log.warn(msg, var7); } } throw ae; } log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info); this.notifySuccess(token, info); return info; } } }
紧接着进入到了ModularRealmAuthenticator认证器对象的doAuthenticate方法
public class ModularRealmAuthenticator extends AbstractAuthenticator { protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); return realms.size() == 1 ? /**终于到了真正的认证逻辑*/ this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken); } }
在这一步当中,在检验咱们的Realms对象建立后,开始进入到doSingleRealmAuthentication方法当中进行认证操做
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } else { //获取认证信息 AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } else { return info; } } }
在这一步当中开始根据咱们传入的令牌获取认证信息
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable { public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 首先从缓存中获取 AuthenticationInfo info = this.getCachedAuthenticationInfo(token); if (info == null) { //缓存中没有,则从持久化数据中获取 info = this.doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { this.cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { this.assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; } }
从持久化数据源中获取登陆对象信息
public class SimpleAccountRealm extends AuthorizingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken)token; //根据用户名查询帐户信息 SimpleAccount account = this.getUser(upToken.getUsername()); //若是查询到了帐户信息 if (account != null) { //开始判断帐户状态 if (account.isLocked()) { throw new LockedAccountException("Account [" + account + "] is locked."); } if (account.isCredentialsExpired()) { String msg = "The credentials for account [" + account + "] are expired"; throw new ExpiredCredentialsException(msg); } } return account; } }
在这里,便完成了对用户名的校验。
接下来,咱们获取到了帐户信息并返回到了AuthenticatingRealm的getAuthenticationInfo方法。
在这个方法中有以下几行代码,在第二行中,调用assertCredentialsMatch方法开始校验用户凭证
if (info != null) { this.assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); }
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { //获取凭证匹配器对象 CredentialsMatcher cm = this.getCredentialsMatcher(); if (cm != null) { if (!cm.doCredentialsMatch(token, info)) { String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication. If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }
在这里完成对用户凭证的校验。真正的比较逻辑则在SimpleCredentialsMatcher的equals方法中完成。里面还会区分加密和不加密的状况,具体请查看源码。
分析到这一步咱们能够发现,SimpleAccountRealm继承了AuthorizingRealm类实现doGetAuthenticationInfo方法完成帐户信息查询并校验,并将结果返回给AuthorizingRealm。AuthorizingRealm帮SimpleAccountRealm完成对用户凭证的校验。
那么,若是咱们须要从数据库当中获取帐户信息,应该怎么将帐户信息传给shiro进行验证呢?这个问题留给你们思考一下,我将在下一篇文章当中为你们解答。
在这篇文章当中,咱们经过断点调试,阅读源码的方式弄清楚了shiro的认证流程。咱们拆开他的层层封装,发如今SimpleAccountRealm对象中的doGetAuthenticationInfo方法中完成帐户验证,在AuthenticatingRealm的assertCredentialsMatch完成对用户凭证的校验。
在下一篇文章当中,咱们将学习如何使用数据库信息完成认证和受权。