源码分析Apache Shiro 加密与登录验证

版权声明:转载请请标明出处。 https://blog.csdn.net/AnIllusion/article/details/81140612

前言

最近用到Shiro安全框架,做加密验证的时候遇到一些问题,对Shiro内部登录验证流程有些疑惑,网上的多数Shiro的环境搭建只是简单的明文密码匹配,甚至有些文章的注释也不尽正确。在这里记录下通过分析源码的整理。

大纲

  1. 使用Shiro提供的类进行密码加密
  2. 登录验证的流程

使用Shiro提供的类进行密码加密

Shiro提供了org.apache.shiro.crypto.hash.SimpleHash加密类继承了org.apache.shiro.crypto.hash.AbstractHash,先看看它主要的构造方法:

/* 
参数说明:
String algorithmName    :加密算法
Object source   :待加密的对象,字符串等
Object salt     :盐,混入待加密的密码进行加密,加大破解难度
int hashIterations  :加密次数
*/
 public SimpleHash(String algorithmName, Object source)
 public SimpleHash(String algorithmName, Object source, Object salt)
 public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations)

由构造方法可知,SimpleHash可以自主指定加密算法,MD5、SHA-256、SHA-512等等,Shiro在同包下,对SimpleHash进一步封装出Md5Hash、Sha256Hash等方便使用。例如Sha256Hash 继承了SimpleHash ,实际上就是指定了algorithmName为”SHA-256”的SimpleHash。

public class Sha256Hash extends SimpleHash {
    public static final String ALGORITHM_NAME = "SHA-256";

    public Sha256Hash() {
        super("SHA-256");
    }

    public Sha256Hash(Object source) {
        super("SHA-256", source);
    }

    public Sha256Hash(Object source, Object salt) {
        super("SHA-256", source, salt);
    }

    public Sha256Hash(Object source, Object salt, int hashIterations) {
        super("SHA-256", source, salt, hashIterations);
    }

    public static Sha256Hash fromHexString(String hex) {
        Sha256Hash hash = new Sha256Hash();
        hash.setBytes(Hex.decode(hex));
        return hash;
    }

    public static Sha256Hash fromBase64String(String base64) {
        Sha256Hash hash = new Sha256Hash();
        hash.setBytes(Base64.decode(base64));
        return hash;
    }
}

问题来了,现在算法、原密码字符串和加密次数都确定了,如何获取盐呢?
为了确保安全性,盐值不应重复,每次修改密码要产生不同的盐值。首先想到的是使用java.util.Random来获得随机数,然而Random使用 LCG 算法生成随机数,不建议使用在信息安全应用中,应使用java.security.SecureRandom产生不可预知的盐值:

       SecureRandom random = SecureRandom.getInstance("SHA1PRNG");//使用SHA1PRNG算法
        String salt = String.valueOf(random.nextInt());

登录验证的流程

这里写图片描述
过程:登录请求->Controller接受请求->将账号密码组装成UsernamePasswordToken->获取subject,调用subject.login(token)进行登录验证->SecurityManager(相当于SpringMVC中的DispatcherServlet)->Realm->凭证匹配器CredentialsMatcher。

下面从subject.login(token)入手,分析Shiro如何进行验证

1.Controller接收用户填写的账号密码信息,并交给Subject来验证,token储存着用户输入的账号和未经加密的密码

  public Map userLogin(HttpServletRequest request, String username, String password) {
         ...
            UsernamePasswordToken token = new UsernamePasswordToken(username,password);
            Subject subject = SecurityUtils.getSubject();
            subject.login(token);
            ...

2.追踪subject.login()的实现可知,org.apache.shiro.subject.support.DelegatingSubject 是Shiro中唯一直接继承Subject的类,并实现了所有方法,其中login(AuthenticationToken token)方法的实现如下:

    public void login(AuthenticationToken token) throws AuthenticationException {
        this.clearRunAsIdentitiesInternal();
        //交由securityManager验证tonken,并负责创建Subject对象
        Subject subject = this.securityManager.login(this, token);
        String host = null;
        PrincipalCollection principals;
        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject)subject;
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }

        if (principals != null && !principals.isEmpty()) {
            this.principals = principals;
            this.authenticated = true;
            if (token instanceof HostAuthenticationToken) {
                host = ((HostAuthenticationToken)token).getHost();
            }

            if (host != null) {
                this.host = host;
            }

            Session session = subject.getSession(false);
            if (session != null) {
                this.session = this.decorate(session);
            } else {
                this.session = null;
            }

        } else {
            String msg = "Principals returned from securityManager.login( token ) returned a null or empty value.  This value must be non null and populated with one or more elements.";
            throw new IllegalStateException(msg);
        }
    }

