文章目录
一、前言
由于之前没有使用过 Shiro,最近开始使用,故对其部分流程和源码进行了阅读,大体总结了一些内容记录下来。本系列并不会完完全全分析 Shiro 的全部代码,仅把主(我)要(用)流(到)程(的) 简单分析一下。由于本系列大部分为个人内容理解 并且 个人学艺实属不精,故难免出现 “冤假错乱”。如有发现,感谢指正,不胜感激。
Shiro 源码分析全集:
当我们 进行 http://localhost:8081/shiro/login?userName=张三&password=123456
请求时,首先会经过 AbstractShiroFilter 过滤器,在经过 AbstractShiroFilter 过滤器之后,就到达我们登录请求的实质请求了。
二、认证流程
如下,登录接口如下:
@PostMapping("login")
public String login() {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("张三", "123456");
Subject subject = SecurityUtils.getSubject();
subject.login(usernamePasswordToken);
return "login";
}
我们直接来看 subject.login(usernamePasswordToken);
。其代码实现在 DelegatingSubject#login
,详细代码如下:
public void login(AuthenticationToken token) throws AuthenticationException {
// 1. 清空之前会话缓存
clearRunAsIdentitiesInternal();
// 2. 交由安全管理器来进行验证操作,通过这一步,则说明验证通过了
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
// 获取 Subject 中的的 principals
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
// ... 抛出异常
}
// 记录下 principals, 并将 authenticated = true,表示当前 Subject 验证通过
this.principals = principals;
this.authenticated = true;
// 针对 HostAuthenticationToken 保存其 host
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
// 获取session
Session session = subject.getSession(false);
if (session != null) {
// 装饰 session, 将 session 包装成 StoppingAwareProxiedSession 类型
this.session = decorate(session);
} else {
this.session = null;
}
}
1. clearRunAsIdentitiesInternal();
在执行方法前,将之前缓存的用户信息清空,因为要重新验证。如果存在缓存则清除,否则什么也不做。
// org.apache.shiro.subject.support.DelegatingSubject.RUN_AS_PRINCIPALS_SESSION_KEY
private static final String RUN_AS_PRINCIPALS_SESSION_KEY =
DelegatingSubject.class.getName() + ".RUN_AS_PRINCIPALS_SESSION_KEY";
private void clearRunAsIdentities() {
Session session = getSession(false);
if (session != null) {
// 清除 session 中的 org.apache.shiro.subject.support.DelegatingSubject.RUN_AS_PRINCIPALS_SESSION_KEY 属性
session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
}
2. securityManager.login(this, token);
这里是整个登录验证的核心逻辑,这一步如果结束,则说明验证通过。securityManager.login(this, token);
的具体实现在 DefaultSecurityManager#login
中 。详细代码如下 :
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
// 1. 进行验证
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
// 登录错误时的回调。可供扩展。默认是针对 RememberMeManager 的操作,登录失败则使之前的记住状态失效
onFailedLogin(token, ae, subject);
} catch (Exception e) {
// ... 异常日志
}
throw ae; //propagate
}
// 2. 创建 subject
Subject loggedIn = createSubject(token, info, subject);
// 登录成功时的回调。基本同错误时相同
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
上面的代码还是比较简洁的,下面我们分步解析内容
2.1 authenticate(token);
authenticate(token);
中会调用 this.authenticator.authenticate(token);
,我们这里直接来看 this.authenticator.authenticate(token);
的具体实现 AbstractAuthenticator#authenticate
:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
// ... 日志异常校验
AuthenticationInfo info;
try {
// 带do的方法才是真正做事的方法,这里开始了验证工作
info = doAuthenticate(token);
// 验证返回的是null则说明验证失败
if (info == null) {
// ...抛出异常
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
// ...抛出异常
}
try {
// 通知 AuthenticationListener 监听器验证失败
notifyFailure(token, ae);
} catch (Throwable t2) {
// ...抛出异常
}
throw ae;
}
...
// 通知 AuthenticationListener 监听器验证成功
notifySuccess(token, info);
return info;
}
...
// doAuthenticate 的实现在其子类 ModularRealmAuthenticator#doAuthenticate
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
// 获取 所有的 Realm
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) {
// 如果当前Realm不支持 当前token,则抛出异常
if (!realm.supports(token)) {
// ... 抛出异常
}
// 调用 realm 的 getAuthenticationInfo 方法,这里可以看到,如果 getAuthenticationInfo 方法返回为null会直接抛出异常,下面详解
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
// ... 抛出异常
}
return info;
}
...
// 多Realm的处理
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
// 获取认证策略
AuthenticationStrategy strategy = getAuthenticationStrategy();
// 在所有Realm进行前 进行回调
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
for (Realm realm : realms) {
// 在每个Realm进行前 进行回调
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
AuthenticationInfo info = null;
Throwable t = null;
try {
// 调用 realm 的 getAuthenticationInfo 方法,下面详解
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
}
// 在每个Realm进行后 进行回调
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
// ... 打印日志
}
}
// 在所有Realm进行后 进行回调
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
这里我们可以看到,针对单一Realm 和 多Realm, Shiro使用了不同的处理方法。
上面代码中,我们看到关键的核心一句就是 realm.getAuthenticationInfo(token)
,其实现AuthenticatingRealm#getAuthenticationInfo
如下:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 1. 获取缓存的 AuthenticationInfo 信息,如果没有缓存,则返回null。解析成功将在第三步将其缓存
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
// 2. 调用我们自己定义的逻辑处理 重新处理一遍
info = doGetAuthenticationInfo(token);
if (token != null && info != null) {
// 3. 解析结束如果 AuthenticationInfo 不为空则进行结果缓存
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
// ... 日志打印
}
if (info != null) {
// 调用 CredentialsMatcher 进行密码校验
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
...
// 进行凭证验证
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
// 获取指定的 CredentialsMatcher (凭证匹配器)
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
// 调用 指定的凭证匹配器进行匹配
if (!cm.doCredentialsMatch(token, info)) {
// 抛出异常
}
} else {
// ... 抛出异常
}
}
2.2 createSubject(token, info, subject);
当登录验证结束后,会返回 AuthenticationInfo,我们则需要根据 AuthenticationInfo 来创建一个新的Subject。在创建这个Subject的时候,会创建一个Session,同时将 principals 和 authenticated (记录当前Session的会话状态已经通过认证,即为true) 状态会被保存到Session 中。
createSubject(token, info, subject);
其详细实现在DefaultSecurityManager#createSubject(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo, org.apache.shiro.subject.Subject)
中,代码如下:
// 创建 Subject
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
// 根据不同的 SecurityManager 可能创建 DefaultSubjectContext 和 DefaultWebSubjectContext 。
// 这里由于是 DefaultWebSecurityManager 所以创建的类型是 DefaultWebSubjectContext
SubjectContext context = createSubjectContext();
// 设置标识,表明已经认证通过,在 save(subject); 中 mergeAuthenticationState(subject); 保存的就是该状态,表明验证通过,在之后的请求中会使用到
context.setAuthenticated(true);
// 将token保存到上下文中
context.setAuthenticationToken(token);
// 将认证的返回信息保存到上下文中
context.setAuthenticationInfo(info);
if (existing != null) {
// 将 Subject 信息保存到上下文中
context.setSubject(existing);
}
// 开始创建Subject
return createSubject(context);
}
...
// 这里的方法和 AbstractShiroFilter 文章中介绍的相同,这里就不会详细介绍了,会着重说明两处创建Subject的不同
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
// 拷贝一个 SubjectContext 副本
SubjectContext context = copy(subjectContext);
//ensure that the context has a SecurityManager instance, and if not, add one:
// 1. 确保上下文具有SecurityManager实例,如果没有,添加一个
context = ensureSecurityManager(context);
// 2. 解析session。
context = resolveSession(context);
// 3. 解析 Principals
context = resolvePrincipals(context);
// 4. 创建 一个全新的 Subject
Subject subject = doCreateSubject(context);
// 5. 保存(缓存) subject 。与AbstractShiroFilter 不同的是,这里会去创建Session,并会写入Cookies 中
save(subject);
return subject;
}
在 AbstractShiroFilter
中也会调用 createSubject(SubjectContext subjectContext)
来创建Subject。
二者的不同之处在于:AbstractShiroFilter 在调用时目的是为了创建一个Subject来绑定当前线程,如果存在Session,则将Session中缓存的信息提取出来,填充至Subject,如果不存在Session,并不会主动创建Session,而是直接返回Subject。
而在认证流程,当认证成功后,这里会将认证后的信息缓存到Session中,如果Session存在,则进行信息合并,如果Session不存在,则会创建一个Session,再将信息进行缓存。
总的来说,AbstractShiroFilter 会从Session中读取数据,认证流程会向Session中写入数据。
关于 createSubject(context); 内容的具体讲解,请参考 Shiro源码分析② :AbstractShiroFilter
至此,整个登录认证流程就已经结束了。
简单总结整个登录过程:
http://localhost:8081/shiro/login?userName=张三&password=123456
请求发送过来时,会先经过AbstractShiroFilter
。AbstractShiroFilter 会创建一个Subject绑定当前线程,同时会将 该请求分发给合适的过滤器,我们这里是分发给 AnonymousFilter,也就是直接放行。- 经过
AbstractShiroFilter
之后,我们直接来到了ShiroDemoController#login
方法,这里面直接调用了subject.login(usernamePasswordToken);
。而在这其中,会调用我们自定义的 Realm 中的认证方法来进行认证。如果认证通过,则会创建Session,将principals
和authenticated
状态会被保存到Session 中。
以上:内容部分参考网络
https://blog.csdn.net/dgh112233/article/details/100083287
https://www.zhihu.com/pin/1105962164963282944
http://www.muzhuangnet.com/show/771.html
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正