Marco's Java【Shiro进阶(二) 之 Shiro源码解析终结篇(精华)】

前言

经过前面阶段的学习,我们从了解Shiro,到使用Shiro,再到整合Shrio及SSM,一层层的拨开Shrio的神秘面纱,见到了Shrio的 “真容”, 那么本节的终结篇呢我们继续深入探索Shiro的 “心” ,带着大家一点点的查看Shiro的底层核心代码并跟着分析,通过对Shiro的源码解析,让我们更清楚它的实现原理和运作流程。也算是对Shiro系列篇的一个收尾,写作并整理这篇文章零零散散的花了3 - 4天时间(当然,主要还是有别的任务啦!),将自己对Shiro的理解全部糅合在其中了,希望能帮助大家更深入的了解Shiro!

废话就不多说了,咱们开始吧~

Shiro底层解析之shiro.ini文件的解析

我们在讲解Shiro的第一天讲到了使用shiro.ini文件配置我们的用户信息,所以就先从IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini") 这个方法开始一点点的解析吧~

跟着IniSecurityManagerFactory()方法,我们进入到下面这个界面,Shiro会判断我们是否有传入shiro.ini文件的路径,若没有传入路径,则抛出非法参数异常
在这里插入图片描述
紧接着通过new一个ini初始化对象,调用loadFromPath方法,通过ResourceUtil先加载文件流
在这里插入图片描述
下面三种加前缀的方式,都可以被读取到(classpath:,url: ,file: ),例如 classpath:shiro.ini
在这里插入图片描述
接着回到loadFromPath()方法,我们看到里面又包了一层load(scanner)
在这里插入图片描述
连续剥了三层,终于进到了解析shiro.ini的核心方法,其实我们不难看出,Shiro对shiro.ini就是一行行的扫描,这就是为什么我们在ini配置文件中采用类似于properties文件一样的方式撰写
在这里插入图片描述
当前缀不为"#"、";"或者null时,调用getSectionName方法,这里的section就是类似于[user]、[role]这样的头部标签,扫描到头部标签之后会被存放在一个Map集合中,key为sectionName,也就是我们的user、role等
在这里插入图片描述
这个value很容易想的到就是[user]下面我们撰写的user的配置信息了
在这里插入图片描述
当我们解析完所有的ini文件中的信息中之后,会存放在ini对象中,而ini对象本质上就是LinkedHashMap,里边的子标签又是一个LinkedHashMap
在这里插入图片描述
且最终ini对象又会被存储到securityManagerFactory中,为我们后续创建SecurityManager做准备,不难看出,当我们使用createInstance()方法创建Security对象的时候,我们首先会去找ini对象
在这里插入图片描述
而createInstance()方法呢本质上又是使用FilterChainResolver过滤器链创建一个默认的实例对象。

protected FilterChainResolver createInstance(Ini ini) {
FilterChainResolver filterChainResolver = createDefaultInstance();//创建默认的过滤器链对象
if (filterChainResolver instanceof PathMatchingFilterChainResolver) {
    PathMatchingFilterChainResolver resolver = (PathMatchingFilterChainResolver) filterChainResolver;
    FilterChainManager manager = resolver.getFilterChainManager();
    buildChains(manager, ini);//构建FilterChainManager
}
return filterChainResolver;
}

