這個項目寫到現在,基本的雛形出來了,在此感謝一直關注的童鞋,送你們一句最近剛學習的一句雞湯:念念不忘,必有回響。再貼一張ui圖片:
前篇思考問題解決
前篇我們只是完成了同一賬戶的登錄人數限制shiro攔截器的編寫,對于手動踢出用戶的功能只是說了采用在session域中添加一個key為kickout的布爾值,由之前編寫的KickoutSessionControlFilter攔截器來判斷是否將用戶踢出,還沒有說怎么獲取當前在線用戶的列表的核心代碼,下面貼出來:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
/** * <p> * 服務實現類 * </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) { // 因為我們是用redis實現了shiro的session的Dao,而且是采用了shiro+redis這個插件 // 所以從spring容器中獲取redisSessionDAO // 來獲取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已經存入session的 // 現在直接取就是了 Session session = it.next(); // 如果被標記為踢出就不顯示 Object obj = session.getAttribute( "kickout" ); if (obj != null ) continue ; UserOnlineBo onlineUser = getSessionBo(session); onlineUserList.add(onlineUser); } // 再將List<UserOnlineBo>轉換成mybatisPlus封裝的page對象 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對象 private UserOnlineBo getSessionBo(Session session){ //獲取session登錄信息。 Object obj = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if ( null == obj){ return null ; } //確保是 SimplePrincipalCollection對象。 if (obj instanceof SimplePrincipalCollection){ SimplePrincipalCollection spc = (SimplePrincipalCollection)obj; /** * 獲取用戶登錄的,@link SampleRealm.doGetAuthenticationInfo(...)方法中 * return new SimpleAuthenticationInfo(user,user.getPswd(), getName());的user 對象。 */ obj = spc.getPrimaryPrincipal(); if ( null != obj && obj instanceof SysUser){ //存儲session + user 綜合信息 UserOnlineBo userBo = new UserOnlineBo((SysUser)obj); //最后一次和系統交互的時間 userBo.setLastAccess(session.getLastAccessTime()); //主機的ip地址 userBo.setHost(session.getHost()); //session ID userBo.setSessionId(session.getId().toString()); //session最后一次與系統交互的時間 userBo.setLastLoginTime(session.getLastAccessTime()); //回話到期 ttl(ms) userBo.setTimeout(session.getTimeout()); //session創建時間 userBo.setStartTime(session.getStartTimestamp()); //是否踢出 userBo.setSessionStatus( false ); return userBo; } } return null ; } } |
代碼中注釋比較完善,也可以去下載源碼查看,這樣結合看,跟容易理解,不懂的在評論區留言,看見必回!
對Ajax請求的優化:這里有一個前提,我們知道Ajax不能做頁面redirect和forward跳轉,所以Ajax請求假如沒登錄,那么這個請求給用戶的感覺就是沒有任何反應,而用戶又不知道用戶已經退出了。也就是說在KickoutSessionControlFilter攔截器攔截后,正常如果被踢出,就會跳轉到被踢出的提示頁面,如果是Ajax請求,給用戶的感覺就是沒有感覺,核心解決代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
Map<String, String> resultMap = new HashMap<String, String>(); //判斷是不是Ajax請求 if ( "XMLHttpRequest" .equalsIgnoreCase(((HttpServletRequest) request).getHeader( "X-Requested-With" ))) { resultMap.put( "user_status" , "300" ); resultMap.put( "message" , "您已經在其他地方登錄,請重新登錄!" ); //輸出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這個攔截器里面做的修改。
目標:
- 現在項目里面的密碼整個流程都是以明文的方式傳遞的。這樣在實際應用中是很不安全的,京東,開源中國等這些大公司都有泄庫事件,這樣對用戶的隱私造成巨大的影響,所以將密碼加密存儲傳輸就非常必要了。
- 密碼重試次數限制,也是出于安全性的考慮。
實現目標一:
shiro本身是有對密碼加密進行實現的,提供了PasswordService及CredentialsMatcher用于提供加密密碼及驗證密碼服務。
我就是自己實現的EDS加密,并且保存的加密明文是采用password+username的方式,減小了密碼相同,密文也相同的問題,這里我只是貼一下,EDS的加密解密代碼,另外我還改了MyShiroRealm文件,再查數據庫的時候加密后再查,而且在創建用戶的時候不要忘記的加密存到數據庫。這里就補貼代碼了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
/** * 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 }; /** * 數據加密,算法(DES) * * @param data * 要進行加密的數據 * @return 加密后的數據 */ @SuppressWarnings ( "restriction" ) public static String encryptBasedDes(String data) { String encryptedData = null ; try { // DES算法要求有一個可信任的隨機數源 SecureRandom sr = new SecureRandom(); DESKeySpec deskey = new DESKeySpec(DES_KEY); // 創建一個密匙工廠,然后用它把DESKeySpec轉換成一個SecretKey對象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" ); SecretKey key = keyFactory.generateSecret(deskey); // 加密對象 Cipher cipher = Cipher.getInstance( "DES" ); cipher.init(Cipher.ENCRYPT_MODE, key, sr); // 加密,并把字節數組編碼成字符串 encryptedData = new sun.misc.BASE64Encoder().encode(cipher.doFinal(data.getBytes())); } catch (Exception e) { // log.error("加密錯誤,錯誤信息:", e); throw new RuntimeException( "加密錯誤,錯誤信息:" , e); } return encryptedData; } /** * 數據解密,算法(DES) * * @param cryptData * 加密數據 * @return 解密后的數據 */ @SuppressWarnings ( "restriction" ) public static String decryptBasedDes(String cryptData) { String decryptedData = null ; try { // DES算法要求有一個可信任的隨機數源 SecureRandom sr = new SecureRandom(); DESKeySpec deskey = new DESKeySpec(DES_KEY); // 創建一個密匙工廠,然后用它把DESKeySpec轉換成一個SecretKey對象 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" ); SecretKey key = keyFactory.generateSecret(deskey); // 解密對象 Cipher cipher = Cipher.getInstance( "DES" ); cipher.init(Cipher.DECRYPT_MODE, key, sr); // 把字符串解碼為字節數組,并解密 decryptedData = new String(cipher.doFinal( new sun.misc.BASE64Decoder().decodeBuffer(cryptData))); } catch (Exception e) { // log.error("解密錯誤,錯誤信息:", e); throw new RuntimeException( "解密錯誤,錯誤信息:" , e); } return decryptedData; } public static void main(String[] args) { String str = "123456" ; // DES數據加密 String s1 = encryptBasedDes(str); System.out.println(s1); // DES數據解密 String s2 = decryptBasedDes(s1); System.err.println(s2); } } |
實現目標二
如在1個小時內密碼最多重試5次,如果嘗試次數超過5次就鎖定1小時,1小時后可再次重試,如果還是重試失敗,可以鎖定如1天,以此類推,防止密碼被暴力破解。我們使用redis數據庫來保存當前用戶登錄次數,也就是執行身份認證方法:
MyShiroRealm.doGetAuthenticationInfo()的次數,如果登錄成功就清空計數。超過就返回相應錯誤信息。(redis的具體操作可以去看我之前的springboot+redis的一篇博客)根據這個邏輯,修改MyShiroRealm.java如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
/** * 認證信息.(身份驗證) : Authentication 是用來驗證用戶身份 * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken) throws AuthenticationException { System.out.println( "身份認證方法:MyShiroRealm.doGetAuthenticationInfo()" ); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String name = token.getUsername(); String password = String.valueOf(token.getPassword()); //訪問一次,計數一次 ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue(); opsForValue.increment(SHIRO_LOGIN_COUNT+name, 1 ); //計數大于5時,設置用戶被鎖定一小時 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( "由于密碼輸入錯誤次數大于5次,帳號已經禁止登錄!" ); } Map<String, Object> map = new HashMap<String, Object>(); map.put( "nickname" , name); //密碼進行加密處理 明文為 password+name String paw = password+name; String pawDES = MyDES.encryptBasedDes(paw); map.put( "pswd" , pawDES); SysUser user = null ; // 從數據庫獲取對應用戶名密碼的用戶 List<SysUser> userList = sysUserService.selectByMap(map); if (userList.size()!= 0 ){ user = userList.get( 0 ); } if ( null == user) { throw new AccountException( "帳號或密碼不正確!" ); } else if (user.getStatus()== 0 ){ /** * 如果用戶的status為禁用。那么就拋出<code>DisabledAccountException</code> */ throw new DisabledAccountException( "此帳號已經設置為禁止登錄!" ); } else { //登錄成功 //更新登錄時間 last login time user.setLastLoginTime( new Date()); sysUserService.updateById(user); //清空登錄計數 opsForValue.set(SHIRO_LOGIN_COUNT+name, "0" ); } return new SimpleAuthenticationInfo(user, password, getName()); } |
demo下載地址:springboot_mybatisplus.rar
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.jianshu.com/p/836308f0b81a