《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 https://my.oschina.net/xiaoqiyiye/blog/1618339 「xiaoqiyiye」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本文在于分析Shiro源码,对于新学习的朋友可以参考
[开涛博客](http://jinnianshilongnian.iteye.com/blog/2018398)进行学习。

Authenticator就是认证器,在Shiro中负责认证用户提交的信息,在Shiro中我们用AuthenticationToken来表示提交的信息。Authenticator接口只提供了一个认证的方法。如下。

/**
* 认证用户提交的信息AuthenticationToken对象,AuthenticationToken包含了身份和凭证。
* 如果认证成功则返回AuthenticationInfo对象,AuthenticationInfo对象代表了用户在Shiro中已经被认证过的账户数据。
* 如果认证失败则抛出一下异常
* @see ExpiredCredentialsException 凭证过期
* @see IncorrectCredentialsException 凭证错误
* @see ExcessiveAttemptsException 多次尝试失败
* @see LockedAccountException 账户锁定
* @see ConcurrentAccessException 并发访问异常(多点登录)
* @see UnknownAccountException 账户未知
*/
public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;

AuthenticationToken分析

AuthenticationToken对象代表了身份和凭证。从下面的接口看提供的方法很简单,但返回的对象都是Object,也就是说在Shiro中对身份和凭证的类型没有限制,Shiro没有提供特有的类型来处理。

public interface AuthenticationToken extends Serializable {

/**
* 获取身份
*/
Object getPrincipal();

/**
* 获取凭证
*/
Object getCredentials();

}

在Shiro中只提供了一种具体的实现类UsernamePasswordToken。UsernamePasswordToken类是以用户名作为身份,密码作为凭证。当然它也实现了RememberMeAuthenticationToken接口,提供rememberMe功能。rememberMe功能的实现在后面再分析,在这里不是重点。UsernamePasswordToken很简单,只有构造方法和setter/getter方法。我们需要对UsernamePasswordToken中的身份和凭证要有很好的理解,什么可以作为身份,什么又是凭证。

AuthenticationInfo分析

AuthenticationInfo表示被Subject存储的账户,这个账户是经过认证的。而AuthenticationToken中的身份/凭证是用户提交的数据,还没有经过认证,如果认证成功才会被存储在AuthenticationInfo中。

AuthenticationInfo只有一个实现类SimpleAuthenticationInfo(备注:SimpleAccount也是其中一个实现类,但功能是完全依赖SimpleAuthenticationInfo实现的)。AuthenticationInfo还有两个子接口,分别是:SaltedAuthenticationInfo和MergableAuthenticationInfo。SaltedAuthenticationInfo提供了获取凭证加密盐的方法,MergableAuthenticationInfo可以合并验证后的身份信息。各自的接口都很简单,下面直接分析SimpleAuthenticationInfo具体实现。

SimpleAuthenticationInfo详细分析

下面是SimpleAuthenticationInfo的属性和构造方法。下面有很多构造方法,但可以看出实现都依赖到SimplePrincipalCollection对象。SimplePrincipalCollection对象负责收集身份(principals)和域(realm)的关系。关于SimplePrincipalCollection我们暂时不展开分析。

/**
* 身份集合
*/
protected PrincipalCollection principals;

/**
* 凭证
*/
protected Object credentials;

/**
* 凭证盐
*/
protected ByteSource credentialsSalt;

public SimpleAuthenticationInfo() {
}

public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = credentials;
}

public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = hashedCredentials;
this.credentialsSalt = credentialsSalt;
}

public SimpleAuthenticationInfo(PrincipalCollection principals, Object credentials) {
this.principals = new SimplePrincipalCollection(principals);
this.credentials = credentials;
}

public SimpleAuthenticationInfo(PrincipalCollection principals, Object hashedCredentials, ByteSource credentialsSalt) {
this.principals = new SimplePrincipalCollection(principals);
this.credentials = hashedCredentials;
this.credentialsSalt = credentialsSalt;
}

在SimpleAuthenticationInfo中,我们主要分析一下merge(AuthenticationInfo info)方法,也就是说可以合并其他的AuthenticationInfo信息。

public void merge(AuthenticationInfo info) {

// 判断是否有身份信息,如果没有就返回
if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) {
return;
}

// 合并身份集合
if (this.principals == null) {
this.principals = info.getPrincipals();
} else {
if (!(this.principals instanceof MutablePrincipalCollection)) {
this.principals = new SimplePrincipalCollection(this.principals);
}
((MutablePrincipalCollection) this.principals).addAll(info.getPrincipals());
}

// 凭证盐只是在Realm凭证匹配过程中使用
// 如果存在凭证盐,就不用管其他的了,如果没有就使用其他的凭证盐
if (this.credentialsSalt == null && info instanceof SaltedAuthenticationInfo) {
this.credentialsSalt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
}

// 合并凭证信息
Object thisCredentials = getCredentials();
Object otherCredentials = info.getCredentials();

if (otherCredentials == null) {
return;
}

if (thisCredentials == null) {
this.credentials = otherCredentials;
return;
}

// 使用集合来合并凭证
if (!(thisCredentials instanceof Collection)) {
Set newSet = new HashSet();
newSet.add(thisCredentials);
setCredentials(newSet);
}

Collection credentialCollection = (Collection) getCredentials();
if (otherCredentials instanceof Collection) {
credentialCollection.addAll((Collection) otherCredentials);
} else {
credentialCollection.add(otherCredentials);
}
}

AbstractAuthenticator抽象类

和AbstractSessionManager一样,AbstractAuthenticator主要功能也是提供监听器,对认证过程中的状态进行监听。在认证过程中监听成功、失败、登出情况。监听器是AuthenticationListener,下面看一下监听器提供的方法。