如果filterChainResolver是PathMatchingFilterChainResolver对象或者是它的子类,则调用buildChains(manager, ini)继续构建FilterChainManager。
PathMatchingFilterChainResolver最重要的作用是:当请求url来的时候,他担任匹配工作(调用该类的getChain方法做匹配

protected void buildChains(FilterChainManager manager, Ini ini) {
    //filters section: 获取配置文件的section,如user
    Ini.Section section = ini.getSection(FILTERS);

    if (!CollectionUtils.isEmpty(section)) {
        String msg = "The [{}] section has been deprecated and will be removed in a future release!  Please " +
                "move all object configuration (filters and all other objects) to the [{}] section.";
        log.warn(msg, FILTERS, IniSecurityManagerFactory.MAIN_SECTION_NAME);
    }
	//开始合并各处的Filter过滤器
    Map<String, Object> defaults = new LinkedHashMap<String, Object>();
	//FilterChainManager 已经有的默认的Filter , 具体可查看 DefaultFilter
    Map<String, Filter> defaultFilters = manager.getFilters();

    //now let's see if there are any object defaults in addition to the filters
    //these can be used to configure the filters:
    //create a Map of objects to use as the defaults:
    if (!CollectionUtils.isEmpty(defaultFilters)) {
        defaults.putAll(defaultFilters);//确保所有默认的Filters都存放在defaults Map集合中
    }
    //User-provided objects must come _after_ the default filters - to allow the user-provided
    //ones to override the default filters if necessary.
    Map<String, ?> defaultBeans = getDefaults();
    if (!CollectionUtils.isEmpty(defaultBeans)) {
        defaults.putAll(defaultBeans);
    }
	//将defaults中的非Filter对象去除,并添加ini中配置的Filter
    Map<String, Filter> filters = getFilters(section, defaults);

    //add the filters to the manager: 将filter添加到FilterChainManager中
    registerFilters(filters, manager);

    //urls section: 生成各url对应的过滤器链
    section = ini.getSection(URLS);
    createChains(section, manager);
}

大家看到上面的代码核心部分有两个,一个是注册过滤器registerFilters(filters, manager),一个是生成过滤器链createChains(section, manager)

protected void registerFilters(Map<String, Filter> filters, FilterChainManager manager) {
    if (!CollectionUtils.isEmpty(filters)) {
        boolean init = getFilterConfig() != null; //判断过滤器配置是否为空,是否可用
        for (Map.Entry<String, Filter> entry : filters.entrySet()) {
            String name = entry.getKey();
            Filter filter = entry.getValue();
            manager.addFilter(name, filter, init);//添加filter到FilterChainManager
        }
    }
}

不难看出以上方法主要是将filter添加到FilterChainManager,而下面的createChains的作用主要是根据页面的url请求,生成对应的NamedFilterList。

protected void createChains(Map<String, String> urls, FilterChainManager manager) {
    if (CollectionUtils.isEmpty(urls)) {
        if (log.isDebugEnabled()) {
            log.debug("No urls to process.");
        }
        return;
    }

    if (log.isTraceEnabled()) {
        log.trace("Before url processing.");
    }
	 //根据url生成对应的NamedFilterList
    for (Map.Entry<String, String> entry : urls.entrySet()) {
        String path = entry.getKey();
        String value = entry.getValue();
        manager.createChain(path, value);
    }
}

createChains(Map<String, String> urls, FilterChainManager manager)本质上就是调用manager.createChain(path, value),遍历要拦截的url,并创建一个个的 “链条” 节点,并连接起来,咱们接着跟进。

 public void createChain(String chainName, String chainDefinition) {
  	...
    //parse the value by tokenizing it to get the resulting filter-specific config entries
    //
    //e.g. for a value of
    //
    //     "authc, roles[admin,user], perms[file:edit]"
    //
    // the resulting token array would equal
    //
    //     { "authc", "roles[admin,user]", "perms[file:edit]" }
    //
    // 先分离出配置的各个filter,比如说
    // "authc, roles[admin,user], perms[file:edit]" 分离后的结果是:
    // { "authc", "roles[admin,user]", "perms[file:edit]" }
    String[] filterTokens = splitChainDefinition(chainDefinition);

    //each token is specific to each filter.
    //strip the name and extract any filter-specific config between brackets [ ]
    // 接着进一步分离出"[]"内的内容,其中nameConfigPair是一个长度为2的数组
    // 比如说 roles[admin,user] 解析后的nameConfigPair为{"roles", "admin,user"}
    //"perms[file:edit]" 解析后的nameConfigPair为{"perms", "file:edit"}
    for (String token : filterTokens) {
        String[] nameConfigPair = toNameConfigPair(token);

        //now we have the filter name, path and (possibly null) path-specific config.  Let's apply them:
        //获取拦截的路径,filter以及可能存在的"[]"中的值之后执行
        //nameConfigPair[0]即为section(如roles),nameConfigPair[1]即为其对应的具体内容(如"admin,user")
        addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
    }
}
public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
     if (!StringUtils.hasText(chainName)) {
         throw new IllegalArgumentException("chainName cannot be null or empty.");
     }
     //通过过滤器的名称从DefaultFilterChainManager中的Map<String, Filter> filters集合中获取filter对象
     Filter filter = getFilter(filterName);
     if (filter == null) {
         throw new IllegalArgumentException("There is no filter with name ‘" + filterName + "‘ to apply to chain [" + chainName + "] in the pool of available Filters.  Ensure a " + "filter with that name/path has first been registered with the addFilter method(s).");
     }

     // 将"[]"中的匹配关系注册到filter中
     applyChainConfig(chainName, filter, chainSpecificFilterConfig);

     // 确保chain已经被加到filterChains这张map中了
     NamedFilterList chain = ensureChain(chainName);
     // 将该filter加入当前chain
     chain.add(filter);
 }

