打造一个安全的用户名密码登陆系统

7 篇文章 0 订阅


很多的网络应用都有基于用户名密码的登陆功能,而绝大多数的登陆都毫无安全性可言,不夸张的说,大多数的程序员根本不知道怎样去保证用户名和密码的安全。

安全的标准

要想一个登陆系统安全,至少要保证以下几个方面。

原始密码的安全

很多人对于用户的原始密码安全,还停留在不被非法第三方获取的层面上,但实际上,原始密码的最大威胁,往往来自于系统的开发人员和服务器的管理人员。这些人可能是有意收集,也可能是无意泄露,往往是用户原始密码的泄露的罪魁祸首。在构建登陆系统的时候,应该从根本上避免,做到只有用户自己和键盘记录器才知道原始密码。

那如何做到这一点呢?首先一点就是一定要在客户端进行密码加密,这可以使得后端拿到的密码已经是加过密的,一来服务器接触不到原始密码,二来就算通信被监听,第三方就算拿到了可以用来登陆的客户端加密密文,也无法获知用户的原始密码。

哈希:不可逆加密

密码加密不同于普通的加密,一是内容重要,二是密码的验证根本不需要原文,要检查一个密码是否正确,只需要看它加密的结果与正确的密码加密的结果是否一致即可。确定了这两点,对于加密的方法,就只要求同一个字符串加密后会得到同样的密文。哈希完全满足了这一要求。

在哈希算法中,首选是 SHA2 系列,虽然安全由于 SHA1 的原因而被质疑,但至少目前还没有证明有什么纰漏。MD5 由于用得太多,而且彩虹表实在过于泛滥,并不推荐使用。

另外一个问题,哈希一遍是不是就够了呢?当然不,不仅要多次哈希,而且还要与用户名一类的数据混加,比如,可以使用下面的方式来在客户端加密原始密码:

1
2
3
sha256(
  sha265(sha265(password)) + sha265(username)
)

这样,不仅可以增加密文反推原文的难度,还加入用户名,使得就算密码相同,不同用户的密文也完全不一样。

在客户端的加密,基本上也就只能到这一步了,因为一个最主要的问题是,客户端的加密算法是公开的。

盐:混入随机数据

虽然在客户端对密码进行了加密,但无论是算法,还是混入的用户名,都是公开了的。剩下的加密,就需要留给后端了。

由于对同一字符串进行哈希的结果是恒定的,所以知道了算法和密文,理论上是可以反推出密码的,反推的难度取决于用户原始密码的复杂度。那如何才能够让反推的难度指数级增大呢?答案是在原始密码密文的基础之上,再加入一个随机字符串,从而达到让用户的密码更复杂的效果。这个随机字符串,便是盐。

后端获取到客户端传来的密码之后,再通过加盐哈希进行再加密。比如像下面这样:

1
2
3
sha256(
  sha256(username + sha256(password + salt)) + salt + sha256(username + salt)
)

注意,盐的保存非常关键,务必将它与用户信息分开存放。

密文和盐的更新与不可追溯

现在密码已经分别在客户端和后端多次哈希,还加了盐,好像已经很安全了。但其实,我们还可以更安全。那就是经常变更盐,让用户信息表中的密文字段值也经常变化。这样,除非同时拿到用户信息和盐,否则依然无效。

那什么时候变更盐和密文呢?由于后端是不存储客户端哈希的密文的,所以只有在登陆的时候,才能够进行盐和密文的修改。

用户名本身可以加密吗?

这个想法好像有点不靠谱,但实际上,用户名如果只是作为单纯的登陆凭证,其实是可以像密码一样加密的。因为无论是注册、登陆还是找回密码,都不需要用户名的原文。但注意,用户名只能哈希,不能加盐,否则就没什么依据去找盐了。

用户名的哈希可以分两部分,一是客户端哈希,到了服务器端,可以进行再次哈希。

在本文的 Demo 中,将不对用户名哈希。

通信的安全

在应用层面基本上已经很安全了。接下来就是客户端和通信的安全。客户端的环境基本不可控,所以只能在通信的安全上想办法了。不过其实也不用想什么多的办法,直接使用 HTTPS 就行了。

登陆流程

上面总结了怎样保证一个用户名密码登陆系统的安全,这里再来看看一个满足上述要求的登陆系统的登陆流程。注册流程相对来讲简单一些,所以就不再详细介绍。

Demo 是一个简单的 Web 用户名密码登陆系统,代码示例也取自于它。

浏览器登陆

