SpringBoot 整合 Shiro 密碼加密
數(shù)據(jù)庫(kù)中密碼相關(guān)字段都不是明文,肯定是加密之后的,傳統(tǒng)方式一般是使用MD5加密。
單純使用不加鹽的MD5加密方式,當(dāng)兩個(gè)用戶的密碼相同時(shí),會(huì)發(fā)現(xiàn)數(shù)據(jù)庫(kù)中存在相同內(nèi)容的密碼,這樣也是不安全的。我們希望即便是兩個(gè)人的原始密碼一樣,加密后的結(jié)果也不一樣。
下面進(jìn)行shiro密碼 加密加鹽配置:
1.ShiroConfig中添加密碼比較器
/**
?*?配置密碼比較器
?*?@return
?*/
@Bean("credentialsMatcher")
public?RetryLimitHashedCredentialsMatcher?retryLimitHashedCredentialsMatcher(){
????RetryLimitHashedCredentialsMatcher?retryLimitHashedCredentialsMatcher?=?new?RetryLimitHashedCredentialsMatcher();
????retryLimitHashedCredentialsMatcher.setRedisManager(redisManager());
????//如果密碼加密,可以打開下面配置
????//加密算法的名稱
????retryLimitHashedCredentialsMatcher.setHashAlgorithmName("MD5");
????//配置加密的次數(shù)
????retryLimitHashedCredentialsMatcher.setHashIterations(2);
????//是否存儲(chǔ)為16進(jìn)制
????retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
????return?retryLimitHashedCredentialsMatcher;
}
2.將密碼比較器配置給ShiroRealm
/**
?*??身份認(rèn)證realm;?(這個(gè)需要自己寫,賬號(hào)密碼校驗(yàn);權(quán)限等)
?*?@return
?*/
@Bean
public?ShiroRealm?shiroRealm(){
????ShiroRealm?shiroRealm?=?new?ShiroRealm();
????shiroRealm.setCachingEnabled(true);
????//啟用身份驗(yàn)證緩存,即緩存AuthenticationInfo信息,默認(rèn)false
????shiroRealm.setAuthenticationCachingEnabled(true);
????//緩存AuthenticationInfo信息的緩存名稱?在ehcache-shiro.xml中有對(duì)應(yīng)緩存的配置
????shiroRealm.setAuthenticationCacheName("authenticationCache");
????//啟用授權(quán)緩存,即緩存AuthorizationInfo信息,默認(rèn)false
????shiroRealm.setAuthorizationCachingEnabled(true);
????//緩存AuthorizationInfo信息的緩存名稱??在ehcache-shiro.xml中有對(duì)應(yīng)緩存的配置
????shiroRealm.setAuthorizationCacheName("authorizationCache");
????//配置自定義密碼比較器
????shiroRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher());
????return?shiroRealm;
}
3.密碼比較器RetryLimitHashedCredentialsMatcher
自定義的密碼比較器,跟前面博客中邏輯沒(méi)有變化,唯一變的是 繼承的類從?SimpleCredentialsMatcher?變?yōu)?HashedCredentialsMatcher
在密碼比較器中做了:如果用戶輸入密碼連續(xù)錯(cuò)誤5次,將鎖定賬號(hào),具體參考博客:https://blog.csdn.net/qq_34021712/article/details/80461177
RetryLimitHashedCredentialsMatcher完整內(nèi)容如下:
package?com.shiro.config;
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;
/**
?*?@description:?登陸次數(shù)限制
?*/
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();
????????//獲取用戶登錄次數(shù)
????????AtomicInteger?retryCount?=?(AtomicInteger)redisManager.get(getRedisKickoutKey(username));
????????if?(retryCount?==?null)?{
????????????//如果用戶沒(méi)有登陸過(guò),登陸次數(shù)加1?并放入緩存
????????????retryCount?=?new?AtomicInteger(0);
????????}
????????if?(retryCount.incrementAndGet()?>?5)?{
????????????//如果用戶登陸失敗次數(shù)大于5次?拋出鎖定用戶異常??并修改數(shù)據(jù)庫(kù)字段
????????????User?user?=?userMapper.findByUserName(username);
????????????if?(user?!=?null?&&?"0".equals(user.getState())){
????????????????//數(shù)據(jù)庫(kù)字段?默認(rèn)為?0??就是正常狀態(tài)?所以?要改為1
????????????????//修改數(shù)據(jù)庫(kù)的狀態(tài)字段為鎖定
????????????????user.setState("1");
????????????????userMapper.update(user);
????????????}
????????????logger.info("鎖定用戶"?+?user.getUsername());
????????????//拋出用戶鎖定異常
????????????throw?new?LockedAccountException();
????????}
????????//判斷用戶賬號(hào)和密碼是否正確
????????boolean?matches?=?super.doCredentialsMatch(token,?info);
????????if?(matches)?{
????????????//如果正確,從緩存中將用戶登錄計(jì)數(shù)?清除
????????????redisManager.del(getRedisKickoutKey(username));
????????}{
????????????redisManager.set(getRedisKickoutKey(username),?retryCount);
????????}
????????return?matches;
????}
????/**
?????*?根據(jù)用戶名?解鎖用戶
?????*?@param?username
?????*?@return
?????*/
????public?void?unlockAccount(String?username){
????????User?user?=?userMapper.findByUserName(username);
????????if?(user?!=?null){
????????????//修改數(shù)據(jù)庫(kù)的狀態(tài)字段為鎖定
????????????user.setState("0");
????????????userMapper.update(user);
????????????redisManager.del(getRedisKickoutKey(username));
????????}
????}
}
4.修改ShiroRealm中doGetAuthenticationInfo方法
package?com.springboot.shiro.realm;
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;
/**
?*?@description:?在Shiro中,最終是通過(guò)Realm來(lái)獲取應(yīng)用程序中的用戶、角色及權(quán)限信息的
?*?在Realm中會(huì)直接從我們的數(shù)據(jù)源中獲取Shiro需要的驗(yàn)證信息。可以說(shuō),Realm是專用于安全框架的DAO.
?*/
public?class?ShiroRealm?extends?AuthorizingRealm?{
????@Autowired
????private?UserMapper?userMapper;
????@Autowired
????private?RoleMapper?roleMapper;
????@Autowired
????private?PermissionMapper?permissionMapper;
????/**
?????*?驗(yàn)證用戶身份
?????*?@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());
????????//從數(shù)據(jù)庫(kù)查詢用戶信息
????????User?user?=?this.userMapper.findByUserName(username);
????????//可以在這里直接對(duì)用戶名校驗(yàn),或者調(diào)用?CredentialsMatcher?校驗(yàn)
????????if?(user?==?null)?{
????????????throw?new?UnknownAccountException("用戶名或密碼錯(cuò)誤!");
????????}
????????//這里將?密碼對(duì)比?注銷掉,否則?無(wú)法鎖定??要將密碼對(duì)比?交給?密碼比較器
????????//if?(!password.equals(user.getPassword()))?{
????????//????throw?new?IncorrectCredentialsException("用戶名或密碼錯(cuò)誤!");
????????//}
????????if?("1".equals(user.getState()))?{
????????????throw?new?LockedAccountException("賬號(hào)已被鎖定,請(qǐng)聯(lián)系管理員!");
????????}
????????SimpleAuthenticationInfo?info?=?new?SimpleAuthenticationInfo(user,?user.getPassword(),new?MyByteSource(user.getUsername()),getName());
????????return?info;
????}
????/**
?????*?授權(quán)用戶權(quán)限
?????*?授權(quán)的方法是在碰到 標(biāo)簽的時(shí)候調(diào)用的
?????*?它會(huì)去檢測(cè)shiro框架中的權(quán)限(這里的permissions)是否包含有該標(biāo)簽的name值,如果有,里面的內(nèi)容顯示
?????*?如果沒(méi)有,里面的內(nèi)容不予顯示(這就完成了對(duì)于權(quán)限的認(rèn)證.)
?????*
?????*?shiro的權(quán)限授權(quán)是通過(guò)繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo();
?????*?當(dāng)訪問(wèn)到頁(yè)面的時(shí)候,鏈接配置了相應(yīng)的權(quán)限或者shiro標(biāo)簽才會(huì)執(zhí)行此方法否則不會(huì)執(zhí)行
?????*?所以如果只是簡(jiǎn)單的身份認(rèn)證沒(méi)有權(quán)限的控制的話,那么這個(gè)方法可以不進(jìn)行實(shí)現(xiàn),直接返回null即可。
?????*
?????*?在這個(gè)方法中主要是使用類:SimpleAuthorizationInfo?進(jìn)行角色的添加和權(quán)限的添加。
?????*?authorizationInfo.addRole(role.getRole());?authorizationInfo.addStringPermission(p.getPermission());
?????*
?????*?當(dāng)然也可以添加set集合:roles是從數(shù)據(jù)庫(kù)查詢的當(dāng)前用戶的角色,stringPermissions是從數(shù)據(jù)庫(kù)查詢的當(dāng)前用戶對(duì)應(yīng)的權(quán)限
?????*?authorizationInfo.setRoles(roles);?authorizationInfo.setStringPermissions(stringPermissions);
?????*
?????*?就是說(shuō)如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add",?"perms[權(quán)限添加]");
?????*?就說(shuō)明訪問(wèn)/add這個(gè)鏈接必須要有“權(quán)限添加”這個(gè)權(quán)限才可以訪問(wèn)
?????*
?????*?如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add",?"roles[100002],perms[權(quán)限添加]");
?????*?就說(shuō)明訪問(wèn)/add這個(gè)鏈接必須要有?"權(quán)限添加"?這個(gè)權(quán)限和具有?"100002"?這個(gè)角色才可以訪問(wèn)
?????*?@param?principalCollection
?????*?@return
?????*/
????@Override
????protected?AuthorizationInfo?doGetAuthorizationInfo(PrincipalCollection?principalCollection)?{
????????System.out.println("查詢權(quán)限方法調(diào)用了!!!");
????????//獲取用戶
????????User?user?=?(User)?SecurityUtils.getSubject().getPrincipal();
????????//獲取用戶角色
????????Set?roles?=this.roleMapper.findRolesByUserId(user.getUid());
????????//添加角色
????????SimpleAuthorizationInfo?authorizationInfo?=??new?SimpleAuthorizationInfo();
????????for?(Role?role?:?roles)?{
????????????authorizationInfo.addRole(role.getRole());
????????}
????????//獲取用戶權(quán)限
????????Set?permissions?=?this.permissionMapper.findPermissionsByRoleId(roles);
????????//添加權(quán)限
????????for?(Permission?permission:permissions)?{
????????????authorizationInfo.addStringPermission(permission.getPermission());
????????}
????????return?authorizationInfo;
????}
????/**
?????*?重寫方法,清除當(dāng)前用戶的的?授權(quán)緩存
?????*?@param?principals
?????*/
????@Override
????public?void?clearCachedAuthorizationInfo(PrincipalCollection?principals)?{
????????super.clearCachedAuthorizationInfo(principals);
????}
????/**
?????*?重寫方法,清除當(dāng)前用戶的?認(rèn)證緩存
?????*?@param?principals
?????*/
????@Override
????public?void?clearCachedAuthenticationInfo(PrincipalCollection?principals)?{
????????super.clearCachedAuthenticationInfo(principals);
????}
????@Override
????public?void?clearCache(PrincipalCollection?principals)?{
????????super.clearCache(principals);
????}
????/**
?????*?自定義方法:清除所有?授權(quán)緩存
?????*/
????public?void?clearAllCachedAuthorizationInfo()?{
????????getAuthorizationCache().clear();
????}
????/**
?????*?自定義方法:清除所有?認(rèn)證緩存
?????*/
????public?void?clearAllCachedAuthenticationInfo()?{
????????getAuthenticationCache().clear();
????}
????/**
?????*?自定義方法:清除所有的??認(rèn)證緩存??和?授權(quán)緩存
?????*/
????public?void?clearAllCache()?{
????????clearAllCachedAuthenticationInfo();
????????clearAllCachedAuthorizationInfo();
????}
}
跟之前的?ShiroRealm?相比,唯一改變的了SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(),new MyByteSource(user.getUsername()),getName());這一行代碼,添加了 加鹽參數(shù)。
注意:大家可能看到了使用了?MyByteSource?而不是?ByteSource.Util.bytes(user.getUsername())具體原因參考博客:https://blog.csdn.net/qq_34021712/article/details/84567437
5.下面是生成密碼加密加鹽的方法,可以在注冊(cè)的時(shí)候?qū)γ魑倪M(jìn)行加密 加鹽 入庫(kù)
package?com.olive.shiro.test;
import?org.apache.shiro.crypto.hash.SimpleHash;
import?org.apache.shiro.util.ByteSource;
import?org.junit.Test;
/**
?*?@description:?給?密碼進(jìn)行?加密加鹽??鹽值默認(rèn)為?用戶名
?*/
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";
????????//鹽:為了即使相同的密碼不同的鹽加密后的結(jié)果也不同
????????ByteSource?byteSalt?=?ByteSource.Util.bytes(salt);
????????//密碼
????????Object?source?=?password;
????????//加密次數(shù)
????????int?hashIterations?=?2;
????????SimpleHash?result?=?new?SimpleHash(hashAlgorithmName,?source,?byteSalt,?hashIterations);
????????return?result.toString();
????}
}
可能出現(xiàn)的問(wèn)題
可能會(huì)發(fā)生這種情況,測(cè)試發(fā)現(xiàn)密碼不對(duì),具體原因debug都可以發(fā)現(xiàn),這里直接把結(jié)果發(fā)出來(lái):
第一種:
debug發(fā)現(xiàn) 傳入的密碼 經(jīng)過(guò)加密加鹽之后是對(duì)的,但是 從數(shù)據(jù)庫(kù)中 獲取的密碼 卻是明文,原因是在ShiroRealm中?doGetAuthenticationInfo方法中,最后返回的SimpleAuthenticationInfo?第二個(gè)參數(shù) 是密碼,這個(gè)密碼 不是從前臺(tái)傳過(guò)來(lái)的密碼,而是從數(shù)據(jù)庫(kù)中查詢出來(lái)的
第二種:
debug發(fā)現(xiàn) 傳入的密碼 經(jīng)過(guò)加密加鹽之后是對(duì)的,但是 從數(shù)據(jù)庫(kù)中 獲取的密碼 卻是更長(zhǎng)的一段密文,原因是在ShiroConfig中配置的RetryLimitHashedCredentialsMatcher一個(gè)屬性:
//是否存儲(chǔ)為16進(jìn)制
retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
默認(rèn)是true,如果改為false,則會(huì)出現(xiàn) 對(duì)比的時(shí)候從數(shù)據(jù)庫(kù)拿出密碼,然后轉(zhuǎn)?base64?變成了另外一個(gè)更長(zhǎng)的字符串,所以怎么對(duì)比都是不通過(guò)的。
source:?//smniuhe.github.io/2018/12/07/SpringBoot整合shiro-密碼加密
分享&在看
