記事のディレクトリ
I.はじめに
Shiroを使ったことがないので、最近使い始めたので、そのプロセスとソースコードをいくつか読んで、内容をまとめて記録しました。このシリーズでは、Shiroのすべてのコードを完全に分析するわけではありませんが、主な(I)必要性(使用)フロー(to)プロセス(の)を簡単に分析するだけです。このシリーズのほとんどは個人的な理解のためのものであり、個人的な学習スキルは良くないので、「不当で誤った混乱」が起こることは避けられません。何か見つけたら訂正してくれてありがとうございます。
Shiroソースコード分析の完全な作業:
- Shiroソースコード分析①:簡単なプロジェクト構築
- Shiroソースコード分析②:AbstractShiroFilter
- 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);
と呼びます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;
}
ここでは、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に基づいて新しいサブジェクトを作成する必要があります。このサブジェクトを作成すると、セッションが作成され、プリンシパルと認証済み(現在のセッションのセッション状態が認証されたことを記録します。これはtrue)がセッションに保存されます。
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)
件名を作成します。
2つの違いは次のとおりです。AbstractShiroFilterは、現在のスレッドをバインドするサブジェクトを作成するために呼び出されます。セッションがある場合は、セッションにキャッシュされている情報を抽出してサブジェクトに入力します。セッションがない場合は、積極的にセッションを作成するのではなく、直接サブジェクトに戻ります。
認証プロセスでは、認証が成功すると、認証された情報がセッションにキャッシュされます。セッションが存在する場合、情報はマージされます。セッションが存在しない場合、セッションが作成され、情報がキャッシュされます。 。
通常、AbstractShiroFilterはセッションからデータを読み取り、認証プロセスはセッションにデータを書き込みます。
createSubject(context);の内容の詳細については、Shiroソースコード分析②:AbstractShiroFilterを参照してください。
この時点で、ログイン認証プロセス全体が終了しました。
ログインプロセス全体の簡単な要約:
http://localhost:8081/shiro/login?userName=张三&password=123456
リクエストが送信されると、最初に通過しAbstractShiroFilter
ます。AbstractShiroFilterは、現在のスレッドをバインドするサブジェクトを作成すると同時に、リクエストを適切なフィルターに配布します。ここでは、それをAnonymousFilterに配布します。つまり、直接放します。- その後
AbstractShiroFilter
、直接ShiroDemoController#login
メソッドになりましたsubject.login(usernamePasswordToken);
。直接呼び出しがあります。その中で、カスタムレルムの認証方法が認証のために呼び出されます。認証が作成された場合、Session、willprincipals
、およびauthenticated
状態がSessionに保存されます。
上:コンテンツ部分はネットワーク
https://blog.csdn.net/dgh112233/article/details/100083287
https://www.zhihu.com/pin/1105962164963282944http://www.muzhuangnet.com/showを
参照しています/ 771. htmlが
邪魔になる場合は、連絡して削除してください。コンテンツは、自己記録と学習にのみ使用されます。エラーがあれば訂正してください