1. 程式人生 > >原始碼分析Apache Shiro 加密與登入驗證

原始碼分析Apache Shiro 加密與登入驗證

前言

最近用到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]