SpringBoot+Shiro學(xué)習(xí)之密碼加密和登錄失敗次數(shù)限制示例
這個項目寫到現(xiàn)在,基本的雛形出來了,在此感謝一直關(guān)注的童鞋,送你們一句最近剛學(xué)習(xí)的一句雞湯:念念不忘,必有回響。再貼一張ui圖片:

前篇思考問題解決
前篇我們只是完成了同一賬戶的登錄人數(shù)限制shiro攔截器的編寫,對于手動踢出用戶的功能只是說了采用在session域中添加一個key為kickout的布爾值,由之前編寫的KickoutSessionControlFilter攔截器來判斷是否將用戶踢出,還沒有說怎么獲取當(dāng)前在線用戶的列表的核心代碼,下面貼出來:
/**
* <p>
* 服務(wù)實現(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) {
// 因為我們是用redis實現(xiàn)了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已經(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對象
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);
//最后一次和系統(tǒng)交互的時間
userBo.setLastAccess(session.getLastAccessTime());
//主機(jī)的ip地址
userBo.setHost(session.getHost());
//session ID
userBo.setSessionId(session.getId().toString());
//session最后一次與系統(tǒng)交互的時間
userBo.setLastLoginTime(session.getLastAccessTime());
//回話到期 ttl(ms)
userBo.setTimeout(session.getTimeout());
//session創(chuàng)建時間
userBo.setStartTime(session.getStartTimestamp());
//是否踢出
userBo.setSessionStatus(false);
return userBo;
}
}
return null;
}
}
代碼中注釋比較完善,也可以去下載源碼查看,這樣結(jié)合看,跟容易理解,不懂的在評論區(qū)留言,看見必回!
對Ajax請求的優(yōu)化:這里有一個前提,我們知道Ajax不能做頁面redirect和forward跳轉(zhuǎn),所以Ajax請求假如沒登錄,那么這個請求給用戶的感覺就是沒有任何反應(yīng),而用戶又不知道用戶已經(jīng)退出了。也就是說在KickoutSessionControlFilter攔截器攔截后,正常如果被踢出,就會跳轉(zhuǎn)到被踢出的提示頁面,如果是Ajax請求,給用戶的感覺就是沒有感覺,核心解決代碼如下:
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", "您已經(jī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這個攔截器里面做的修改。
目標(biāo):
- 現(xiàn)在項目里面的密碼整個流程都是以明文的方式傳遞的。這樣在實際應(yīng)用中是很不安全的,京東,開源中國等這些大公司都有泄庫事件,這樣對用戶的隱私造成巨大的影響,所以將密碼加密存儲傳輸就非常必要了。
- 密碼重試次數(shù)限制,也是出于安全性的考慮。
實現(xiàn)目標(biāo)一:
shiro本身是有對密碼加密進(jìn)行實現(xiàn)的,提供了PasswordService及CredentialsMatcher用于提供加密密碼及驗證密碼服務(wù)。
我就是自己實現(xiàn)的EDS加密,并且保存的加密明文是采用password+username的方式,減小了密碼相同,密文也相同的問題,這里我只是貼一下,EDS的加密解密代碼,另外我還改了MyShiroRealm文件,再查數(shù)據(jù)庫的時候加密后再查,而且在創(chuàng)建用戶的時候不要忘記的加密存到數(shù)據(jù)庫。這里就補(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算法要求有一個可信任的隨機(jī)數(shù)源
SecureRandom sr = new SecureRandom();
DESKeySpec deskey = new DESKeySpec(DES_KEY);
// 創(chuàng)建一個密匙工廠,然后用它把DESKeySpec轉(zhuǎn)換成一個SecretKey對象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(deskey);
// 加密對象
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("加密錯誤,錯誤信息:", e);
throw new RuntimeException("加密錯誤,錯誤信息:", 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算法要求有一個可信任的隨機(jī)數(shù)源
SecureRandom sr = new SecureRandom();
DESKeySpec deskey = new DESKeySpec(DES_KEY);
// 創(chuàng)建一個密匙工廠,然后用它把DESKeySpec轉(zhuǎn)換成一個SecretKey對象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(deskey);
// 解密對象
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("解密錯誤,錯誤信息:", e);
throw new RuntimeException("解密錯誤,錯誤信息:", 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);
}
}
實現(xiàn)目標(biāo)二
如在1個小時內(nèi)密碼最多重試5次,如果嘗試次數(shù)超過5次就鎖定1小時,1小時后可再次重試,如果還是重試失敗,可以鎖定如1天,以此類推,防止密碼被暴力破解。我們使用redis數(shù)據(jù)庫來保存當(dāng)前用戶登錄次數(shù),也就是執(zhí)行身份認(rèn)證方法:
MyShiroRealm.doGetAuthenticationInfo()的次數(shù),如果登錄成功就清空計數(shù)。超過就返回相應(yīng)錯誤信息。(redis的具體操作可以去看我之前的springboot+redis的一篇博客)根據(jù)這個邏輯,修改MyShiroRealm.java如下:
/**
* 認(rèn)證信息.(身份驗證) : Authentication 是用來驗證用戶身份
*
* @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());
//訪問一次,計數(shù)一次
ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
opsForValue.increment(SHIRO_LOGIN_COUNT+name, 1);
//計數(shù)大于5時,設(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("由于密碼輸入錯誤次數(shù)大于5次,帳號已經(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ù)庫獲取對應(yīng)用戶名密碼的用戶
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("此帳號已經(jīng)設(shè)置為禁止登錄!");
}else{
//登錄成功
//更新登錄時間 last login time
user.setLastLoginTime(new Date());
sysUserService.updateById(user);
//清空登錄計數(shù)
opsForValue.set(SHIRO_LOGIN_COUNT+name, "0");
}
return new SimpleAuthenticationInfo(user, password, getName());
}
demo下載地址:springboot_mybatisplus_jb51.rar
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
MyBatis的注解使用、ORM層優(yōu)化方式(懶加載和緩存)
這篇文章主要介紹了MyBatis的注解使用、ORM層優(yōu)化方式(懶加載和緩存),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
zuulGateway 通過filter統(tǒng)一修改返回值的操作
這篇文章主要介紹了zuulGateway 通過filter統(tǒng)一修改返回值的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
如何在Netty中注解使用Service或者M(jìn)apper
這篇文章主要介紹了如何在Netty中注解使用Service或者M(jìn)apper,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
Java使用MessageFormat應(yīng)注意的問題
這篇文章主要介紹了Java使用MessageFormat應(yīng)注意的問題,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的朋友可以參考一下2022-06-06
SpringBoot+Shiro+LayUI權(quán)限管理系統(tǒng)項目源碼
本項目旨在打造一個基于RBAC架構(gòu)模式的通用的、并不復(fù)雜但易用的權(quán)限管理系統(tǒng),通過SpringBoot+Shiro+LayUI權(quán)限管理系統(tǒng)項目可以更好的幫助我們掌握springboot知識點(diǎn),感興趣的朋友一起看看吧2021-04-04
詳解SpringBoot Redis自適應(yīng)配置(Cluster Standalone Sentinel)
這篇文章主要介紹了詳解SpringBoot Redis自適應(yīng)配置(Cluster Standalone Sentinel),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07