public interface AuthenticationListener {

/**
* 监听认证成功
*/
void onSuccess(AuthenticationToken token, AuthenticationInfo info);

/**
* 监听认证失败
*/
void onFailure(AuthenticationToken token, AuthenticationException ae);

/**
* 监听用户登出
*/
void onLogout(PrincipalCollection principals);
}

另外,AbstractAuthenticator实现了Authenticator#authenticate(AuthenticationToken token),处理了对监听器通知的情况,但执行认证的具体过程提供抽象方法doAuthenticate(AuthenticationToken token)让子类完成。

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {

// Token参数异常
if (token == null) {
throw new IllegalArgumentException("Method argumet (authentication token) cannot be null.");
}

AuthenticationInfo info;
try {
// 执行认证过程的抽象方法,子类去实现
info = 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 t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
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, t);
}
try {
// 认证失败了,通知监听器
notifyFailure(token, ae);
} catch (Throwable t2) {

}

throw ae;
}

// 认证成功,通知监听器
notifySuccess(token, info);

return info;
}

ModularRealmAuthenticator类分析

在Shiro中只提供了一个具体的实现类ModularRealmAuthenticator,该类可以处理多个Realm的认证方式。

在ModularRealmAuthenticator中,把认证的权利交给域(Realm)去完成,在Shiro中Realm相当于数据的来源,可以自定义。ModularRealmAuthenticator支持多个Realm进行认证,在多个Realm认证时,需要设置认证策略,策略接口是AuthenticationStrategy。在Shiro中提供了三种认证策略。分别是:

  1. AllSuccessfulStrategy:所有Realm认证成功。
  2. AtLeastOneSuccessfulStrategy:至少有一个Realm认证成功。
  3. FirstSuccessfulStrategy: 第一个Realm认证成功。

关于认证策略我们在后面在分析,现在继续分析ModularRealmAuthenticator。我们还是从属性和构造方法分析。

// 认证的过程交由Realm去处理
private Collection<Realm> realms;

// 指定认证策略
private AuthenticationStrategy authenticationStrategy;

public ModularRealmAuthenticator() {
// 默认提供策略:至少有一个Realm认证成功就算认证成功
this.authenticationStrategy = new AtLeastOneSuccessfulStrategy();
}

下面我看看ModularRealmAuthenticator是如何实现doAuthenticate(token)方法的。

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
// 判断realms属性,必须要有Realm
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
// 分支:如果只有一个就按照单个流程处理,如果有多个Realm就按照多个流程走认证策略。
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)) {
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);
}

// realm处理认证过程,处理过程中可能会抛出认证异常AuthenticationException。
// 如果认证成功info不会用null。
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;
}

// 处理多个Realm
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {

AuthenticationStrategy strategy = getAuthenticationStrategy();

// 返回一个空的聚合对象
// AllSuccessfulStrategy - 返回空的SimpleAuthenticationInfo对象
// AtLeastOneSuccessfulStrategy - 返回空的SimpleAuthenticationInfo对象
// FirstSuccessfulStrategy - 返回null
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);

for (Realm realm : realms) {
// 认证前处理
// AllSuccessfulStrategy - 判断realm.supports(token),如果不支持直接抛异常,返回aggregate
// AtLeastOneSuccessfulStrategy - 返回aggregate
// FirstSuccessfulStrategy - 返回aggregate,也就是null
aggregate = strategy.beforeAttempt(realm, token, aggregate);

if (realm.supports(token)) {
AuthenticationInfo info = null;
Throwable t = null;
try {
//认证过程是由Realm处理的
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}

// 认证后处理,
// AllSuccessfulStrategy - 如果有异常会抛出异常, 如果没有就合并info和aggregate
// AtLeastOneSuccessfulStrategy - 如果有异常并不会抛出,只是会合并info和aggregate
// FirstSuccessfulStrategy - 如果aggregate存在,则返回aggregate;否则返回info
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);

} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}

// AllSuccessfulStrategy - 返回aggregate
// AtLeastOneSuccessfulStrategy - 判断aggregate不为空,否则抛异常
// FirstSuccessfulStrategy - 返回aggregate
aggregate = strategy.afterAllAttempts(token, aggregate);

return aggregate;
}

通过对上面的代码分析:

  • AllSuccessfulStrategy策略流程:逐一处理Realm,每个Realm必须支持token处理,然后合并AuthenticationInfo。如果遇到异常,则抛出异常结束循环。
  • AtLeastOneSuccessfulStrategy策略流程:逐一处理Realm,不支持处理token的Realm跳过。如果遇到异常,忽略对异常的处理。对于认证通过的AuthenticationInfo进行合并成aggregate,最后判断aggregate,aggregate不能为空,如果有空抛出异常。
  • FirstSuccessfulStrategy策略流程:逐一处理Realm,不支持处理token的Realm跳过。如果遇到异常,忽略对异常的处理。在循环处理Realm前aggregate=null,重点是在strategy.afterAttempt(realm, token, info, aggregate, t)的处理上,并不会合并info和aggregate。如果aggregate为空,则返回info。所以在处理后返回的总是第一个认证成功的AuthenticationInfo。

总结

Authenticator负责对AuthenticationToken进行认证,然后返回一个已经被认证过的信息AuthenticationInfo。Authenticator也提供了监听器AuthenticationListener,对认证状态进行监听。Authenticator真实的认证过程是由Realm来处理的,可以支持都多个Realm来认证,认证的过程中可以选择不同的认证策略。

文章目录
  1. 1. AuthenticationToken分析
  2. 2. AuthenticationInfo分析
    1. 2.1. SimpleAuthenticationInfo详细分析
  3. 3. AbstractAuthenticator抽象类
  4. 4. ModularRealmAuthenticator类分析
  5. 5. 总结