NamedFilterList 本质上就是List<Filter>集合,最终我们将filter存入到DefaultFilterChainManager定义的NamedFilterList “花名册” 中!至此,咱们的过滤器链就创建完成!


Shiro底层解析之Subject主体的创建

在这里插入图片描述
咱们还是先以这张图为参考,之前已经分析过,要想实现登录认证,就必须得有个 “送口信的” ,这个 “送口信” 的角色就是Subject主体对象,可以稍加想象,我们输入的账号密码就好比咱们进入某神秘组织或者工会的口令,这个工会十分神秘,因此安全措施做的非常好,因此咱们的口令需要 “跑腿的” 给传达过去,SecirityManager就是工会负责人,跑腿的小弟把口信带给负责人之后,负责人会按照制度办事,这个时候他会通知 “暗部” 组织Realm去查 “工会成员簿” (数据库),“咱们工会有这号人物么?”,“有,放他进来吧!”,这会儿,我们就可以名正言顺的进入工会大门了。倘若 “暗部” 查出来没有这号人物,那么我们就会吃闭门羹了…

因此,整个流程就是从 “跑腿小弟” 带信开始的,“跑腿小弟” 也是工会Shiro的一员,因此肯定是有身份的,所以,咱们得先了解Subject是怎么创建的,后面的流程才能走下去。

在这里插入图片描述
为了更好了分析,我先把之前在 Marco’s Java【Shiro进阶(一) 之 Shiro+SMM集成Maven项目串烧篇(下)】 里的LoginController中的代码搬过来。
咱们回顾一下,之前使用shiro.ini文件作为数据源的时候,是靠IniSecurityManagerFactory创建SecurityManager,接着使用SecurityUtils绑定SecurityManager到运行环境中。
后来使用了自定义Realm,通过application-shiro.xml配置SecurityManager,由此可见Spring的运行环境中肯定会存在SecurityManager的实例,因此就不需要我们再绑定SecurityManager了。
在这里插入图片描述
所以关于SecurityManager对象的创建就不多赘述了,咱们就直接以SecurityUtils.getSubject()为入口,来探个究竟!

//获取Subject主体
Subject subject = SecurityUtils.getSubject();

ThreadContext.getSubject()很显然是ThreadLocal设计模式,大家点进去ThreadContext看的话会发现这个类聚合了ThreadLocal<Map<Object, Object>>,以及两个常量SECURITY_MANAGER_KEYSUBJECT_KEY ,咱们ThreadLocal中存放的是一个Map集合,集合中的key正是这两个常量,通过这两个key来存取SecurityManager和Subject两个对象的。
这里我就不去分析线程局部变量的操作原理啦。从线程局部变量中取出subject对象之后,先这个主体对象判断是否为null,如果为null,则通过buildSubject()创建subject对象,并绑定到当前线程的线程局部变量中,这么一来,下次如果是同一个线程的话,直接去ThreadContext中取就好啦!

public static Subject getSubject() {
    Subject subject = ThreadContext.getSubject();//从线程局部变量中取出subject对象
    if (subject == null) {
        subject = (new Subject.Builder()).buildSubject();//如果subject为空,则创建subject对象
        ThreadContext.bind(subject);//并绑定到线程局部变量中
    }
    return subject;
}

