前言
经过前面阶段的学习,我们从了解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_KEY
和SUBJECT_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(普通的主体对象),多出了下面两个属性
servletRequest
,servletResponse
,这不正是Web中的请求和相应对象么?
Shiro底层解析之登录原理
分析完Suject是如何获取的,咱们就要马不停蹄的去探索下一个 “关卡”。既然现在 “送信” 的有了,那么他是如何将口信捎给 “掌门” SecurityManager,然后 “掌门” 又是怎么通知 “暗部组织” Realm去处理呢?这一切还是个谜!
所以我们继续以subject.login(token)
为突破口,进一步挖掘。果不其然,咱们 “送信” 的小哥Suject把口令给 “掌门” SecurityManager之后,最终还是由 “掌门” 去处理。
正规的工会办事还是严谨的啊!这里有个小细节,就是咱们的账号和密码输入之后是被存放在 “锦囊” UsernamePasswordToken中的,而且判断了密码是否为null?如果是则返回null,否则将密码转为字符数组的形式,至于后面的false
和null
分别代表什么,我也不知道… 所以接着往下面分析呗~
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还是有帮助的,希望这篇博文能让各位受益,如果有分析的不正确或者不完整的地方,欢迎指正!