浏览器主要完成以下工作:

  • 获取用户输入的用户名及密码
  • 通过输入的用户名和密码,进行哈希,得到浏览器端密文
  • 将用户名和密文提交给后端

主要代码如下,取自 client/app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 密码与用户名的哈希
function encryptPwd(username, password) {
  username = username.toLowerCase();
  return sha256(
    username + sha256 (
      sha256(sha256(sha256(password))) + sha256(username)
    )
  );
}

$scope.login = function(){
  // 检查用户名和密码的合法性,比如是否输入,长度是否足够等
  if($scope.check()) {
    return;
  }
  $scope.successMessage = '';
  $scope.errorMessage = '';
  $scope.status = 'loading';
  // 向后端提交登陆请求
  $resource('/user/login')
  .save({
    username: $scope.username,
    password: encryptPwd($scope.username, $scope.password)
  }, function(res){
    $scope.status = 'done';
    $scope.successMessage = 'login successful!';
  }, function(reason){
    $scope.status = 'done';
    $scope.errorMessage = reason.data || 'failed';
  });
};

后端密码验证

后端的验证流程如下:

  • 获取前端提交的用户名及浏览器端密文
  • 根据用户名,在数据库中查询出对应的盐 id
  • 通过盐 id 取出对应的盐,再通过用户名、浏览器端密文和盐算出后端密文
  • 根据用户名和后端密文到用户表查询,如果有结果,则表明登陆信息正确,返回给浏览器登陆成功的响应
  • 生成新的盐,算出新的后端密文,并将两者更新到数据库中

实现的代码如下,取自 app/controllers/user.server.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function encryptPwd(usr, pwd, salt){
  usr = usr.toLowerCase();
  return sha256(
    sha256(usr + sha256(pwd + salt)) + salt + sha256(usr + salt)
  )
}

function login(req, res, next){
  // 用户名密码获取和检查已省略
  // 根据用户名,获取盐 id
  req.models.user
  .findOne({select:['username', 'saltId'], where: {username: username}})
  .exec(function(err, userDoc){
    if(err) return next(err);
    if(!userDoc) return next(new Error('username not exists'));

    // 取盐
    req.models.salt
    .findOne({id: userDoc.saltId})
    .exec(function(err, saltDoc){
      if(err) return next(err);
      if(!saltDoc) return next(new Error('can NOT find salt'));

      // 根据用户名、密码和盐推算出密文
      var pwdHash = encryptPwd(username, password, saltDoc.salt);
      // 在数据库中核对用户名和密文
      req.models.user
      .findOne({select: ['id'], where: {username: username, password: pwdHash }})
      .exec(function(err, doc){
        if(err) return next(err);
        if(!doc) return next(new Error('password error'));

        res.json({
          username: username
        });

        return updateSalt(saltDoc, userDoc, password, next);
      });
    });
  });
}

盐与密文的更新

前面返回给用户成功登陆的响应之后,调用了更新盐和密文的方法,该方法具体流程如下:

  • 生成并存储新盐
  • 根据新盐、用户名和浏览器端密文,生成新的后端密文
  • 存储后端密文到用户信息表

实现如下,取自 app/controllers/user.server.controller.js

1
2
3
4
5
6
7
8
9
10
11
function updateSalt(saltDoc, userDoc, passwordInputed, next){
  saltDoc.salt = Math.random().toString(15).substr(3, 27);
  saltDoc.save(function(err){
    if(err) return next(err);
    userDoc.password = encryptPwd(userDoc.username, passwordInputed, saltDoc.salt);
    userDoc.save(function(err){
      if(err) return next(err);
      return next();
    });
  });
}

Demo

Demo 托管在 Github 上。前端采用 AngularJS + Bootstrap ,后端使用 Node.js + Express + MongoDB ,是一个典型的 MEAN 应用 。

数据存储这块,使用了 Waterline 这个 ORM 中间件(以前也曾经写过两篇介绍文章,可供参考:Node.js ORM 数据操作中间件 Waterline在 Express 项目中使用 Waterline)。使用它的目的主要是为了将用户信息和盐存储到不同的地方。本例中将盐用 sails-disk 存储到了文件中,用户信息用 sails-mongo 存储到了 MongoDB 中。

