SpringBoot+Shiro學(xué)習(xí)之密碼加密和登錄失敗次數(shù)限制示例
這個(gè)項(xiàng)目寫(xiě)到現(xiàn)在,基本的雛形出來(lái)了,在此感謝一直關(guān)注的童鞋,送你們一句最近剛學(xué)習(xí)的一句雞湯:念念不忘,必有回響。再貼一張ui圖片:
前篇思考問(wèn)題解決
前篇我們只是完成了同一賬戶的登錄人數(shù)限制shiro攔截器的編寫(xiě),對(duì)于手動(dòng)踢出用戶的功能只是說(shuō)了采用在session域中添加一個(gè)key為kickout的布爾值,由之前編寫(xiě)的KickoutSessionControlFilter攔截器來(lái)判斷是否將用戶踢出,還沒(méi)有說(shuō)怎么獲取當(dāng)前在線用戶的列表的核心代碼,下面貼出來(lái):
/** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> * * @author z77z * @since 2017-02-10 */ @Service public class SysUserService extends ServiceImpl<SysUserMapper, SysUser> { @Autowired RedisSessionDAO redisSessionDAO; public Page<UserOnlineBo> getPagePlus(FrontPage<UserOnlineBo> frontPage) { // 因?yàn)槲覀兪怯胷edis實(shí)現(xiàn)了shiro的session的Dao,而且是采用了shiro+redis這個(gè)插件 // 所以從spring容器中獲取redisSessionDAO // 來(lái)獲取session列表. Collection<Session> sessions = redisSessionDAO.getActiveSessions(); Iterator<Session> it = sessions.iterator(); List<UserOnlineBo> onlineUserList = new ArrayList<UserOnlineBo>(); Page<UserOnlineBo> pageList = frontPage.getPagePlus(); // 遍歷session while (it.hasNext()) { // 這是shiro已經(jīng)存入session的 // 現(xiàn)在直接取就是了 Session session = it.next(); // 如果被標(biāo)記為踢出就不顯示 Object obj = session.getAttribute("kickout"); if (obj != null) continue; UserOnlineBo onlineUser = getSessionBo(session); onlineUserList.add(onlineUser); } // 再將List<UserOnlineBo>轉(zhuǎn)換成mybatisPlus封裝的page對(duì)象 int page = frontPage.getPage() - 1; int rows = frontPage.getRows() - 1; int startIndex = page * rows; int endIndex = (page * rows) + rows; int size = onlineUserList.size(); if (endIndex > size) { endIndex = size; } pageList.setRecords(onlineUserList.subList(startIndex, endIndex)); pageList.setTotal(size); return pageList; } //從session中獲取UserOnline對(duì)象 private UserOnlineBo getSessionBo(Session session){ //獲取session登錄信息。 Object obj = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if(null == obj){ return null; } //確保是 SimplePrincipalCollection對(duì)象。 if(obj instanceof SimplePrincipalCollection){ SimplePrincipalCollection spc = (SimplePrincipalCollection)obj; /** * 獲取用戶登錄的,@link SampleRealm.doGetAuthenticationInfo(...)方法中 * return new SimpleAuthenticationInfo(user,user.getPswd(), getName());的user 對(duì)象。 */ obj = spc.getPrimaryPrincipal(); if(null != obj && obj instanceof SysUser){ //存儲(chǔ)session + user 綜合信息 UserOnlineBo userBo = new UserOnlineBo((SysUser)obj); //最后一次和系統(tǒng)交互的時(shí)間 userBo.setLastAccess(session.getLastAccessTime()); //主機(jī)的ip地址 userBo.setHost(session.getHost()); //session ID userBo.setSessionId(session.getId().toString()); //session最后一次與系統(tǒng)交互的時(shí)間 userBo.setLastLoginTime(session.getLastAccessTime()); //回話到期 ttl(ms) userBo.setTimeout(session.getTimeout()); //session創(chuàng)建時(shí)間 userBo.setStartTime(session.getStartTimestamp()); //是否踢出 userBo.setSessionStatus(false); return userBo; } } return null; } }
代碼中注釋比較完善,也可以去下載源碼查看,這樣結(jié)合看,跟容易理解,不懂的在評(píng)論區(qū)留言,看見(jiàn)必回!
對(duì)Ajax請(qǐng)求的優(yōu)化:這里有一個(gè)前提,我們知道Ajax不能做頁(yè)面redirect和forward跳轉(zhuǎn),所以Ajax請(qǐng)求假如沒(méi)登錄,那么這個(gè)請(qǐng)求給用戶的感覺(jué)就是沒(méi)有任何反應(yīng),而用戶又不知道用戶已經(jīng)退出了。也就是說(shuō)在KickoutSessionControlFilter攔截器攔截后,正常如果被踢出,就會(huì)跳轉(zhuǎn)到被踢出的提示頁(yè)面,如果是Ajax請(qǐng)求,給用戶的感覺(jué)就是沒(méi)有感覺(jué),核心解決代碼如下:
Map<String, String> resultMap = new HashMap<String, String>(); //判斷是不是Ajax請(qǐng)求 if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) { resultMap.put("user_status", "300"); resultMap.put("message", "您已經(jīng)在其他地方登錄,請(qǐng)重新登錄!"); //輸出json串 out(response, resultMap); }else{ //重定向 WebUtils.issueRedirect(request, response, kickoutUrl); } private void out(ServletResponse hresponse, Map<String, String> resultMap) throws IOException { try { hresponse.setCharacterEncoding("UTF-8"); PrintWriter out = hresponse.getWriter(); out.println(JSON.toJSONString(resultMap)); out.flush(); out.close(); } catch (Exception e) { System.err.println("KickoutSessionFilter.class 輸出JSON異常,可以忽略。"); } }
這是在KickoutSessionControlFilter這個(gè)攔截器里面做的修改。
目標(biāo):
- 現(xiàn)在項(xiàng)目里面的密碼整個(gè)流程都是以明文的方式傳遞的。這樣在實(shí)際應(yīng)用中是很不安全的,京東,開(kāi)源中國(guó)等這些大公司都有泄庫(kù)事件,這樣對(duì)用戶的隱私造成巨大的影響,所以將密碼加密存儲(chǔ)傳輸就非常必要了。
- 密碼重試次數(shù)限制,也是出于安全性的考慮。
實(shí)現(xiàn)目標(biāo)一:
shiro本身是有對(duì)密碼加密進(jìn)行實(shí)現(xiàn)的,提供了PasswordService及CredentialsMatcher用于提供加密密碼及驗(yàn)證密碼服務(wù)。
我就是自己實(shí)現(xiàn)的EDS加密,并且保存的加密明文是采用password+username的方式,減小了密碼相同,密文也相同的問(wèn)題,這里我只是貼一下,EDS的加密解密代碼,另外我還改了MyShiroRealm文件,再查數(shù)據(jù)庫(kù)的時(shí)候加密后再查,而且在創(chuàng)建用戶的時(shí)候不要忘記的加密存到數(shù)據(jù)庫(kù)。這里就補(bǔ)貼代碼了。
/** * DES加密解密 * * @author z77z * @datetime 2017-3-13 */ public class MyDES { /** * DES算法密鑰 */ private static final byte[] DES_KEY = { 21, 1, -110, 82, -32, -85, -128, -65 }; /** * 數(shù)據(jù)加密,算法(DES) * * @param data * 要進(jìn)行加密的數(shù)據(jù) * @return 加密后的數(shù)據(jù) */ @SuppressWarnings("restriction") public static String encryptBasedDes(String data) { String encryptedData = null; try { // DES算法要求有一個(gè)可信任的隨機(jī)數(shù)源 SecureRandom sr = new SecureRandom(); DESKeySpec deskey = new DESKeySpec(DES_KEY); // 創(chuàng)建一個(gè)密匙工廠,然后用它把DESKeySpec轉(zhuǎn)換成一個(gè)SecretKey對(duì)象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey key = keyFactory.generateSecret(deskey); // 加密對(duì)象 Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.ENCRYPT_MODE, key, sr); // 加密,并把字節(jié)數(shù)組編碼成字符串 encryptedData = new sun.misc.BASE64Encoder().encode(cipher.doFinal(data.getBytes())); } catch (Exception e) { // log.error("加密錯(cuò)誤,錯(cuò)誤信息:", e); throw new RuntimeException("加密錯(cuò)誤,錯(cuò)誤信息:", e); } return encryptedData; } /** * 數(shù)據(jù)解密,算法(DES) * * @param cryptData * 加密數(shù)據(jù) * @return 解密后的數(shù)據(jù) */ @SuppressWarnings("restriction") public static String decryptBasedDes(String cryptData) { String decryptedData = null; try { // DES算法要求有一個(gè)可信任的隨機(jī)數(shù)源 SecureRandom sr = new SecureRandom(); DESKeySpec deskey = new DESKeySpec(DES_KEY); // 創(chuàng)建一個(gè)密匙工廠,然后用它把DESKeySpec轉(zhuǎn)換成一個(gè)SecretKey對(duì)象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey key = keyFactory.generateSecret(deskey); // 解密對(duì)象 Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.DECRYPT_MODE, key, sr); // 把字符串解碼為字節(jié)數(shù)組,并解密 decryptedData = new String(cipher.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(cryptData))); } catch (Exception e) { // log.error("解密錯(cuò)誤,錯(cuò)誤信息:", e); throw new RuntimeException("解密錯(cuò)誤,錯(cuò)誤信息:", e); } return decryptedData; } public static void main(String[] args) { String str = "123456"; // DES數(shù)據(jù)加密 String s1 = encryptBasedDes(str); System.out.println(s1); // DES數(shù)據(jù)解密 String s2 = decryptBasedDes(s1); System.err.println(s2); } }
實(shí)現(xiàn)目標(biāo)二
如在1個(gè)小時(shí)內(nèi)密碼最多重試5次,如果嘗試次數(shù)超過(guò)5次就鎖定1小時(shí),1小時(shí)后可再次重試,如果還是重試失敗,可以鎖定如1天,以此類推,防止密碼被暴力破解。我們使用redis數(shù)據(jù)庫(kù)來(lái)保存當(dāng)前用戶登錄次數(shù),也就是執(zhí)行身份認(rèn)證方法:
MyShiroRealm.doGetAuthenticationInfo()的次數(shù),如果登錄成功就清空計(jì)數(shù)。超過(guò)就返回相應(yīng)錯(cuò)誤信息。(redis的具體操作可以去看我之前的springboot+redis的一篇博客)根據(jù)這個(gè)邏輯,修改MyShiroRealm.java如下:
/** * 認(rèn)證信息.(身份驗(yàn)證) : Authentication 是用來(lái)驗(yàn)證用戶身份 * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken) throws AuthenticationException { System.out.println("身份認(rèn)證方法:MyShiroRealm.doGetAuthenticationInfo()"); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String name = token.getUsername(); String password = String.valueOf(token.getPassword()); //訪問(wèn)一次,計(jì)數(shù)一次 ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue(); opsForValue.increment(SHIRO_LOGIN_COUNT+name, 1); //計(jì)數(shù)大于5時(shí),設(shè)置用戶被鎖定一小時(shí) if(Integer.parseInt(opsForValue.get(SHIRO_LOGIN_COUNT+name))>=5){ opsForValue.set(SHIRO_IS_LOCK+name, "LOCK"); stringRedisTemplate.expire(SHIRO_IS_LOCK+name, 1, TimeUnit.HOURS); } if ("LOCK".equals(opsForValue.get(SHIRO_IS_LOCK+name))){ throw new DisabledAccountException("由于密碼輸入錯(cuò)誤次數(shù)大于5次,帳號(hào)已經(jīng)禁止登錄!"); } Map<String, Object> map = new HashMap<String, Object>(); map.put("nickname", name); //密碼進(jìn)行加密處理 明文為 password+name String paw = password+name; String pawDES = MyDES.encryptBasedDes(paw); map.put("pswd", pawDES); SysUser user = null; // 從數(shù)據(jù)庫(kù)獲取對(duì)應(yīng)用戶名密碼的用戶 List<SysUser> userList = sysUserService.selectByMap(map); if(userList.size()!=0){ user = userList.get(0); } if (null == user) { throw new AccountException("帳號(hào)或密碼不正確!"); }else if(user.getStatus()==0){ /** * 如果用戶的status為禁用。那么就拋出<code>DisabledAccountException</code> */ throw new DisabledAccountException("此帳號(hào)已經(jīng)設(shè)置為禁止登錄!"); }else{ //登錄成功 //更新登錄時(shí)間 last login time user.setLastLoginTime(new Date()); sysUserService.updateById(user); //清空登錄計(jì)數(shù) opsForValue.set(SHIRO_LOGIN_COUNT+name, "0"); } return new SimpleAuthenticationInfo(user, password, getName()); }
demo下載地址:springboot_mybatisplus_jb51.rar
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
MyBatis的注解使用、ORM層優(yōu)化方式(懶加載和緩存)
這篇文章主要介紹了MyBatis的注解使用、ORM層優(yōu)化方式(懶加載和緩存),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10zuulGateway 通過(guò)filter統(tǒng)一修改返回值的操作
這篇文章主要介紹了zuulGateway 通過(guò)filter統(tǒng)一修改返回值的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10如何在Netty中注解使用Service或者M(jìn)apper
這篇文章主要介紹了如何在Netty中注解使用Service或者M(jìn)apper,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02Java使用MessageFormat應(yīng)注意的問(wèn)題
這篇文章主要介紹了Java使用MessageFormat應(yīng)注意的問(wèn)題,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-06-06Java自定義equals產(chǎn)生的問(wèn)題分析
這篇文章主要介紹了Java自定義equals時(shí)super.equals帶來(lái)的問(wèn)題分析,總的來(lái)說(shuō)這并不是一道難題,那為什么要拿出這道題介紹?拿出這道題真正想要傳達(dá)的是解題的思路,以及不斷優(yōu)化探尋最優(yōu)解的過(guò)程。希望通過(guò)這道題能給你帶來(lái)一種解題優(yōu)化的思路2023-01-01SpringBoot+Shiro+LayUI權(quán)限管理系統(tǒng)項(xiàng)目源碼
本項(xiàng)目旨在打造一個(gè)基于RBAC架構(gòu)模式的通用的、并不復(fù)雜但易用的權(quán)限管理系統(tǒng),通過(guò)SpringBoot+Shiro+LayUI權(quán)限管理系統(tǒng)項(xiàng)目可以更好的幫助我們掌握springboot知識(shí)點(diǎn),感興趣的朋友一起看看吧2021-04-04Maven倉(cāng)庫(kù)的具體使用(本地倉(cāng)庫(kù)+遠(yuǎn)程倉(cāng)庫(kù))
Maven 在某個(gè)統(tǒng)一的位置存儲(chǔ)所有項(xiàng)目的構(gòu)件,這個(gè)統(tǒng)一的位置,我們就稱之為倉(cāng)庫(kù),本文主要介紹了Maven倉(cāng)庫(kù)的具體使用(本地倉(cāng)庫(kù)+遠(yuǎn)程倉(cāng)庫(kù)),感興趣的可以了解一下2023-11-11Java在制作jar包時(shí)引用第三方j(luò)ar包的方法
這篇文章主要介紹了Java在制作jar包時(shí)引用第三方j(luò)ar包的方法的相關(guān)資料,需要的朋友可以參考下2016-01-01詳解SpringBoot Redis自適應(yīng)配置(Cluster Standalone Sentinel)
這篇文章主要介紹了詳解SpringBoot Redis自適應(yīng)配置(Cluster Standalone Sentinel),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07