3.看看SecurityManager是如何验证tonken,追踪securityManager.login(this, token),可知DefaultSecurityManager继承SecurityManager实现login方法:

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
        //调用authenticate(token)获取info,info存放着来自数据库的账号和经过加密的密码、盐等信息,凭证匹配器就是通过对比info和token来作验证的
        //验证失败会抛出AuthenticationException
            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;
        }
        Subject loggedIn = this.createSubject(token, info, subject);
        this.onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;
    }

4.追踪 上面的 this.authenticate(token)方法的实现可知,认证工作交由认证器authenticator进行:

  public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }

5.authenticator是如何认证的呢?Shiro中org.apache.shiro.authc.AbstractAuthenticator直接继承Authenticator并实现authenticate()方法,关键代码如下:

  public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
            try {
            //由doAuthenticate(token)方法处理
               info = this.doAuthenticate(token);
                if (info == null) {
                ........
                    throw new AuthenticationException(msg);
                }
            } catch (Throwable var8) {
    ........
}
}

6.由上可知doAuthenticate(token)才是真正的处理,authenticate()只是对异常进行一些处理。在AbstractAuthenticator中doAuthenticate()是个抽象方法,它在AbstractAuthenticator的实现类ModularRealmAuthenticator中得到实现:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        this.assertRealmsConfigured();
        Collection<Realm> realms = this.getRealms();
        //调用realm进行验证
        return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
    }

    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken 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 {
            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,都知道Realm需要我们自己来实现,主要是两个方法:AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authctoken)。getAuthenticationInfo()方法内也是调用了doGetAuthenticationInfo()来获取info,并且assertCredentialsMatch()中使用CredentialsMatcher凭证匹配器来做密码验证。至此真相水落石出,真正将realm与CredentialsMatcher密码验证器关联起来的代码在Realm中的assertCredentialsMatch方法。

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
        if (info == null) {
        //从我们自定义的doGetAuthenticationInfo方法中获取info
            info = this.doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                this.cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

        if (info != null) {
       //将token和info交给凭证匹配器来做验证工作
            this.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 cm = this.getCredentialsMatcher();
        if (cm != null) {
            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.");
        }
    }

由上可知,doGetAuthenticationInfo方法只是提供一个地方来让使用者自定义获取info,真正的身份验证并非在这里,而网上很多把这个方法说成是身份验证的地方,其实并不准确,真正的身份验证在凭证匹配器CredentialsMatcher。CredentialsMatcher的配置则是在需要我们自定义的Shiro配置类ShiroConfig里进行配置,下面主要解读CredentialsMatcher的配置:

    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("SHA-256");//散列算法:这里使用SHA-256算法;
        hashedCredentialsMatcher.setHashIterations(3);//散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedCredentialsMatcher;
    }

因为上面使用SimpleHash对密码进行哈希加密,这里配置了哈希凭证匹配器与其对应,值得注意的是,SimpleHash(String algorithmName, Object source, Object salt, int hashIterations)中hashIterations(即哈希的次数)和algorithmName(算法名),应与凭证匹配器保持一致。
7.问题又来了,凭证匹配器里面是怎么样工作的呢?
进入org.apache.shiro.authc.credential.HashedCredentialsMatcher源码,doCredentialsMatch()负责校验凭证

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    /*上面说过,info携带数据库真实的账号、经过加密的密码、盐等信息,
    token携带用户数据的账号、源密码
    ShirConfig配置了加密的算法和次数。
    hashProvidedCredentials()就是源密码进行了加密
    */
        Object tokenHashedCredentials = this.hashProvidedCredentials(token, info);
        //数据库中存放的凭证
        Object accountCredentials = this.getCredentials(info);
        //匹配凭证
        return this.equals(tokenHashedCredentials, accountCredentials);
    }

    protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
        Object salt = null;
        if (info instanceof SaltedAuthenticationInfo) {
            salt = ((SaltedAuthenticationInfo)info).getCredentialsSalt();
        } else if (this.isHashSalted()) {
            salt = this.getSalt(token);
        }

        return this.hashProvidedCredentials(token.getCredentials(), salt, this.getHashIterations());
    }
    protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
        String hashAlgorithmName = this.assertHashAlgorithmName();
        //同样使用SimpleHash方法进行加密
        return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
    }

由源码可知,凭证匹配器同样也是使用SimpleHash方法对用户输入的密码进行加密.。

[END]

猜你喜欢

转载自blog.csdn.net/AnIllusion/article/details/81140612