1
2
3
4
5
git clone https://github.com/stiekel/safe-username-password-login.git
cd safe-username-password-login
npm i
npm i -g gulp
gulp
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java是一种功能强大的编程语言,它可以用于开发各种类型的应用程序,包括网络销售系统。要使用Java开发一个网络销售系统并连接数据库,可以按照以下步骤进行: 1.确定系统需求:首先,需要明确网络销售系统的功能和要求,包括用户注册、商品展示、购物车管理、订单处理等。 2.设计数据库结构:根据系统需求,设计合适的数据库模式。可以使用MySQL、Oracle或其他关系型数据库来存储和管理数据,例如创建用户表、商品表、订单表等。 3.建立数据库连接:使用Java提供的数据库连接API,如JDBC,建立与数据库的连接。这可以通过加载驱动程序、创建连接对象和使用连接对象来执行SQL语句实现。 4.编写Java代码:根据系统需求和数据库结构编写Java代码。可以使用面向对象的思想,创建适当的实体类,如用户类、商品类、订单类等,并为它们定义相应的属性和方法。 5.实现系统功能:根据系统需求,实现各个功能模块的代码。例如,用户注册功能可以实现用户信息的输入、保存到数据库的操作;商品展示功能可以查询数据库中的商品信息并展示给用户等。 6.连接数据库:编写代码来执行数据库操作,如查询、插入、更新和删除数据。可以使用SQL语句来对数据库进行操作,并通过Java代码调用执行。 7.测试和调试:完成系统开发后,进行测试和调试,确保系统能够正常运行并与数据库正确连接。 总结:通过以上步骤,可以使用Java开发一个网络销售系统,并连接数据库来存储和管理数据。这样的系统将可以实现用户注册、商品展示、购物车管理、订单处理等功能,为用户提供便捷的网络购物体验。 ### 回答2: Java可以用来开发一个网络销售系统,并且能够连接数据库。首先,我们可以使用Java的Servlet和JSP技术来实现网络销售系统的前端与后端交互。Servlet可以作为服务器端的处理程序,负责接收和处理客户端发送的请求,并将结果返回给客户端。JSP可以用来编写网页模板,将动态内容和静态内容进行结合,使得页面更加灵活和动态。 在数据库方面,Java可以使用JDBC(Java数据库连接)技术来连接数据库,JDBC提供了一种标准的数据库操作接口,使得开发人员可以使用统一的方式访问不同的数据库。我们可以使用Java提供的JDBC驱动程序来连接和操作数据库。 具体实现网络销售系统时,可以使用数据库来存储商品信息、订单信息和用户信息等。通过使用Java的JDBC接口,我们可以使用SQL语句来执行数据库操作,例如插入、更新、删除和查询等。通过执行相应的SQL语句,我们可以实现向数据库中添加商品信息、创建订单、查询订单状态以及验证用户身份等功能。 在连接数据库之前,我们需要在Java项目中引入合适的数据库驱动程序,并配置数据库相关信息,例如数据库的URL、用户名密码等。连接数据库时,我们可以创建数据库连接对象,并使用该对象来执行SQL语句,获取结果集或者进行事务管理等操作。 总而言之,通过使用Java开发网络销售系统,并连接数据库,我们可以实现前后端的交互和数据的持久化,为用户提供丰富的购物体验和高效的订单处理。通过灵活的JSP模板和数据库的支持,开发者可以根据具体需求实现各种复杂的业务逻辑,从而打造一个完善的网络销售系统。 ### 回答3: Java可以很轻松地开发一个网络销售系统,并与数据库进行连接。首先,我们需要确保已经安装了Java开发环境(JDK)和相应的集成开发环境(IDE)。 接下来,我们可以使用Java提供的各种网络库,如Java HTTP库或Java Servlet库,来处理与网络通信的部分。通过这些库,我们可以创建网站页面,处理用户的请求和响应,并与数据库进行交互。 为了连接到数据库,我们需要使用Java的数据库连接API,如JDBC(Java 数据库连接)。这个API提供了一系列类和方法,用于连接到数据库服务器,执行SQL查询和更新,并将结果返回给用户。 在系统开发的过程中,我们可以定义各种Java类来表示系统的不同组件,如用户、产品、订单等。通过这些类,我们可以实现登录、注册、浏览商品、下订单等功能。 为了保证系统的可靠性和安全性,我们可以使用Java的异常处理机制来处理潜在的错误和异常情况。此外,我们还可以使用Java的加密库来加密用户的敏感信息,如密码和支付信息。 最后,在系统开发完成后,我们可以将Java代码编译为可执行的二进制文件,并将其部署到一个Web服务器上,以便用户可以通过浏览器来访问并使用这个网络销售系统。 总之,Java是一个功能强大且易于学习的编程语言,可以用于开发各种类型的应用程序,包括网络销售系统。通过合适的库和API,我们可以轻松创建一个功能完善的系统,并与数据库进行连接来存储和检索数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值