SpringBoot+Shiro 学习之密码加密和登录失败次数限制

·  阅读 1597
SpringBoot+Shiro 学习之密码加密和登录失败次数限制

这个项目写到现在,基本的雏形出来了,在此感谢一直关注的童鞋,送你们一句最近刚学习的一句鸡汤:念念不忘,必有回响。再贴一张ui图片:

z77z后台管理系统 z77z后台管理系统

·······················································································································································

个人博客:z77z.oschina.io/

此项目下载地址:git.oschina.net/z77z/spring…

·······················································································································································

前篇思考问题解决


  1. 前篇我们只是完成了同一账户的登录人数限制shiro拦截器的编写,对于手动踢出用户的功能只是说了采用在session域中添加一个key为kickout的布尔值,由之前编写的KickoutSessionControlFilter拦截器来判断是否将用户踢出,还没有说怎么获取当前在线用户的列表的核心代码,下面贴出来:
/**
 * 


 * 服务实现类
 * 


 *
 * @author z77z
 * @since 2017-02-10
 */
@Service
public class SysUserService extends ServiceImpl {
	@Autowired
	RedisSessionDAO redisSessionDAO;
	public Page getPagePlus(FrontPage frontPage) {
		// 因为我们是用redis实现了shiro的session的Dao,而且是采用了shiro+redis这个插件
		// 所以从spring容器中获取redisSessionDAO
		// 来获取session列表.
		Collection sessions = redisSessionDAO.getActiveSessions();
		Iterator it = sessions.iterator();
		List onlineUserList = new ArrayList();
		Page 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转换成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;
	}
}复制代码

代码中注释比较完善,也可以去下载源码查看,这样结合看,跟容易理解,不懂的在评论区留言,看见必回!

  1. 对Ajax请求的优化:这里有一个前提,我们知道Ajax不能做页面redirect和forward跳转,所以Ajax请求假如没登录,那么这个请求给用户的感觉就是没有任何反应,而用户又不知道用户已经退出了。也就是说在KickoutSessionControlFilter拦截器拦截后,正常如果被踢出,就会跳转到被踢出的提示页面,如果是Ajax请求,给用户的感觉就是没有感觉,核心解决代码如下:
Map resultMap = new HashMap();
//判断是不是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 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这个拦截器里面做的修改。

目标:


  1. 现在项目里面的密码整个流程都是以明文的方式传递的。这样在实际应用中是很不安全的,京东,开源中国等这些大公司都有泄库事件,这样对用户的隐私造成巨大的影响,所以将密码加密存储传输就非常必要了。

  2. 密码重试次数限制,也是出于安全性的考虑。

实现目标一:


shiro本身是有对密码加密进行实现的,提供了PasswordService及CredentialsMatcher用于提供加密密码及验证密码服务。这里我觉得这种过于麻烦,大家有需要可以去看这篇博客:shiro加密解密

我就是自己实现的EDS加密,并且保存的加密明文是采用password+username的方式,减小了密码相同,密文也相同的问题,这里我只是贴一下,EDS的加密解密代码,另外我还改了MyShiroRealm文件,再查数据库的时候加密后再查,而且在创建用户的时候不要忘记的加密存到数据库。这里就补贴代码了。

/**
 * 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()的次数,如果登录成功就清空计数。超过就返回相应错误信息。根据这个逻辑,修改MyShiroRealm.java如下:

/**
* 认证信息.(身份验证) : 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 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 map = new HashMap();
	map.put("nickname", name);
	//密码进行加密处理  明文为  password+name
	String paw = password+name;
	String pawDES = MyDES.encryptBasedDes(paw);
	map.put("pswd", pawDES);
	SysUser user = null;
	// 从数据库获取对应用户名密码的用户
	List userList = sysUserService.selectByMap(map);
	if(userList.size()!=0){
		user = userList.get(0);
	} 
	if (null == user) {
		throw new AccountException("帐号或密码不正确!");
	}else if(user.getStatus()==0){
		/**
		 * 如果用户的status为禁用。那么就抛出DisabledAccountException
		 */
		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());
}复制代码
分类:
后端
收藏成功!
已添加到「」, 点击更改