这里我们重点要 “深挖” 的是 (new Subject.Builder()).buildSubject() 这个方法,new Subject.Builder()实质上是调用SecurityUtils的getSecurityManager()方法获取当前上下文中已经创建好的SecurityManager对象

/**
 * Constructs a new {@link Subject.Builder} instance, using the {@code SecurityManager} instance available
 * to the calling code as determined by a call to {@link org.apache.shiro.SecurityUtils#getSecurityManager()}
 * to build the {@code Subject} instance.
 */
public Builder() {
    this(SecurityUtils.getSecurityManager());
}

咱们接着往下走,看看buildSubject()方法具体做了什么… 欸?看到这个方法我们就应该清楚了Subject本质上就是由SecurityManager创建的
在这里插入图片描述
这里的SubjectContext就是Shiro中的上下文,可以把它理解成是一个巨大的作用域,就和Spring的ApplicationContext作用差不多,从这作用域中,我们可以轻易的获取SecurityManager、Subject、AuthenticationInfo等信息,因此Shiro官方很形象的称它为 “桶”。
在这里插入图片描述
咱们接着走,发现SecurityManager的createSubject方法中没有任何内容,显然要去找它的实现类DefaultSecurityManager。下面的绝大多数操作都是围绕着SubjectContext,这一个个创建主体对象Subject的方法跟 “套娃” 似的,打开一个,还有一个… 不过真正干活的方法来了,咱们继续跟进doCreateSubject(context)方法。

##DefaultSecurityManager
public Subject createSubject(SubjectContext subjectContext) {
   //create a copy so we don't modify the argument's backing map:
   //copy一个SubjectContext上下文对象的副本,放置默认的参数被修改
   SubjectContext context = copy(subjectContext);

   //ensure that the context has a SecurityManager instance, and if not, add one:
   //确保SubjectContext对象中存在SecurityManager对象
   //如果没有,就将当前的DefaultSecurityManager对象存放进去
   context = ensureSecurityManager(context);

   //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
   //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
   //process is often environment specific - better to shield the SF from these details:
   //该方法用于处理关联的会话(通常基于引用的会话ID)
   //向会话域中存入一个Session实例; 此操作可能失败, 届时会构建一个缺少Session的会话域   
   //注意这里的构建Session出错是被允许的, 如果出现异常是会以Debug的方式输出.  
   //Session的维护是交给了专门的SessionManager来负责  
   //注意这里用的是SessionKey类型的Key,而不是简单的string类型的sessionId
   context = resolveSession(context);

   //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
   //if possible before handing off to the SubjectFactory:
   //这一步主要是将principals(可以理解成查询到的用户信息)set到SubjectContext上下文中
   //当然principals也有为null的可能性
   context = resolvePrincipals(context);
	//创建Subject主体对象
   Subject subject = doCreateSubject(context);

   //save this subject for future reference if necessary:
   //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
   //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
   //Added in 1.2:
   //SubjectDAO接口会负责保存当前subject,其实继续跟进源码会发现Principals会以
   //_PRINCIPALS_SESSION_KEY为key存储在session中
   save(subject);

   return subject;
}

在这里插入图片描述
很直观的可以看出来,上面方法的意思是获取Subject的工厂类SubjectFactory创建Subject
在这里插入图片描述
因为我们接触的基本上都是Web项目,所以选择DefaultWebSubjectFactory,找到它的createSubject方法,首先这个方法会判断从Shiro的上下文对象中获取到的主体是WebSubject还是普通的主体对象,如果是普通的Subject对象,则调用DefaultWebSubjectFactory的父类DefaultSubjectFactory去创建普通的Subject对象,否则,将下面一堆(如SecurityManager,Session)和Web相关的对象从上下文中获取出来。
在这里插入图片描述
接着将这些属性来个 “大杂烩”,调用WebDelegatingSubject的构造方法,创建该对象,并返回。
在这里插入图片描述
其实不难看出WebDelegatingSubject相对于DelegatingSubject(普通的主体对象),多出了下面两个属性
servletRequestservletResponse,这不正是Web中的请求和相应对象么?


