1. 程式人生 > 實用技巧 >springboot整合shiro-對密碼進行MD5並加鹽處理(十五)(轉載)

springboot整合shiro-對密碼進行MD5並加鹽處理(十五)(轉載)

原文地址,轉載請註明出處:https://blog.csdn.net/qq_34021712/article/details/84571067 ©王賽超

資料庫中密碼相關欄位都不是明文,肯定是加密之後的,傳統方式一般是使用MD5加密。

單純使用不加鹽的MD5加密方式,當兩個使用者的密碼相同時,會發現資料庫中存在相同內容的密碼,這樣也是不安全的。我們希望即便是兩個人的原始密碼一樣,加密後的結果也不一樣。

下面進行shiro密碼 加密加鹽配置:

1.ShiroConfig中新增密碼比較器

/**
 * 配置密碼比較器
 * @return
 */
@Bean("credentialsMatcher")
public
RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher(){ RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher(); retryLimitHashedCredentialsMatcher.setRedisManager(redisManager()); //如果密碼加密,可以開啟下面配置 //加密演算法的名稱 retryLimitHashedCredentialsMatcher.setHashAlgorithmName("MD5");
//配置加密的次數 retryLimitHashedCredentialsMatcher.setHashIterations(2); //是否儲存為16進位制 retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return retryLimitHashedCredentialsMatcher; }

2.將密碼比較器配置給ShiroRealm

/**
 *  身份認證realm; (這個需要自己寫,賬號密碼校驗;許可權等)
 * @return
 */
@Bean
public
ShiroRealm shiroRealm(){ ShiroRealm shiroRealm = new ShiroRealm(); shiroRealm.setCachingEnabled(true); //啟用身份驗證快取,即快取AuthenticationInfo資訊,預設false shiroRealm.setAuthenticationCachingEnabled(true); //快取AuthenticationInfo資訊的快取名稱 在ehcache-shiro.xml中有對應快取的配置 shiroRealm.setAuthenticationCacheName("authenticationCache"); //啟用授權快取,即快取AuthorizationInfo資訊,預設false shiroRealm.setAuthorizationCachingEnabled(true); //快取AuthorizationInfo資訊的快取名稱 在ehcache-shiro.xml中有對應快取的配置 shiroRealm.setAuthorizationCacheName("authorizationCache"); //配置自定義密碼比較器 shiroRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher()); return shiroRealm; }

3.密碼比較器RetryLimitHashedCredentialsMatcher

自定義的密碼比較器,跟前面部落格中邏輯沒有變化,唯一變的是 繼承的類從SimpleCredentialsMatcher變為HashedCredentialsMatcher

在密碼比較器中做了: 如果使用者輸入密碼連續錯誤5次,將鎖定賬號,具體參考部落格:https://blog.csdn.net/qq_34021712/article/details/80461177

RetryLimitHashedCredentialsMatcher完整內容如下:

package com.springboot.test.shiro.config.shiro;

import java.util.concurrent.atomic.AtomicInteger;

import com.springboot.test.shiro.modules.user.dao.UserMapper;
import com.springboot.test.shiro.modules.user.dao.entity.User;
import org.apache.log4j.Logger;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.springframework.beans.factory.annotation.Autowired;


/**
 * @author: WangSaiChao
 * @date: 2018/5/25
 * @description: 登陸次數限制
 */
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {

    private static final Logger logger = Logger.getLogger(RetryLimitHashedCredentialsMatcher.class);

    public static final String DEFAULT_RETRYLIMIT_CACHE_KEY_PREFIX = "shiro:cache:retrylimit:";
    private String keyPrefix = DEFAULT_RETRYLIMIT_CACHE_KEY_PREFIX;
    @Autowired
    private UserMapper userMapper;
    private RedisManager redisManager;

    public void setRedisManager(RedisManager redisManager) {
        this.redisManager = redisManager;
    }

    private String getRedisKickoutKey(String username) {
        return this.keyPrefix + username;
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {

        //獲取使用者名稱
        String username = (String)token.getPrincipal();
        //獲取使用者登入次數
        AtomicInteger retryCount = (AtomicInteger)redisManager.get(getRedisKickoutKey(username));
        if (retryCount == null) {
            //如果使用者沒有登陸過,登陸次數加1 並放入快取
            retryCount = new AtomicInteger(0);
        }
        if (retryCount.incrementAndGet() > 5) {
            //如果使用者登陸失敗次數大於5次 丟擲鎖定使用者異常  並修改資料庫欄位
            User user = userMapper.findByUserName(username);
            if (user != null && "0".equals(user.getState())){
                //資料庫欄位 預設為 0  就是正常狀態 所以 要改為1
                //修改資料庫的狀態欄位為鎖定
                user.setState("1");
                userMapper.update(user);
            }
            logger.info("鎖定使用者" + user.getUsername());
            //丟擲使用者鎖定異常
            throw new LockedAccountException();
        }
        //判斷使用者賬號和密碼是否正確
        boolean matches = super.doCredentialsMatch(token, info);
        if (matches) {
            //如果正確,從快取中將使用者登入計數 清除
            redisManager.del(getRedisKickoutKey(username));
        }{
            redisManager.set(getRedisKickoutKey(username), retryCount);
        }
        return matches;
    }

    /**
     * 根據使用者名稱 解鎖使用者
     * @param username
     * @return
     */
    public void unlockAccount(String username){
        User user = userMapper.findByUserName(username);
        if (user != null){
            //修改資料庫的狀態欄位為鎖定
            user.setState("0");
            userMapper.update(user);
            redisManager.del(getRedisKickoutKey(username));
        }
    }

}

4.修改ShiroRealm中doGetAuthenticationInfo方法

package com.springboot.test.shiro.config.shiro;

import com.springboot.test.shiro.modules.user.dao.PermissionMapper;
import com.springboot.test.shiro.modules.user.dao.RoleMapper;
import com.springboot.test.shiro.modules.user.dao.entity.Permission;
import com.springboot.test.shiro.modules.user.dao.entity.Role;
import com.springboot.test.shiro.modules.user.dao.UserMapper;
import com.springboot.test.shiro.modules.user.dao.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Set;

/**
 * @author: wangsaichao
 * @date: 2018/5/10
 * @description: 在Shiro中,最終是通過Realm來獲取應用程式中的使用者、角色及許可權資訊的
 * 在Realm中會直接從我們的資料來源中獲取Shiro需要的驗證資訊。可以說,Realm是專用於安全框架的DAO.
 */
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private PermissionMapper permissionMapper;

    /**
     * 驗證使用者身份
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        //獲取使用者名稱密碼 第一種方式
        //String username = (String) authenticationToken.getPrincipal();
        //String password = new String((char[]) authenticationToken.getCredentials());

        //獲取使用者名稱 密碼 第二種方式
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        String username = usernamePasswordToken.getUsername();
        String password = new String(usernamePasswordToken.getPassword());

        //從資料庫查詢使用者資訊
        User user = this.userMapper.findByUserName(username);

        //可以在這裡直接對使用者名稱校驗,或者呼叫 CredentialsMatcher 校驗
        if (user == null) {
            throw new UnknownAccountException("使用者名稱或密碼錯誤!");
        }
        //這裡將 密碼對比 登出掉,否則 無法鎖定  要將密碼對比 交給 密碼比較器
        //if (!password.equals(user.getPassword())) {
        //    throw new IncorrectCredentialsException("使用者名稱或密碼錯誤!");
        //}
        if ("1".equals(user.getState())) {
            throw new LockedAccountException("賬號已被鎖定,請聯絡管理員!");
        }

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(),new MyByteSource(user.getUsername()),getName());
        return info;
    }

    /**
     * 授權使用者許可權
     * 授權的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>標籤的時候呼叫的
     * 它會去檢測shiro框架中的許可權(這裡的permissions)是否包含有該標籤的name值,如果有,裡面的內容顯示
     * 如果沒有,裡面的內容不予顯示(這就完成了對於許可權的認證.)
     *
     * shiro的許可權授權是通過繼承AuthorizingRealm抽象類,過載doGetAuthorizationInfo();
     * 當訪問到頁面的時候,連結配置了相應的許可權或者shiro標籤才會執行此方法否則不會執行
     * 所以如果只是簡單的身份認證沒有許可權的控制的話,那麼這個方法可以不進行實現,直接返回null即可。
     *
     * 在這個方法中主要是使用類:SimpleAuthorizationInfo 進行角色的新增和許可權的新增。
     * authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission());
     *
     * 當然也可以新增set集合:roles是從資料庫查詢的當前使用者的角色,stringPermissions是從資料庫查詢的當前使用者對應的許可權
     * authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions);
     *
     * 就是說如果在shiro配置檔案中添加了filterChainDefinitionMap.put("/add", "perms[許可權新增]");
     * 就說明訪問/add這個連結必須要有“許可權新增”這個許可權才可以訪問
     *
     * 如果在shiro配置檔案中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[許可權新增]");
     * 就說明訪問/add這個連結必須要有 "許可權新增" 這個許可權和具有 "100002" 這個角色才可以訪問
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        System.out.println("查詢許可權方法呼叫了!!!");

        //獲取使用者
        User user = (User) SecurityUtils.getSubject().getPrincipal();

        //獲取使用者角色
        Set<Role> roles =this.roleMapper.findRolesByUserId(user.getUid());
        //新增角色
        SimpleAuthorizationInfo authorizationInfo =  new SimpleAuthorizationInfo();
        for (Role role : roles) {
            authorizationInfo.addRole(role.getRole());
        }

        //獲取使用者許可權
        Set<Permission> permissions = this.permissionMapper.findPermissionsByRoleId(roles);
        //新增許可權
        for (Permission permission:permissions) {
            authorizationInfo.addStringPermission(permission.getPermission());
        }

        return authorizationInfo;
    }

    /**
     * 重寫方法,清除當前使用者的的 授權快取
     * @param principals
     */
    @Override
    public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }

    /**
     * 重寫方法,清除當前使用者的 認證快取
     * @param principals
     */
    @Override
    public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }

    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

    /**
     * 自定義方法:清除所有 授權快取
     */
    public void clearAllCachedAuthorizationInfo() {
        getAuthorizationCache().clear();
    }

    /**
     * 自定義方法:清除所有 認證快取
     */
    public void clearAllCachedAuthenticationInfo() {
        getAuthenticationCache().clear();
    }

    /**
     * 自定義方法:清除所有的  認證快取  和 授權快取
     */
    public void clearAllCache() {
        clearAllCachedAuthenticationInfo();
        clearAllCachedAuthorizationInfo();
    }

}

