热门标签 | HotTags
当前位置:  开发笔记 > 后端 > 正文

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

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

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

前篇思考问题解决

前篇我们只是完成了同一账户的登录人数限制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; } }

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

对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用于提供加密密码及验证密码服务。

我就是自己实现的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()的次数,如果登录成功就清空计数。超过就返回相应错误信息。(redis的具体操作可以去看我之前的springboot+redis的一篇博客)根据这个逻辑,修改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());
}

demo下载地址:springboot_mybatisplus_jb51.rar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • 单点登录原理及实现方案详解
    本文详细介绍了单点登录的原理及实现方案,其中包括共享Session的方式,以及基于Redis的Session共享方案。同时,还分享了作者在应用环境中所遇到的问题和经验,希望对读者有所帮助。 ... [详细]
  • 玩转直播系列之消息模块演进(3)
    一、背景即时消息(IM)系统是直播系统重要的组成部分,一个稳定的,有容错的,灵活的,支持高并发的消息模块是影响直播系统用户体验的重要因素。IM长连接服务在直播系统有发挥着举足轻重的 ... [详细]
  • 技术分享:如何在没有公钥的情况下实现JWT密钥滥用
      ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 生成对抗式网络GAN及其衍生CGAN、DCGAN、WGAN、LSGAN、BEGAN介绍
    一、GAN原理介绍学习GAN的第一篇论文当然由是IanGoodfellow于2014年发表的GenerativeAdversarialNetworks(论文下载链接arxiv:[h ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • Servlet多用户登录时HttpSession会话信息覆盖问题的解决方案
    本文讨论了在Servlet多用户登录时可能出现的HttpSession会话信息覆盖问题,并提供了解决方案。通过分析JSESSIONID的作用机制和编码方式,我们可以得出每个HttpSession对象都是通过客户端发送的唯一JSESSIONID来识别的,因此无需担心会话信息被覆盖的问题。需要注意的是,本文讨论的是多个客户端级别上的多用户登录,而非同一个浏览器级别上的多用户登录。 ... [详细]
  • 一面自我介绍对象相等的判断,equals方法实现。可以简单描述挫折,并说明自己如何克服,最终有哪些收获。职业规划表明自己决心,首先自己不准备继续求学了,必须招工作了。希望去哪 ... [详细]
  • 现在很多App在与服务器接口的请求和响应过程中,为了安全都会涉及到加密和解密的问题,如果不加的话就会是明文的,即使加了GZIP也可以被直接解压成明文。如果同时有Android和IO ... [详细]
author-avatar
111wen_292
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有