Shiro底层解析之登录原理

分析完Suject是如何获取的,咱们就要马不停蹄的去探索下一个 “关卡”。既然现在 “送信” 的有了,那么他是如何将口信捎给 “掌门” SecurityManager,然后 “掌门” 又是怎么通知 “暗部组织” Realm去处理呢?这一切还是个谜!

所以我们继续以subject.login(token)为突破口,进一步挖掘。果不其然,咱们 “送信” 的小哥Suject把口令给 “掌门” SecurityManager之后,最终还是由 “掌门” 去处理。
在这里插入图片描述
正规的工会办事还是严谨的啊!这里有个小细节,就是咱们的账号和密码输入之后是被存放在 “锦囊” UsernamePasswordToken中的,而且判断了密码是否为null?如果是则返回null,否则将密码转为字符数组的形式,至于后面的falsenull分别代表什么,我也不知道… 所以接着往下面分析呗~

public UsernamePasswordToken(final String username, final String password) {
    this(username, password != null ? password.toCharArray() : null, false, null);
}

本质上是调用authenticate(token)方法,获取登录认证的信息,并交给Subject,那么 “送信” 的小哥收到结果后会决定要不要放你进工会。所以,接下来我们重心就放在authenticate(token)

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
   AuthenticationInfo info;
   try {
       info = authenticate(token);//调用authenticate方法获取登录认证的信息
   } catch (AuthenticationException ae) {
       try {
           onFailedLogin(token, ae, subject);//抛出异常后,登陆失败后执行的方法,就不去分析了
       } catch (Exception e) {
           if (log.isInfoEnabled()) {
               log.info("onFailedLogin method threw an " +
                       "exception.  Logging and propagating original AuthenticationException.", e);
           }
       }
       throw ae; //propagate
   }

   Subject loggedIn = createSubject(token, info, subject);//将登录认证的信息封装到主题对象中并返回

   onSuccessfulLogin(token, info, loggedIn);//记住密码功能

   return loggedIn;
}

这里稍微说下onSuccessfulLogin()方法,之前咱们不是不清楚UsernamePasswordToken构造函数中的false值有啥用吗,其实继续跟进onSuccessfulLogin()的源码会发现,后面会调用isRememberMe(token)方法判断token中记住密码的信息是true还是false,显然咱们之前看到了,这个值是false,因此,不会开启记住密码功能。如果手动选择记住密码,会调用下面这个方法,将我们的身份信息转化为base64的形式存在cookie中。
在这里插入图片描述
接着通过Authenticator对象调用authenticate(token)方法
在这里插入图片描述
大家注意了!前方是一个迷宫入口,正确的入口是从AbstractAuthenticator,而不是下面的AuthenticatingSecurityManager,否则你会发现怎么绕都绕不出去的!
在这里插入图片描述
又到了 “套娃” 时间,下面的方法本质上是调用里面的doAuthenticate(token)方法,我们接着跟进,不过有兴趣的朋友可以看下这个方法在不同情形下抛出的异常,因为Shiro就是通过抛异常这种方式来判断,你输入的究竟是账号错误还是密码错误,或者说是别的情形。
在这里插入图片描述
终于要到头了!下面的方法首先会判断,是否存在Realm?因为当我们使用shiro.ini文件的时候是不会使用Realm的。接着判断有多少个Realm,并分为两种情况进行处理,这里我们就直接进到doSingleRealmAuthentication方法中来进行分析。

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();//获取存放所有的Realm的List集合,判断List是否为空
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
    	//当只有一个real时执行的操作
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
   		//当有多个real时执行的操作
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

说句题外话,看了这么久的源码,大家有没有发现一个问题?就是咱们Shiro的底层的代码看上去很多,但实际上绝大多数代码都是异常!而且Shiro的异常和源码的注释都写的贼详细!
好啦,回到正轨,我们来看realm.getAuthenticationInfo(token)方法,这个方法返回了AuthenticationInfo对象,我们先记住这个对象的名字,后面分析凭证匹配器的时候会用到,留个印象先。

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);
   }
   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);
   }
   return info;
}

