Apach Shiro MD5密碼加密過程(明文生成密碼過程)詳細解析


前言:

最近再項目當中使用的ApachShiro安全框架,對於權限和服務器資源的保護都有一個很好的管理。前期主要參考的文章有

 

項目中設計密碼的加鹽處理以及二次加密問題,跟着斷點 一步步揭開Apach Shiro 的神秘面紗

 

數據庫:

這里我們就用最簡單的admin + 123456(加密前的密碼) 來做測試

 

ShiroConfig 配置

    /**
     * 憑證匹配器  告訴
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:這里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);// 散列的次數,比如散列兩次,相當於 md5(md5(""));
        return hashedCredentialsMatcher;
    }

這里我摘取了一段shiro 配置類當中的一個Bean注入對象,這里是告訴ApachShiro 我用的加密方式是MD5 散列次數是兩次,后面你把傳上來的用戶名和密碼交給Shiro校驗的時候,它會按照你傳入的憑證匹配器去校驗用戶名和密碼是否正確

 

繼承 AuthorizingRealm 重寫 doGetAuthenticationInfo(校驗)

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
        // 獲取用戶的輸入的賬號.
        String username = (String) token.getPrincipal();
        System.out.println(token.getCredentials());
        // 通過username從數據庫中查找 User對象,如果找到,沒找到.
        // 實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鍾內不會重復執行該方法
        UserInfo userInfo = userInfoService.findByUsername(username);
        System.out.println("----->>userInfo=" + userInfo);
        if (userInfo == null) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, // 用戶名
                userInfo.getPassword(), // 密碼
                ByteSource.Util.bytes(userInfo.getCredentialsSalt()), // salt=username+salt
                getName() // realm name
        );
        return authenticationInfo;

    }

 

這個方法最后返回一個 AuthenticationInfo (身份驗證信息)需要傳入這樣幾個參數,principal(當前需要校驗的用戶)hashedCredentials (已經加密后密碼)  credentialsSalt(密碼加鹽)realmName(特定安全的 DAO名稱

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

構建校驗對象

 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, // 用戶名
                userInfo.getPassword(), // 密碼
                ByteSource.Util.bytes(userInfo.getCredentialsSalt()), // salt=username+salt
                getName() // realm name
        );

我們在這里把從數據庫查出來的userInfo對象傳入,全局作為唯一

第二個參數依然是傳入用戶被MD5加鹽加密后的密碼    d3c59d25033dbf980d29554025c23a75

第三個參數傳入鹽值 (這里的鹽值是用戶名+鹽值)再用 ByteSource進行一次編碼    YWRtaW44ZDc4ODY5ZjQ3MDk1MTMzMjk1OTU4MDQyNGQ0YmY0Zg==

最后一個參數當然就是本次Realm的名字

斷點跟進,進入一個getAuthenticationInfo核心方法 

 public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
        if (info == null) {
       //從自定義的Realm中拿出一個包裝好的對象 ,和token傳過來的進行一個比較 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) { this.assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }

注意:

1.方法傳入的AuthenticationToken  token 保存了當前傳入過來的用戶名 密碼 

 

繼續用斷點進行跟進 進入這樣一個方法  cacheAuthenticationInfoIfPossible()

進行可能的緩存身份驗證 方法;
    private void cacheAuthenticationInfoIfPossible(AuthenticationToken token, AuthenticationInfo info) {
      
     //
isAuthenticationCachingEnabled 首選檢查是否啟用緩存 我這里沒有啟用 返回的是false 直接跳出這個方法
if (!this.isAuthenticationCachingEnabled(token, info)) {
            log.debug("AuthenticationInfo caching is disabled for info [{}].  Submitted token: [{}].", info, token);
        } else {
            Cache<Object, AuthenticationInfo> cache = this.getAvailableAuthenticationCache();
            if (cache != null) {
                Object key = this.getAuthenticationCacheKey(token);
                cache.put(key, info);
                log.trace("Cached AuthenticationInfo for continued authentication.  key=[{}], value=[{}].", key, info);
            }

        }
    }

 

跳出來回到上面的 getAuthenticationInfo 核心方法 ,這里才是身份驗證的主要位置

        if (info != null) {
        //憑證匹配器,提交的憑證和存儲的憑證進行匹配比較 
this.assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); }

進入這個 assertCredentialsMatch(token,info)

    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
     //取出我們配置的憑證管理器 這里當然是MD5 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."); } }

引用:

返回設定的憑證匹配器(匹配規則),包含了hashAlgorithmName(加密方式名如md5)、hashIterations(加密次數)、

storedCredentialsHexEncoded(密文進行16進制存儲)、hashSalted(默認值false)、passwordRetryCache(密碼重試緩存)5個屬性。  

 

憑證管理器 CredentialsMatcher 

public interface CredentialsMatcher {
    boolean doCredentialsMatch(AuthenticationToken var1, AuthenticationInfo var2);
}

憑證管理器定義為一個接口,我們在注入憑證管理器的時候,我們選擇的是 Hash加密方法

HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

 

所以跳到 HashedCredentialsMatcher 類下面的校驗方法

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
     //把前台傳過來的token明文密碼進行加密 Object tokenHashedCredentials
= this.hashProvidedCredentials(token, info);
      //數據庫查出來的已經加密過的密碼 Object accountCredentials
= this.getCredentials(info);
      //進行比較
return this.equals(tokenHashedCredentials, accountCredentials); }

繼續斷點跟進,這里是要給取出鹽值的操作 判斷info的類型是否是 SaltedAuthenticationInfo 

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

拿到鹽值后 傳給hashProvidedCredentials方法 傳入明文密碼 鹽值 以及 加密散列的次數

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

最后使用 SimpleHash構造器構造出加密后的對象

 

 

兩個 SimpleHash 進行比較的方法

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

 

得知這些以后 ,我們自然而然的知道了密碼加密的過程 ,我們在創建用戶的時候 按照同樣的方式,對密碼進行一個加密 ,這樣解密一點毛病也沒有了

密碼生成過程

    @Test
    public void contextLoads() {


        String password  = "123456";

        String salt = "admin8d78869f470951332959580424d4bf4f";


        int hashIterations = 2;

        SimpleHash simpleHash = new SimpleHash("md5",password, ByteSource.Util.bytes(salt),hashIterations);

        System.out.println(simpleHash.toHex());
    // 結果 : d3c59d25033dbf980d29554025c23a75 }

 

 

 比較數據庫的密碼  發現一致

 

參考博文:

https://www.iteye.com/blog/sunjy22-2398775

Apach Shiro 中文文檔

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM