Shiro+Redis實(shí)現(xiàn)登錄次數(shù)凍結(jié)的示例
概述
假設(shè)我們需要有這樣一個場景:如果用戶連續(xù)輸錯5次密碼,那可能說明有人在搞事情,所以需要暫時凍結(jié)該賬戶的登錄功能
關(guān)于Shiro整合JWT,可以看這里:Springboot實(shí)現(xiàn)Shiro+JWT認(rèn)證
假設(shè)我們的項目中用到了shiro,因為Shiro是建立在完善的接口驅(qū)動設(shè)計和面向?qū)ο笤瓌t之上的,支持各種自定義行為,所以我們可以結(jié)合Shiro框架的認(rèn)證模塊和redis來實(shí)現(xiàn)這個功能。
思路
我們大體的思路如下:
- 用戶登錄
- Shiro去Redis檢查賬戶的登錄錯誤次數(shù)是否超過規(guī)定范圍(超過了就是所謂的凍結(jié))
- Shiro進(jìn)行密碼比對
- 如果登錄失敗,則去Redis里記錄:登錄錯誤次數(shù)+1
- 如果密碼正確,則登錄成功,刪除Redis里的登錄錯誤記錄
前期準(zhǔn)備
除了需要用到Shiro以外,我們也需要用到Redis,這里需要先配置好RedisTemplate,(由于這個不是重點(diǎn),我就把代碼和配置方法貼在文章的最后了),另外,在Controller層,登錄接口的異常處理除了之前的登錄錯誤,還需要新增一個賬戶凍結(jié)類的異常,代碼如下:
@PostMapping(value = "/login") public AccountVO login(String userName, String password){ //嘗試登錄 Subject subject = SecurityUtils.getSubject(); try { //通過shiro提供的安全接口來進(jìn)行認(rèn)證 subject.login(new UsernamePasswordToken(userName, password)); } catch (ExcessiveAttemptsException e1) { //新增一個賬戶鎖定類錯誤 throw new AccountLockedException(); } catch (Exception e) { //其他的錯誤判定 throw new LoginFailed(); } //聚合登錄信息 AccountVO account = accountService.getAccountByUserName(userName); //返回正確登錄的結(jié)果 return account; }
自定義Shiro認(rèn)證管理器
HashedCredentialsMatcher
當(dāng)你在上面的Controller層調(diào)用subject.login方法后,會進(jìn)入到自定義的Realm里去,然后慢慢進(jìn)入到Shiro當(dāng)前的Security Manager里定義的HashedCredentialsMatcher認(rèn)證管理器的doCredentialsMatch方法,進(jìn)行密碼匹配,原版代碼如下:
/** * This implementation first hashes the {@code token}'s credentials, potentially using a * {@code salt} if the {@code info} argument is a * {@link org.apache.shiro.authc.SaltedAuthenticationInfo SaltedAuthenticationInfo}. It then compares the hash * against the {@code AuthenticationInfo}'s * {@link #getCredentials(org.apache.shiro.authc.AuthenticationInfo) already-hashed credentials}. This method * returns {@code true} if those two values are {@link #equals(Object, Object) equal}, {@code false} otherwise. * * @param token the {@code AuthenticationToken} submitted during the authentication attempt. * @param info the {@code AuthenticationInfo} stored in the system matching the token principal * @return {@code true} if the provided token credentials hash match to the stored account credentials hash, * {@code false} otherwise * @since 1.1 */ @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenHashedCredentials = hashProvidedCredentials(token, info); Object accountCredentials = getCredentials(info); return equals(tokenHashedCredentials, accountCredentials); }
可以發(fā)現(xiàn),原版的邏輯很簡單,就做了兩件事,獲取密碼,比對密碼。
由于我們需要聯(lián)動Redis,在每次登錄前都做一次凍結(jié)檢查,每次遇到登錄失敗之后還需要實(shí)現(xiàn)對redis的寫操作,所以現(xiàn)在需要重寫一個認(rèn)證管理器去配置到Security Manager里。
CustomMatcher
我們自定義一個CustomMatcher,這個類繼承了HashedCredentialsMatcher,唯獨(dú)重寫了doCredentialsMatch方法,在這里面加入了我們自己的邏輯,代碼如下:
import com.imlehr.internship.redis.RedisStringService; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.springframework.beans.factory.annotation.Autowired; /** * @author Lehr * @create: 2020-02-25 */ public class CustomMatcher extends HashedCredentialsMatcher { //這個是redis里的key的統(tǒng)一前綴 private static final String PREFIX = "USER_LOGIN_FAIL:"; @Autowired RedisStringService redisUtils; @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //檢查本賬號是否被凍結(jié) //先獲取用戶的登錄名字 UsernamePasswordToken myToken = (UsernamePasswordToken) token; String userName = myToken.getUsername(); //初始化錯誤登錄次數(shù) Integer errorNum = 0; //從數(shù)據(jù)庫里獲取錯誤次數(shù) String errorTimes = (String)redisUtils.get(PREFIX+userName); if(errorTimes!=null && errorTimes.trim().length()>0) { //如果得到的字符串不為空不為空 errorNum = Integer.parseInt(errorTimes); } //如果用戶錯誤登錄次數(shù)超過十次 if (errorNum >= 10) { //拋出賬號鎖定異常類 throw new ExcessiveAttemptsException(); } //先按照父類的規(guī)則來比對密碼 boolean matched = super.doCredentialsMatch(token, info); if(matched) { //清空錯誤次數(shù) redisUtils.remove(PREFIX+userName); } else{ //添加一次錯誤次數(shù) 秒為單位 redisUtils.set(PREFIX+userName,String.valueOf(++errorNum),60*30L); } return matched; } }
首先,我們從AuthenticationToken里面拿到之前存入的用戶的登錄信息,這個對象其實(shí)就是你在Controller層
subject.login(new UsernamePasswordToken(userName, password));
這一步里面你實(shí)例化的對象
然后,通過用戶的登錄名加上固定前綴(為了防止防止userName和其他主鍵沖突)去Redis里獲取到錯誤次數(shù)。判斷賬戶是否被凍結(jié)的邏輯其實(shí)就是看當(dāng)前用戶的錯誤登錄次數(shù)是否超過某個規(guī)定值,這里我們定為5次。
接下來,說明用戶沒有被凍結(jié),可以執(zhí)行登錄操作,所以我們就直接調(diào)用父類的驗證方法來進(jìn)行密碼比對(就是之前提到的那三行代碼),得到密碼的比對結(jié)果
如果比對一致,那么就成功登錄,返回true即可,也可以選擇一旦登錄成功,就消除所有錯誤次數(shù)記錄,上面的代碼就是這樣做的。
如果對比結(jié)果不一樣,那就再添加一次錯誤記錄,然后返回false
測試
第一次登錄:頁面結(jié)果:
Redis中:
然后連續(xù)錯誤10次:
頁面結(jié)果:
Redis中:
然后等待了半小時之后(其實(shí)我調(diào)成了5分鐘)
再次嘗試錯誤密碼登錄:
再次報錯,此時Redis里由于之前的記錄到期了,自動銷毀了,所以再次觸發(fā)錯誤又會添加一次錯誤記錄
現(xiàn)在嘗試一次正確登錄:
成功登錄
查看Redis:
🎉Done!
附RedisTemplate代碼
配置類
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { //我就用的默認(rèn)的序列化處理器 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); JdkSerializationRedisSerializer ser = new JdkSerializationRedisSerializer(); RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(stringRedisSerializer); template.setValueSerializer(ser); return template; } @Bean public RedisStringService myStringRedisTemplate() { return new RedisStringService(); } }
工具類RedisStringService
一個只能用來處理Value是String的工具類,就是我在CustomMatcher里Autowired的這個類
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; public class RedisStringService { @Autowired protected StringRedisTemplate redisTemplate; /** * 寫入redis緩存(不設(shè)置expire存活時間) * @param key * @param value * @return */ public boolean set(final String key, String value){ boolean result = false; try { ValueOperations operations = redisTemplate.opsForValue(); operations.set(key, value); result = true; } catch (Exception e) { e.getMessage(); } return result; } /** * 寫入redis緩存(設(shè)置expire存活時間) * @param key * @param value * @param expire * @return */ public boolean set(final String key, String value, Long expire){ boolean result = false; try { ValueOperations operations = redisTemplate.opsForValue(); operations.set(key, value); redisTemplate.expire(key, expire, TimeUnit.SECONDS); result = true; } catch (Exception e) { e.getMessage(); } return result; } /** * 讀取redis緩存 * @param key * @return */ public Object get(final String key){ Object result = null; try { ValueOperations operations = redisTemplate.opsForValue(); result = operations.get(key); } catch (Exception e) { e.getMessage(); } return result; } /** * 判斷redis緩存中是否有對應(yīng)的key * @param key * @return */ public boolean exists(final String key){ boolean result = false; try { result = redisTemplate.hasKey(key); } catch (Exception e) { e.getMessage(); } return result; } /** * redis根據(jù)key刪除對應(yīng)的value * @param key * @return */ public boolean remove(final String key){ boolean result = false; try { if(exists(key)){ redisTemplate.delete(key); } result = true; } catch (Exception e) { e.getMessage(); } return result; } /** * redis根據(jù)keys批量刪除對應(yīng)的value * @param keys * @return */ public void remove(final String... keys){ for(String key : keys){ remove(key); } } }
到此這篇關(guān)于Shiro+Redis實(shí)現(xiàn)登錄次數(shù)凍結(jié)的文章就介紹到這了,更多相關(guān)Shiro+Redis登錄凍結(jié)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Shiro中session超時頁面跳轉(zhuǎn)的處理方式
- spring boot整合redis實(shí)現(xiàn)shiro的分布式session共享的方法
- Spring Boot集成Shiro并利用MongoDB做Session存儲的方法詳解
- spring boot實(shí)戰(zhàn)教程之shiro session過期時間詳解
- springboot整合shiro登錄失敗次數(shù)限制功能的實(shí)現(xiàn)代碼
- SpringBoot+Shiro學(xué)習(xí)之密碼加密和登錄失敗次數(shù)限制示例
- Shiro實(shí)現(xiàn)session限制登錄數(shù)量踢人下線功能
相關(guān)文章
Netty分布式pipeline管道Handler的刪除邏輯操作
這篇文章主要為大家介紹了Netty分布式pipeline管道Handler的刪除邏輯操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03JavaWeb使用Session和Cookie實(shí)現(xiàn)登錄認(rèn)證
本篇文章主要介紹了JavaWeb使用Session和Cookie實(shí)現(xiàn)登錄認(rèn)證,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03Java 如何快速,優(yōu)雅的實(shí)現(xiàn)導(dǎo)出Excel
這篇文章主要介紹了Java 如何快速,優(yōu)雅的實(shí)現(xiàn)導(dǎo)出Excel,幫助大家更好的理解和學(xué)習(xí)使用Java,感興趣的朋友可以了解下2021-03-03基于Java并發(fā)容器ConcurrentHashMap#put方法解析
下面小編就為大家?guī)硪黄贘ava并發(fā)容器ConcurrentHashMap#put方法解析。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06