接下来的这个方法是 “重头戏" ,相当有料!首先我们从缓存对象Cache<Object, AuthenticationInfo>中获取AuthenticationInfo对象,本质上就是从token中取出Principal对象,通过Principal获取AuthenticationInfo认证信息,如果缓存中没有认证信息,则调用doGetAuthenticationInfo(token)获取认证信息,接着在调用assertCredentialsMatch(token, info)判断Realm中数据库获取的数据和页面的数据是否匹配。信息量是不是特别大!

 public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {Principal
   //从缓存Cache<Object, AuthenticationInfo>中获取AuthenticationInfo对象,Object就好比是key,
   //而这里的Object就是Principal对象
   AuthenticationInfo info = getCachedAuthenticationInfo(token);
   if (info == null) {
       //otherwise not cached, perform the lookup:
       //如果缓存中没有AuthenticationInfo,则调用doGetAuthenticationInfo方法获取
       info = doGetAuthenticationInfo(token);
       log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
       if (token != null && info != null) {
       	   //将AuthenticationInfo放入缓存中
           cacheAuthenticationInfoIfPossible(token, info);
       }
   } else {
       log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
   }

   if (info != null) {
       assertCredentialsMatch(token, info);//判断Realm中数据库获取的数据和页面的数据是否匹配
   } else {
       log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
   }

   return info;
}

不着急,我们先来分析doGetAuthenticationInfo(token),这个方法有点熟啊?是不是在我们自定的Realm里面看到过?
在这里插入图片描述
如果大家没啥印象就看下之前的代码回忆下。