跟之前的ShiroRealm相比,唯一改變的了
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(),new MyByteSource(user.getUsername()),getName());這一行程式碼,添加了 加鹽引數。

注意:大家可能看到了使用了MyByteSource而不是ByteSource.Util.bytes(user.getUsername())

具體原因參考部落格:https://blog.csdn.net/qq_34021712/article/details/84567437

5.下面是生成密碼加密加鹽的方法,可以在註冊的時候對明文進行加密 加鹽 入庫

package com.springboot.test.shiro;

import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
import org.junit.Test;

/**
 * @author: WangSaiChao
 * @date: 2018/11/27
 * @description: 給 密碼進行 加密加鹽  鹽值預設為 使用者名稱
 */
public class PasswordSaltTest {

    @Test
    public void test() throws Exception {
        System.out.println(md5("123456","admin"));
    }

    public static final String md5(String password, String salt){
        //加密方式
        String hashAlgorithmName = "MD5";
        //鹽:為了即使相同的密碼不同的鹽加密後的結果也不同
        ByteSource byteSalt = ByteSource.Util.bytes(salt);
        //密碼
        Object source = password;
        //加密次數
        int hashIterations = 2;
        SimpleHash result = new SimpleHash(hashAlgorithmName, source, byteSalt, hashIterations);
        return result.toString();
    }

}

可能出現的問題

可能會發生這種情況,測試發現密碼不對,具體原因debug都可以發現,這裡直接把結果發出來:

第一種:

debug發現 傳入的密碼 經過加密加鹽之後是對的,但是 從資料庫中 獲取的密碼 卻是明文,原因是在ShiroRealmdoGetAuthenticationInfo方法中,最後返回的SimpleAuthenticationInfo第二個引數 是密碼,這個密碼 不是從前臺傳過來的密碼,而是從資料庫中查詢出來的

第二種:

debug發現 傳入的密碼 經過加密加鹽之後是對的,但是 從資料庫中 獲取的密碼 卻是更長的一段密文,原因是在ShiroConfig中配置的RetryLimitHashedCredentialsMatcher一個屬性:

//是否儲存為16進位制
retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);

預設是true,如果改為false,則會出現 對比的時候從資料庫拿出密碼,然後轉base64變成了另外一個更長的字串,所以怎麼對比都是不通過的。