Shiro的身份验证流程的源码分析

这篇文章将对上一篇文章:SSM项目整合Shiro进行身份验证中的身份验证流程进行分析。

这是项目中的具体使用,分为以下三个步骤:

  1. 创建Subject对象
  2. 封装UsernamePasswordToken 对象token
  3. 调用Subject对象的login方法进行登录认证
Subject currentUser = SecurityUtils.getSubject();
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
currentUser.login(token);  

从应用程序角度的来观察如何使用Shiro完成工作,如图(选取自:第一章 Shiro简介——《跟我学Shiro》):
在这里插入图片描述
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:

  • Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
  • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
  • Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

下面根据这三个步骤来分析相关的类:

一、SecurityUtils

在看SecurityUtils前,先看一下SecurityManager的定义。SecurityManager接口在Shiro中是最为核心的接口。SecurityManager接口的定义:

public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
    //登录
    Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
   //登出
    void logout(Subject subject);
    //创建subject
    createSubject(SubjectContext context);
}

SecurityUtils类核心功能是获取SecurityManager以及Subject。下面来看看SecurityUtils的方法:

1.操作SecurityManager

首先从ThreadContext中获取,如果没有,则从SecurityUtils属性securityManager中获取。一定要存在一个SecurityManager实例对象,否则抛异常。

   public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException {
        SecurityManager securityManager = ThreadContext.getSecurityManager();
        if (securityManager == null) {
            securityManager = SecurityUtils.securityManager;
        }
        if (securityManager == null) {
            String msg = "No SecurityManager accessible to the calling code, either bound to the " +
                    ThreadContext.class.getName() + " or as a vm static singleton.  This is an invalid application " +
                    "configuration.";
            throw new UnavailableSecurityManagerException(msg);
        }
        return securityManager;
    }

此静态方法调用主要用于框架开发支持。我们在applicationContext.xml中注入了securityManager,所以无需手动调用此方法创建securityManager。

    public static void setSecurityManager(SecurityManager securityManager) {
        SecurityUtils.securityManager = securityManager;
    }

值得一提的是SecurityManager 在SecuritUtils中是静态的,也就是说SecurityManager对象在应用中是全局唯一的。

private static SecurityManager securityManager;

2.获取Subject

首先从ThreadContext中获取,如果不存在,则创建新的Subject,再存放到ThreadContext中,以便下次可以获取。

public static Subject getSubject() {
	Subject subject = ThreadContext.getSubject();
	if (subject == null) {
		subject = (new Subject.Builder()).buildSubject();
		ThreadContext.bind(subject);
	}
	return subject;
}

在上面的代码中是先调用 Subject.Builder类提供的无参构造方法,然后在这个无参构造方法中,以当前应用全局唯一的SecurityManager对象为参调用了构造方法。对应当前subject创建了subjectContext,并配置SecurityManager对象。如下:

        public Builder() {
            this(SecurityUtils.getSecurityManager());
        }
        public Builder(SecurityManager securityManager) {
            if (securityManager == null) {
                throw new NullPointerException("SecurityManager method argument cannot be null.");
            }
            this.securityManager = securityManager;
            this.subjectContext = newSubjectContextInstance();
            if (this.subjectContext == null) {
                throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' " +
                        "cannot be null.");
            }
            this.subjectContext.setSecurityManager(securityManager);
        }

而buildSubject()方法则实际上是调用SecurityManager接口中的createSubject(SubjectContext subjectContext)方法。

        public Subject buildSubject() {
            return this.securityManager.createSubject(this.subjectContext);
        }

通过上面的调用即完成了完成了第一步中当前用户subject的创建。

二、UsernamePasswordToken分析

UsernamePasswordToken继承自AutenticationToken,AuthenticationToken的继承关系如图:
在这里插入图片描述
UsernamePasswordToken类是以用户名作为身份,密码作为凭证。也实现了 HostAuthenticationToken,RememberMeAuthenticationToken接口,提供host,rememberMe功能。

public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
    private String username;

    private char[] password;
    private boolean rememberMe = false;
    private String host;

    public UsernamePasswordToken(final String username, final String password) {
        this(username, password != null ? password.toCharArray() : null, false, null);
    }
//一系列的构造方法
//一系列的getter和setter方法
    public void clear() {
        this.username = null;
        this.host = null;
        this.rememberMe = false;

        if (this.password != null) {
            for (int i = 0; i < password.length; i++) {
                this.password[i] = 0x00;
            }
            this.password = null;
        }

    }
//toString() 方法
}

三、Token是如何传到AuthenticatingRealm的

shiro源码分析之Realm调用过程

四、SimpleAuthenticationInfo详细分析

AuthenticationInfo对象中存储的是主体(Subject)的身份认证信息,而AuthenticationToken中的身份/凭证是用户提交的数据,还没有经过认证。Shiro会调用CredentialsMatcher对象的doCredentialsMatch方法对AuthenticationInfo对象和AuthenticationToken进行匹配。匹配成功则表示主体(Subject)认证成功,否则表示认证失败。

而项目中使用的SimpleAuthenticationInfo是AuthenticationInfo的子类,AuthenticationInfo继承关系如图:
在这里插入图片描述

部分源码如下:

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

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

/**
 * 加密的盐值
 */
protected ByteSource credentialsSalt;

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

五、Shiro是怎么进行密码比对的

密码的比对是由AuthenticatingRealm的assertCredentialsMatch方法中调用doCredentialsMatch来进行比对的

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

前面在applicationContext中配置了CredentialsMatcher接口的实现类HashedCredentialsMatcher

    <bean id="jdbcRealm" class="com.learn.shiro.realms.ShiroRealm">
        <property name="credentialsMatcher">
         <!-- 凭证匹配器的类型 -->
        <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
          <!--加密算法 -->
             <property name="hashAlgorithmName" value="MD5"></property>
         <!-- 加密次数 -->
             <property name="hashIterations" value="1024"></property>
        </bean>
        </property>    
    </bean>

于是assertCredentialsMatch()方法中的cm为上面配置的HashedCredentialsMatcher对象,cm.doCredentialsMatch(token, info)接着调用HashedCredentialsMatcher的doCredentialsMatch()方法。

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenHashedCredentials = hashProvidedCredentials(token, info);
        Object accountCredentials = getCredentials(info);
        return equals(tokenHashedCredentials, accountCredentials);
    }

继续进入到hashProvidedCredentials()方法实现中,方法中获取到盐值salt后继续调用同名的有salt参数的方法。

    protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
        Object salt = null;
        if (info instanceof SaltedAuthenticationInfo) {
            salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
        } else {
            //retain 1.0 backwards compatibility:
            if (isHashSalted()) {
                salt = getSalt(token);
            }
        }
        return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
    }

在这里就可以看到了Shiro是调用SimpleHash时指定散列算法对用户输入的密码进行加密的。而我们保存在数据库的值也要调用SimpleHash并指定相同的散列算法和salt,如MD5,来加密再保存。这样就提高了用户数据的安全性。

    protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
        String hashAlgorithmName = assertHashAlgorithmName();
        return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
    }

参考:
https://www.iteye.com/blog/jinnianshilongnian-2021439
http://www.iocoder.cn/Shiro/xiaoqiyiye/SessionManager/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值