应用系统如何保证用户登录验证的安全有效?

1.概述

目前开发的应用系统很多,其中最基本的功能是用户登录,这是每一个系统基本上都有的功能,但一般系统对于这个功能的实现都比较简单,无法达到安全的目的。如何正确的对用户名密码进行验证,防止用户名、密码泄露,防止暴力破解,这是一个需要大家关心的问题。特别是目前政府网站项目,都会进行安全扫描,其中暴露的一个问题就是用户名、密码明文传输、无法防止暴力破解。

2.解决办法

要完美的解决以上问题,需要了解加密、解密、数据安全、网络安全等相关知识,网上也有很多的解决方案。本文结合实际应用,给出一个例子,供大家参考。

2.1 防止明文传输

2.1.1 采用安全HTTPS(SSL/TLS)协议

采用安全HTTPS(SSL/TLS)协议,而不是普通的HTTP协议,这样前后端传输的内容就是加密后内容了,解决了明文传输的问题,可以很好地防止非法嗅探,可参考下面的实现:

[Spring Boot SSL [https]配置例子]( https://www.cnblogs.com/chenp...

2.1.2 加强前后端安全处理

若有框架支持用户、密码的,则可直接用框架的用户登录、验证功能,如

Flask框架中密码的加盐哈希加密和验证功能的用法详解

2.1.2.1 前端处理及传输

前端页面得到用户输入的用户名、密码、验证码后,采用Java script函数计算出密码的哈希值,然后调用后台方法进行用户验证,将用户名、密码哈希值、验证码作为后台方法的参数传入。

  • 密码哈希值=哈希(密码明文+盐值(用户名+网站特有的字符串(如域名)))

2.1.2.1 后端验证方法

  • 从数据库取出用户的密码哈希值和对应盐值
  • 将盐值混入前端传入的密码哈希值,并且使用同样的哈希函数进行加密
  • 比较上一步的结果和数据库储存的哈希值是否相同,如果相同那么密码正确,反之密码错误

2.1.2.1 后端数据库存储

  • 数据库存储:用户名、盐值、密码哈希值
  • 密码哈希值 = 哈希(前端传入的密码哈希值) = 哈希(密码明文+盐值(用户名+网站特有的字符串(如域名)))
  • 盐值=随机值

盐值的一些注意事项:

  • 每一用户应有不同的盐值(最常见的错误就是在多次哈希加密中使用相同的盐值或者太短的盐值)
  • 用户创建账户或每次修改密码时都应该重新生成新的盐值进行加密
  • 为了使攻击者无法构造包含所有可能盐值的查询表,盐值必须足够长。一个好的做法是使用和哈希函数输出的字符串等长的盐值比如SHA256算法的输出是256bits(32 bytes)那么盐值也至少应该是32个随机字节。

2.2 防止暴力破解

防止暴力破解,可在前端登录界面加入图片验证,登录时要求同时输入用户名、口令、验证码,这样就很好地防止了别人采用程序或机器人进行字典攻击、暴力攻击(通过采用尝试用户名、口令进行系统登录,从而获取用户信息)。

图片验证可以采用Google Kaptcha组件实现,在前端点击img组件时,调用后台Controller方法生成图像二进制流,直接更新图像内容。后台session中存储了正确的验证码字符串,与页面返回的tryCode比较。若一致跳转到新的页面,否则返回当前页面是,并显示错误信息。

前端关键示例代码如下所示:

<body>
    <!-- 提示 -->
    <h3 th:text="${info}"></h3>
    <div>
        <!-- 后面添加参数起到清除缓存作用 -->
        <img alt="验证码" onclick="this.src='/defaultKaptcha?d='+new Date()*1" src="/defaultKaptcha" />
    </div>
    <form action="imgvrifyControllerDefaultKaptcha" > 
        <input type="text" name="tryCode" />
        <input type="submit" value="提交" class="btn btn-success"></input>
    </form>
</body>

后台验证码生成及验证关键代码:

/**
     * 2、生成验证码
     * @param httpServletRequest
     * @param httpServletResponse
     * @throws Exception
     */
    @RequestMapping("/defaultKaptcha")
    public void defaultKaptcha(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
            throws Exception {
        byte[] captchaChallengeAsJpeg = null;
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        try {
            // 生产验证码字符串并保存到session中
            String createText = defaultKaptcha.createText();
            httpServletRequest.getSession().setAttribute("rightCode", createText);
            // 使用生产的验证码字符串返回一个BufferedImage对象并转为byte写入到byte数组中
            BufferedImage challenge = defaultKaptcha.createImage(createText);
            ImageIO.write(challenge, "jpg", jpegOutputStream);
        } catch (IllegalArgumentException e) {
            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
        captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
        httpServletResponse.setHeader("Cache-Control", "no-store");
        httpServletResponse.setHeader("Pragma", "no-cache");
        httpServletResponse.setDateHeader("Expires", 0);
        httpServletResponse.setContentType("image/jpeg");
        ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();
        responseOutputStream.write(captchaChallengeAsJpeg);
        responseOutputStream.flush();
        responseOutputStream.close();
    }

    /**
     * 3、校对验证码
     * @param httpServletRequest
     * @param httpServletResponse
     * @return
     */
    @RequestMapping("/imgvrifyControllerDefaultKaptcha")
    public ModelAndView imgvrifyControllerDefaultKaptcha(HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) {
        ModelAndView andView = new ModelAndView();
        String rightCode = (String) httpServletRequest.getSession().getAttribute("rightCode");
        String tryCode = httpServletRequest.getParameter("tryCode");
        System.out.println("rightCode:"+rightCode+" ———— tryCode:"+tryCode);
        if (!rightCode.equals(tryCode)) {
            andView.addObject("info", "错误的验证码");
            andView.setViewName("index");
        } else {
            andView.addObject("info", "登录成功");
            andView.setViewName("success");
        }
        return andView;
    }

参考资料

加盐密码哈希:如何正确使用

以上文档的主要内容摘要,供参考:

在基于哈希加密的账户系统中,通常用户注册和认证的流程是这样的:

  • 用户注册一个帐号
  • 密码经过哈希加密储存在数据库中。只要密码被写入磁盘,任何时候都不允许是明文
  • 当用户登录的时候,从数据库取出已经加密的密码,和经过哈希的用户输入进行对比
  • 如果哈希值相同,用户获得登入授权,否则,会被告知输入了无效的登录信息
  • 每当有用户尝试登录,以上两步都会重复

也许你很容易就认为只需要简单地执行一遍加密哈希函数,密码就能安全,那么你大错特错了。有太多的办法可以快速地把密码从简单哈希值中恢复出来,但也有很多比较容易实现的技术能使攻击者的效率大大降低。

正确的做法:恰当使用哈希加密

本节会准确讲述应该如何对密码进行哈希加密。其中第一部分介绍最基本的要素,也是在哈希加密中一定要做到的;后面讲解怎样在这个基础上进行扩展,使得加密更难被破解。

基本要素:加盐哈希

对于每个用户的每个密码,盐值都应该是独一无二的。每当有新用户注册或者修改密码,都应该使用新的盐值进行加密。并且这个盐值也应该足够长,使得有足够多的盐值以供加密。一个好的标准的是:盐值至少和哈希函数的输出一样长;盐值应该被储存和密码哈希一起储存在账户数据表中。

存储密码的步骤

  • 使用CSPRNG生成一个长度足够的盐值
  • 将盐值混入密码,并使用标准的加密哈希函数进行加密,如SHA256
  • 把哈希值和盐值一起存入数据库中对应此用户的那条记录

校验密码的步骤

  • 从数据库取出用户的密码哈希值和对应盐值
  • 将盐值混入用户输入的密码,并且使用同样的哈希函数进行加密
  • 比较上一步的结果和数据库储存的哈希值是否相同,如果相同那么密码正确,反之密码错误

这并不是说你不应该在浏览器端进行加密,但是如果你这么做了,一定要在服务端再次加密。在浏览器中进行哈希加密是个好想法,不过实现的时候注意下面几点:

• 客户端密码哈希并不能代替HTTPS(SSL/TLS)。如果浏览器和服务器之间的连接是不安全的,那么中间人攻击可以修改JavaScript代码,删除加密函数,从而获取用户密码。

• 客户端哈希同样需要加盐,很显然的办法就是向服务器请求用户的盐值,但是不要这么做。因为这给了坏蛋一个机会,能够在不知道密码的情况下检测用户名是否有效。既然你已经在服务端对密码进行了加盐哈希,那么在客户端把用户名(或邮箱)加上网站特有的字符串(如域名)作为盐值是可行的。

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章