/**
 * 处理授权
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
	//获取当前的principal身份信息(一般是用户名、邮箱或者手机号等等)
	String principal = (String) token.getPrincipal();
	System.out.println("Current principal is " + principal);
	//获取当前的credential凭证信息(一般是密码等等)
	char[] credentials = (char[]) token.getCredentials();
	String credential = new String(credentials);
	System.out.println("Current credential is " + credential);
	System.out.println("###@#####################################################");
	//获取账号信息
	String userName = principal.toString();
	User user = userService.queryUserByUserName(userName);
	if(null != user) {
		//查询用户的角色
		List<String> roles = roleService.queryRolesByUserId(user.getUserid());
		//查询用户的权限
		List<String> permissions = permissionService.queryPermissionByUserId(user.getUserid());
		//封装activeUser
		ActiveUser activeUser = new ActiveUser();
		activeUser.setRoles(roles);
		activeUser.setUser(user);
		activeUser.setPermissions(permissions);
		//组装“盐”
		String salt = user.getUsername() + user.getAddress();
		ByteSource credentialsSalt = new SimpleByteSource(salt.getBytes());
		SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(activeUser, user.getUserpwd(), credentialsSalt, this.getName());
		return authenticationInfo;
	}
	return null;
}

不难看出我们的Realm和数据库的连接的核心入口就是SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(activeUser, user.getUserpwd(), credentialsSalt, this.getName())这个部分!
所以从现在开始,咱们要进入到Shiro源码解析的第三个 “关卡”。


Shiro底层解析之 探秘 “暗部” Realm

我一直把Realm比喻成暗部是有原因的,因为Realm在Shiro中一直是在 “底层” 工作的部门,负责和数据库打交道,因为我们的数据都是存在工会的一个秘密的角落,并且各种加密,以防 “外敌” 入侵,所谓天机不可泄漏!因此就诞生这么个部门专门来和数据库对接。

大家注意看这个对象SimpleAuthenticationInfo,是不是觉得很熟悉?
在这里插入图片描述
如果还是想不起来,这样总该想起来了吧?
在这里插入图片描述
也就是说SimpleAuthenticationInfo本质上就是AuthenticationInfo认证信息,而且SimpleAuthenticationInfo中已经包含我们的Principal对象(activeUser),数据库中查询出来的真实的password,加密的"盐",以及当前Realm分部的名称UserRealm。

好,带着这个信息我们再回过头来看这个方法下面的assertCredentialsMatch(token, info)方法。

 public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {Principal
   //从缓存Cache<Object, AuthenticationInfo>中获取AuthenticationInfo对象,Object就好比是key,
   //而这里的Object就是Principal对象
   AuthenticationInfo info = getCachedAuthenticationInfo(token);
   if (info == null) {
       //otherwise not cached, perform the lookup:
       //如果缓存中没有AuthenticationInfo,则调用doGetAuthenticationInfo方法获取
       info = doGetAuthenticationInfo(token);
       log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
       if (token != null && info != null) {
       	   //将AuthenticationInfo放入缓存中
           cacheAuthenticationInfoIfPossible(token, info);
       }
   } else {
       log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
   }

   if (info != null) {
       assertCredentialsMatch(token, info);//判断Realm中数据库获取的数据和页面的数据是否匹配
   } else {
       log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
   }

   return info;
}

首先调用getCredentialsMatcher()获取凭证匹配器,当然,这个凭证匹配器已经在xml中配置好了,所以肯定被Spring创建好,直接拿来用就好啦!
接着调用doCredentialsMatch(token, info)方法做验证。验证不通过则抛出IncorrectCredentialsException异常。

protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    CredentialsMatcher cm = getCredentialsMatcher();//获取凭证匹配器
    if (cm != null) {
        if (!cm.doCredentialsMatch(token, info)) {//使用凭证匹配器验证密码是否正确
            //not successful - throw an exception to indicate this:
            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.");
    }
}

因为CredentialsMatcher 是个接口,所以我们去找它的实现类HashedCredentialsMatcher

@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenHashedCredentials = hashProvidedCredentials(token, info);
    Object accountCredentials = getCredentials(info);
    //判断数据库中的密码和页面上加密后的密码是否匹配
    return equals(tokenHashedCredentials, accountCredentials);
}

看到这里,相信各位已经不需要我再多做说明了吧!不过还是稍微提一嘴,凭证匹配器判断数据库中的密码和页面上加密后的密码是否匹配,比较的是它们的转码之后的hash值,本身就已经使用算法加密过了,再这么一处理,可谓 “铜墙铁壁” ,怎么都破解不了(PS:反正我肯定是破解不了…)。
在这里插入图片描述


总结

怎么说呢,这节的信息量还是比较大的,所以咱们还是稍微总结一下,我将Shiro的底层运作流程归纳为下面这张图,然后看着这张图我们来大致的梳理下。
首先创建SecurityManager,当然这个我们已经写在application-shiro.xml中了,因此Spring会帮我们生成,接着将自定义Realm注入到SecurityManager中,当然中间肯定有一个绑定SecurityManager到Shiro上下文环境的一步,因此我这边引用一下SecurityUtils的bind方法,形象的表达出这个意思。
完成上面的准备工作之后(当然远远不止这样,这里我们稍微简化一下,突出重点 ),假设我们从页面上输入账号和密码进行登陆操作,账号和密码会被进一步封装为token,然后Subject会带着token找SecurityManager,接着就是authenticate认证过程了(这个认证方法是由AuthenticatingSecurityManager调用的 )。
Realm收到指示之后,会根据账号去数据库查询是否有该用户,当然中间还涉及到凭证匹配器去匹配密码是否正确的操作,这一部分产生异常的几率极高,因为Shrio处理认证失败都是以异常的形式抛出去的,如果捕捉到异常,会直接返回,不会继续下面的操作。
当然,如果认证成功,Realm则会将获取到的用户信息返回给SecurityManager,随之SecurityManager将info封装到Subject中,返回最终的认证结果给Login Page。
在这里插入图片描述
到此为止,咱们的Shiro源码解析篇就结束啦,当然啦,可能我讲的还是冰山一角,但是讲这个的目的主要是为了帮大家梳理Shrio的运作流程,只有当你熟悉了它的流程之后,使用起来才能得心应手,当然也会对你今后更深入的研究Shrio还是有帮助的,希望这篇博文能让各位受益,如果有分析的不正确或者不完整的地方,欢迎指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值