写在前面

在上一篇文章当中,我们通过一个简单的例子,简单地认识了一下shiro。在这篇文章当中,我们将通过阅读源码的方式了解shiro的认证流程。

建议大家边读文章边动手调试代码,这样效果会更好。

认证异常分析

shiro中的异常主要分为两类,一类是AuthenticationException认证异常,一类是AuthorizationException权限异常。分别对应http响应状态码中的401403

认证异常AuthenticationException子类

权限异常AuthorizationException子类

当认证不通过时将根据具体情况抛出AuthenticationException的子类,当鉴权不通过时将会抛出AuthorizationException的子类。

我们通过检验shiro是否抛出异常,从而判断登录对象是否通过认证、是否具备相关保护资源的访问权限。

这也是我们在上一节的例子中,需要捕获相关异常的原因。

接下来,我们通过阅读源码的方式来分析一下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认证的入口处打个断点,以便跟踪其认证流程。

然后在idea中以debug的形式启动程序。

DelegatingSubject

login()方法

我们发现我们进入了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执行。

DefaultSecurityManager

login() 方法

接着,我们进入到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;
} }

AuthenticatingSecurityManager

authenticate()方法

当我们进入到authenticate方法中时,发现他是AuthenticatingSecurityManager的方法

public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
//省略了其他无关代码
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
}

AbstractAuthenticator

authenticate()方法

接着,他又调用了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()方法

紧接着进入到了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方法当中进行认证操作

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;
}
}
}

AuthenticatingRealm

getAuthenticationInfo()方法

在这一步当中开始根据我们传入的令牌获取认证信息

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;
}
}

SimpleAccountRealm

doGetAuthenticationInfo()方法

从持久化数据源中获取登录对象信息

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

接下来,我们获取到了账户信息并返回到了AuthenticatingRealm的getAuthenticationInfo方法。

在这个方法中有如下几行代码,在第二行中,调用assertCredentialsMatch方法开始校验用户凭证

if (info != null) {
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}

assertCredentialsMatch()方法

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完成对用户凭证的校验。

在下一篇文章当中,我们将学习如何使用数据库信息完成认证和授权。

shiro认证流程源码分析--练气初期的更多相关文章

  1. [Android]Android系统启动流程源码分析

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5013863.html Android系统启动流程源码分析 首先 ...

  2. [Android]从Launcher开始启动App流程源码分析

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5017056.html 从Launcher开始启动App流程源码 ...

  3. Spring加载流程源码分析03【refresh】

      前面两篇文章分析了super(this)和setConfigLocations(configLocations)的源代码,本文来分析下refresh的源码, Spring加载流程源码分析01[su ...

  4. Android笔记--View绘制流程源码分析(二)

    Android笔记--View绘制流程源码分析二 通过上一篇View绘制流程源码分析一可以知晓整个绘制流程之前,在activity启动过程中: Window的建立(activit.attach生成), ...

  5. Android笔记--View绘制流程源码分析(一)

    Android笔记--View绘制流程源码分析 View绘制之前框架流程分析 View绘制的分析始终是离不开Activity及其内部的Window的.在Activity的源码启动流程中,一并包含 着A ...

  6. Spark(五十一):Spark On YARN(Yarn-Cluster模式)启动流程源码分析(二)

    上篇<Spark(四十九):Spark On YARN启动流程源码分析(一)>我们讲到启动SparkContext初始化,ApplicationMaster启动资源中,讲解的内容明显不完整 ...

  7. Spark(四十九):Spark On YARN启动流程源码分析(一)

    引导: 该篇章主要讲解执行spark-submit.sh提交到将任务提交给Yarn阶段代码分析. spark-submit的入口函数 一般提交一个spark作业的方式采用spark-submit来提交 ...

  8. spring boot 加载web容器tomcat流程源码分析

    spring boot 加载web容器tomcat流程源码分析 我本地的springboot版本是2.5.1,后面的分析都是基于这个版本 <parent> <groupId>o ...

  9. springboot 事务创建流程源码分析

    springboot 事务创建流程源码分析 目录 springboot 事务创建流程源码分析 1. 自动加载配置 2. InfrastructureAdvisorAutoProxyCreator类 3 ...

随机推荐

  1. WebApis中DOM操作的基本案例

    1.1. 排他操作 1.1.1 排他思想 如果有同一组元素,我们想要某一个元素实现某种样式, 需要用到循环的排他思想算法: 所有元素全部清除样式(干掉其他人) 给当前元素设置样式 (留下我自己) 注意 ...

  2. 2020,最新Model的设计-APP重构之路

    很多的app使用MVC设计模式来将“用户交互”与“数据和逻辑”分开,而model其中一个重要作用就是持久化.下文中设计的Model可能不是一个完美的,扩展性强的model范例,但在我需要重构的app中 ...

  3. hadoop平台环境搭建

    centos01 配置静态ip vim /etc/sysconfig/network-scripts/ifcfg-eth0 见图1 修改主机名 vim /etc/sysconfig/network 见 ...

  4. Android开发之 当前日期String类型转date类型 java代码中实现方法

    /** * 获取当前时间 * * @return */ public Date getDate(String str) { try { java.text.SimpleDateFormat forma ...

  5. ARouter使用

    1. androidstudio3.0配置 javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_ ...

  6. 【jmespath】—1. 基础用法

    一.jsonpath 之前我写接口自动化测试时候,对于复杂的json返回,会使用jsonpath这个第三方库,就像写xpath一样,方便的查询json元素. 因为之前写WEB自动化时候,总用xpath ...

  7. Java反射(一)

    什么是反射? 在程序的运行过程中,可以动态的创建对象. 反射的基石是什么? 字节码对象是反射的基石.字节码对象:Java类文件通过javac进行编译后生成的xxx.class文件,此文件由jvm加载至 ...

  8. ASP.NET Core 进程内与进程外的性能对比

    ASP.NET Core 进程内与进程外的性能对比 本文内容是<深入去浅出ASP.NET Core>提供的扩展内容,毕竟在书里说进程内外的性能说明对比,对于初学者而言,稍微复杂了点. 我在 ...

  9. RabbitMQ安装和运行

    RabbitMQ在Windows下安装和运行 1.下载Erlang: http://www.erlang.org/downloads/19.2 2.下载Windows版RabbitMq: http:/ ...

  10. centos7图形化安装oracle11g

    #设置主机名 hostnamectl set-hostname oracle #yum安装 yum -y install unzip vim* bash-completion bash-complet ...