首页 > 编程 > Java > 正文

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

2019-11-26 12:49:30
字体:
来源:转载
供稿:网友

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

前篇思考问题解决

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

/** * <p> * 服务实现类 * </p> * * @author z77z * @since 2017-02-10 */@Servicepublic 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请求,给用户的感觉就是没有感觉,核心解决代码如下:

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这个拦截器里面做的修改。

目标:

  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 */@Overrideprotected 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_jb51.rar

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

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表