写点什么

Shiro 认证源码图文解析

  • 2022 年 4 月 23 日
  • 本文字数:9169 字

    阅读完需:约 30 分钟

[](()RealmSecurityManager

RealmSecurityManager 里面只有一个属性,一个 Realms 的集合,是所有安全数据来源的集合



里面对于添加 Realm,既可以添加一个,也可以添加多个



所以 SecurityManager 可以装配我们自定义的 Realm 或者 Realms。

[](()AuthenticatingSecurityManager

AuthenticatingSecurityManager 是负责认证管理的,所以前面 Shiro 图的认证管理器就是在这里,里面只有一个熟悉感,就是认证管理器 Authenticator。



可以看到,认证器默认的引用类型是 ModularRealmAuthenticator。

[](()AuthorizingSecurityManager

AuthorizingSecurityManager 是负责授权管理的,也就是前面的授权器所在类,里面也是只有一个属性,就是授权器 Authorizer



也是很容易看到,授权器的默认实现方式为 ModularRealmAuthorizer。

[](()SessionSecurityManager

SessionSecurityManager 是负责会话管理的,里面也只有一个会话管理器属性,默认为 DefaultSessionManager


[](()DefaultSecurityManager

拥有上面提到的所有 SecurityManager 的功能,具体的 SecurityManager.login 方法就是在这里实现的。


public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {


//该变量存储认证信息


AuthenticationInfo info;


try {


//获取认证信息


info = this.authenticate(token);


} catch (AuthenticationException var7) {


//下面的操作都是由获取认证信息步骤引起的


//要返回上一步,看认证信息步骤


AuthenticationException ae = var7;


try {


this.onFailedLogin(token, ae, subject);


} catch (Exception var6) {


if (log.isInfoEnabled()) {


log.info("onFailedLogin method threw an exception. Logging and propagating original AuthenticationException.", var6);


}


}


throw var7;


}


//根据认证信息和 token 创建出 Subject 对象,所以要先管抓住认证信息怎么拿


Subject loggedIn = this.createSubject(token, info, subject);


this.onSuccessfulLogin(token, info, loggedIn);


return loggedIn;


}

[](()DefaultSecurityManager 获取认证信息

来到这一步,我们首先要去认识 AuthenticationInfo 这个对象,因为后面的比较都与它相关。

[](()AuthenticationInfo 对象

记得 Realm 中,在认证方法中,我们返回的是 SimpleAuthenticationInfo 这个对象的,这个对象跟 AuthenticationInfo 接口相关,所以我们从这里入手



我们可以看到这个接口,被 3 个子接口继承,而这三个子接口,总体上被 2 个类去实现,一个是 SimpleAccount,另一个是 SimpleAuthenticationInfo。


我们先来认识一下这四个接口吧

[](()AuthenticationInfo


里面只有两个方法,分别是获取认证信息和密码的。

[](()MergableAuthenticationInfo


可以看到他新增了一个合并认证信息的功能(认证信息如何进行合并的呢?下面再说)

[](()Account


Account 没有新增抽象方法,但还多继承了一个 AuthorizationInfo(用来授权的,以后再说)

[](()SaltedAuthenticationInfo


里面增加了一个获取盐值的方法(ByteSouce 类型)。

[](()认识 SimpleAuthenticationInfo


从上图源码,可以看到,SimpleAuthenticationInfo 实现了 MergableAuthenticationInfo 和 SaltedAuthenticationInfo 接口,所以他就有这两个接口的所有方法,并且有三个属性


  • PrincipalCollection principals 认证信息集合

  • credentials:密码

  • credentialsSalt:盐值


我们从构造方法开始入手



可以看到里面包括无参构造,总共有 5 个构造方法,盐值(credentialsSalt 可以不注入)和 credentials(密码)都是注入的,而 principals 是通过构造方法出来的,创建一个 SimplePrincipalsCollection


[](()SimplePrincipalsCollection


下面我们进入 SimplePrincipalsCollection 看看其到底是什么架构,


public class SimplePrincipalCollection implements MutablePrincipalCollection {


//序列化 id,可以进行序列,然后放到内存中


private static final long serialVersionUID = -6305224034025797558L;


//所有 realm 的认证信息,一个 map 集合,键是 String 类型,Value 是 Set 类型


private Map<String, Set> realmPrincipals;


//内存中的对象反序列化回来变成字符串,transizent 是让这个属性不被序列化


private transient String cachedToString;


public SimplePrincipalCollection() {


}


/**


  • 构造方法都是使用 addAll 或者 add 方法来进行初始化的

  • 所以下面具体看一下这两个方法


**/


public SimplePrincipalCollection(Object principal, String realmName) {


if (principal instanceof Collection) {


this.addAll((Collection)principal, realmName);


} else {


this.add(principal, realmName);


}


}


public SimplePrincipalCollection(Collection principals, String realmName) {


this.addAll(principals, realmName);


}


public SimplePrincipalCollection(PrincipalCollection principals) {


this.addAll(principals);


}


//。。。。


}


add 方法


/**


  • add 方法源码


**/


public void add(Object principal, String realmName) {


//如果 realmName 或者 principal 为空,就抛出异常


if (realmName == null) {


throw new IllegalArgumentException("realmName argument cannot be null.");


} else if (principal == null) {


throw new IllegalArgumentException("principal argument cannot be null.");


}


//不为空,就初始化 cachedToString 属性


//然后调用 getPrincipalslazy 方法


else {


this.cachedToString = null;


//拿到 realmName 对应的 set 集合,存放 principal 信息


this.getPrincipalsLazy(realmName).add(principal);


}


}


/**


  • getPrincipalsLazy 方法具体实现


**/


protected Collection getPrincipalsLazy(String realmName) {


//我们可以看到,其实这个方法是用来初始化 relamPrincipals 的


//如果是第一次加入,先对 realmPrincipals 进行初始化


if (this.realmPrincipals == null) {


//第一次定义为是一个 LinkedHashMap


//这个 LinkedHashMap,键是 realmName,值是一个 set 集合


this.realmPrincipals = new LinkedHashMap();


}


//通过键值对方式,获取 realmPrincipals 的对应 realmName 的 set 集合


Set principals = (Set)this.realmPrincipals.get(realmName);


//如果没有,证明该 Realm 是第一次存放认证信息,还没有进行与其他 Realm 的信息合并


if (principals == null) {


//set 集合具体是一个 LinkedHashSet


principals = new LinkedHashSet();


//往 realmPrincipals 中放入这 realmName 为 key,principals 为值的键值对


this.realmPrincipals.put(realmName, principals);


}


//如果已经有了,证明已经合并过了


//返回 realmName 对应的 set 集合


return (Collection)principals;


}


所以 SimplePrincipalCollection 的底层是一个 LinkedHashMap,以 RealmName 为键,是一个字符串对象,值对应的是一个 LinkedHashSet 集合,里面存放的就是 principal(账号信息)



我们可以看到 key 是 Realm 的全限定符,value 里面存的就是是一个以账号组成的 LinkedHashSet。


为什么要用 LinkedHashSet 呢?一个 Realm 里面还会存在多个账号的吗?



下面回到我们的认证过程


DefaultSecurityManager 调用自身的 authenticate 方法发来获取认证信息,该方法实现具体如下



点进去,发现 Authenticator 是一个接口,而且拥有两个实现类,那么具体是哪一个呢?



这里,我们必须返回到上一层看一下 DefaultSecurityManager 的 authenticator 是哪一个,去看看到底调用的是哪一个实现类,前面已经提到过,DefaultSecurityManager 的 authenticator(认证器)是 AuthenticatingSecurityManager 的认证器,所以默认的类型是 ModularRealmAuthenticator。


![在这里插入图片描述](https://img-blog.csdnimg.cn/20210428184247885.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 ,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0dEVVRfVHJpbQ==,size_16,color_FFFFFF,t_70#pic_center)


我们可以看到 ModularRealmAuthenticator 是继承 AbstractAuthenticator 的,而且并没有 authenticate 方法的实现,所以可以断定,authenticate 的实现肯定在父类 AbstractAuthenticator 中



在里面,很轻易找到了对应具体实现方法


public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {


//如果 token 不存在,抛出异常


if (token == null) {


throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");


} else {


log.trace("Authentication attempt received for token [{}]", token);


//该变量用于记录认证信息


AuthenticationInfo info;


try {


//调用自身的 doAuthenticate 方法获取认证信息


info = this.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 var8) {


AuthenticationException ae = null;


if (var8 instanceof AuthenticationException) {


ae = (AuthenticationException)var8;


}


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, var8);


if (log.isWarnEnabled()) {


log.warn(msg, var8);


}


}


try {


this.notifyFailure(token, ae);


} catch (Throwable var7) {


if (log.isWarnEnabled()) {


String msg = "Unable to send notification for failed authentication attempt - listener error?. Please check your AuthenticationListener implementation(s). Logging sending exception and propagating original AuthenticationException instead...";


log.warn(msg, var7);


}


}


throw ae;


}


log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);


this.notifySuccess(token, info);


return info;


}


}


我们可以看到,这里是调用了自身的 doAuthenticate 方法去获取认证信息的,所以,下面就去看看这个方法


protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken var1) throws AuthenticationException;


这个方法是一个抽象方法,所以肯定是由 AbstractAuthenticator 的子类 ModularRealmAuthenticator 去实现的



具体实现如下


protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {


//判断是否有 Realm 装配


this.assertRealmsConfigured();


//获取所有 Realm


Collection<Realm> realms = this.getRealms();


//如果只有一个 Realm,就只调用那个 Realm(通过迭代器获取)


//如果有多个 Realm,就多个执行


return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);


}


好了,现在弄清楚认证方法是怎样的了,具体的规则如下


  • 如果只有一个 Realm,就只调用那一个 Realm

  • 如果有多个 Realm,都调用。


下面我们进入到 Realm 认证中

[](()ModularRealmAuthenticator 的 doSingleRealmAuthentication

这是源码


protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {


//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);


} else {


//从 Realm 中取出认证信息,即调用 Realm 的认证方法


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);


} else {


return info;


}


}


}


进入 realm.getAuthenticationInfo()



可以看到 Realm 是一个接口,并且实现该接口的类,主要有 CachingRealm,AuthenticatingRealm 和 AuthorizingRealm,这里也是使用装饰器模式,一层一层递进,而实现 getAuthenticationInfo 的 Realm 是 AuthenticatingRealm(这里三个 Realm 都是抽象类,实现接口是不需要去实现里面的方法的


下面是源码


public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {


//先从缓存中获取


AuthenticationInfo info = this.getCachedAuthenticationInfo(token);


if (info == null) {


//如果缓存中没有,就通过 doGetAuthenticationInfo 中获取


info = this.doGetAuthenticationInfo(token);


log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);


if (token != null && info != null) {


//将 token 和 info 都放入缓存(前面已经判断缓存中没有)


this.cacheAuthenticationInfoIfPossible(token, info);


}


} else {


log.debug("Using cached authentication info [{}] to perform credentials matching.", info);


}


if (info != null) {


//如果 Info 不为空,证明存在账号,然后进行校验密码


this.assertCredentialsMatch(token, info);


} else {


log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);


}


return info;


}


步骤总结如下


  1. 首先尝试从缓存中取出 info

  2. 缓存中取不出就从 doAuthenticationInfo 方法中取

  3. 此时再判断 info 和 token 是否为 NULL,如果不为 NULL,就放入缓存中(前面已经判断缓存中没有)

  4. 然后通过 assertCredentialsMatch 方法进行校验密码


下面进入到 doGetAuthenticationInfo 里面看一下



这是一个抽象方法,这个方法其实就是我们自定义 Realm 时要去实现认证方法。


然后我们回到上一层,看一下,密码是如何校验的


下面是密码校验的源码


protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {


//拿到密码匹配器


CredentialsMatcher cm = this.getCredentialsMatcher();


if (cm != null) {


//如果密码不匹配(使用 token 和前面获取到的认证信息进行匹配),抛出异常


if (!cm.doCredentialsMatch(token, info)) {


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.");


}


}

[](()cm.doCredentialsMatch 方法进行密码匹对

我们再详细去看一下 cm.doCredentialsMatch 方法。


可以看到,这是一个接口方法,而且有三个实现类


  1. AllowCredentialsMatcher

  2. PasswordMatcher

  3. SimpleCredentialsMatcher



AllowAllCredentialsMatcher


一直返回都是 True,即所有匹配都是成功的,无论密码是什么都会验证成功


public class AllowAllCredentialsMatcher implements CredentialsMatcher {


public AllowAllCredentialsMatcher() {


}


/**


  • 返回的都是 true


**/


public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {


return true;


}


}


SimpleCredentialsMatcher


这个很简单,只是进行输入的密码跟 Realm 认证信息里面的密码是否一致即可


public class SimpleCredentialsMatcher extends CodecSupport implements CredentialsMatcher {


private static final Logger log = LoggerFactory.getLogger(SimpleCredentialsMatcher.class);


public SimpleCredentialsMatcher() {


}


protected Object getCredentials(AuthenticationToken token) {


return token.getCredentials();


}


protected Object getCredentials(AuthenticationInfo info) {


return info.getCredentials();


}


/**


  • 这里是匹配的细节


**/


protected boolean equals(Object tokenCredentials, Object accountCredentials) {


if (log.isDebugEnabled()) {


log.debug("Performing credentials equality check for tokenCredentials of type [" + tokenCredentials.getClass().getName() + " and accountCredentials of type [" + accountCredentials.getClass().getName() + "]");


}


//判断能否被序列化字节


if (this.isByteSource(tokenCredentials) && this.isByteSource(accountCredentials)) {


//如果两个参数都可以很简单的变成字符数组


if (log.isDebugEnabled()) {


log.debug("Both credentials arguments can be easily converted to byte arrays. Performing array equals comparison");


}


//将其变成字符数组进行比较


byte[] tokenBytes = this.toBytes(tokenCredentials);


byte[] accountBytes = this.toBytes(accountCredentials);


return MessageDigest.isEqual(tokenBytes, accountBytes);


} else {


//直接字符串进行匹配


return accountCredentials.equals(tokenCredentials);


}


}


/**


*进行验证


**/


public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {


Object tokenCredentials = this.getCredentials(token);


Object accountCredentials = this.getCredentials(info);


//将输入的密码和认证信息里面的密码进行匹配


return this.equals(tokenCredentials, accountCredentials);


}


}


既然都来到这里了,我们再看一看是怎么判断可以序列化的,点进去 this.isByteSource 方法,原来是由 CodeSupport 实现的


/**


  • 就是简单的判断是类型是否为字节数组、字符数组、字符串、文件、输入流等等


**/


protected boolean isByteSource(Object o) {


return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;


}


我们再看看,两个字节数组是怎么进行对比的。


/**


  • Compares two digests for equality. Does a simple byte compare.

  • @param digesta one of the digests to compare.

  • @param digestb the other digest to compare.

  • @return true if the digests are equal, false otherwise.


*/


public static boolean isEqual(byte[] digesta, byte[] digestb) {


//同一个对象,返回 true


if (digesta == digestb) return true;


//都为空,返回 false


if (digesta == null || digestb == null) {


return false;


}


//长度不一样,返回 false


if (digesta.length != digestb.length) {


return false;


}


//遍历比较所有的字节


int result = 0;


// time-constant comparison


for (int i = 0; i < digesta.length; i++) {


//通过异或运算比较,两个相同就返回 1


//即 digesta[i]与 digestb[i]是相同的,就返回 0,不同返回 1


//|=是 result 与右边值进行或运算后,将结果赋给 result


//所以只要出现一次不同,result 就会为 1,因为或运算


result |= digesta[i] ^ digestb[i];


}


return result == 0;


}


可以看到,这里前面使用了,比较两个对象的地址是否一样,和比较数组长度是否一样,来减少运算,如果地址不一样,长度一样,就要进行遍历比较,通过异或运算和或运算来比较(使用异或比较两个字节数组,只有有一个不对应就会返回 1,此时 result 就会一直为 1,因为使用或运算,然后最后比较 result 是否为 0 即可)。


PasswordMatcher


public class PasswordMatcher implements CredentialsMatcher {


//装配一个 PasswordService


private PasswordService passwordService = new DefaultPasswordService();


public PasswordMatcher() {


}


/**


  • 进行密码匹配


**/


public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {


//这一步是确保 PasswordService 装配成功,通过检验 this.passwordService 是否为 Null


PasswordService service = this.ensurePasswordService();


//获取 token 里面的密码


Object submittedPassword = this.getSubmittedPassword(token);


//获取认证信息里面的密码信息


Object storedCredentials = this.getStoredPassword(info);


//判断密码的加密类型(在密码信息中有保存)


//这个加密类型是在 ShiroConfig 中注入 Relam 时有设置的


this.assertStoredCredentialsType(storedCredentials);


//如果加密算法是哈希


if (storedCredentials instanceof Hash) {


//将密码装换成哈希类型


Hash hashedPassword = (Hash)storedCredentials;


//这一步是将默认的密码服务方式强转成哈希密码服务方式


HashingPasswordService hashingService = this.assertHashingPasswordService(service);


//进行密码对比,对比 Realm 的密码和进行哈希加密后的密码


return hashingService.passwordsMatch(submittedPassword, hashedPassword);


} else {


//如果是普通的话,是进行特殊算法进行匹配的,没看懂


String formatted = (String)storedCredentials;


return this.passwordService.passwordsMatch(submittedPassword, formatted);


}


}


//。。。。。。


}

[](()ModularRealmAuthenticator 的 doMultiRealmAuthentication

当有多个 Realm 时,就有问题了,如何判断认证成功呢?认证信息又该是怎样的呢?


现在我们看,当有多个 Realm 时,是怎么进行的


protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {


//AuthenticationStrategy 是认证策略


AuthenticationStrategy strategy = this.getAuthenticationStrategy();


//在经过所有的 Realm 进行认证时的初始化操作


AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);


if (log.isTraceEnabled()) {


log.trace("Iterating through {} realms for PAM authentication", realms.size());


}


Iterator var5 = realms.iterator();


//使用迭代器遍历所有 Realm


while(var5.hasNext()) {


Realm realm = (Realm)var5.next();


//记录进行验证当前 Realm 前的合计结果


aggregate = strategy.beforeAttempt(realm, token, aggregate);


if (realm.supports(token)) {


log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);


AuthenticationInfo info = null;

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Shiro认证源码图文解析_Java_爱好编程进阶_InfoQ写作社区