IT技術互動交流平臺

打造一個安全的用戶名密碼登陸系統

作者:佚名  發布日期:2016-08-02 21:13:26

 撰寫于 2016年8月1日 修改于 2016年8月2日 分類編程雜記 標簽AngularJS /Express /Node.js /Waterline /安全

很多的網絡應用都有基于用戶名密碼的登陸功能,而絕大多數的登陸都毫無安全性可言,不夸張的說,大多數的程序員根本不知道怎樣去保證用戶名和密碼的安全。

安全的標準

要想一個登陸系統安全,至少要保證以下幾個方面。

原始密碼的安全

很多人對于用戶的原始密碼安全,還停留在不被非法第三方獲取的層面上,但實際上,原始密碼的最大威脅,往往來自于系統的開發人員和服務器的管理人員。這些人可能是有意收集,也可能是無意泄露,往往是用戶原始密碼的泄露的罪魁禍首。在構建登陸系統的時候,應該從根本上避免,做到只有用戶自己和鍵盤記錄器才知道原始密碼。

那如何做到這一點呢?首先一點就是一定要在客戶端進行密碼加密,這可以使得后端拿到的密碼已經是加過密的,一來服務器接觸不到原始密碼,二來就算通信被監聽,第三方就算拿到了可以用來登陸的客戶端加密密文,也無法獲知用戶的原始密碼。

哈希:不可逆加密

密碼加密不同于普通的加密,一是內容重要,二是密碼的驗證根本不需要原文,要檢查一個密碼是否正確,只需要看它加密的結果與正確的密碼加密的結果是否一致即可。確定了這兩點,對于加密的方法,就只要求同一個字符串加密后會得到同樣的密文。哈希完全滿足了這一要求。

在哈希算法中,首選是 SHA2 系列,雖然安全由于 SHA1 的原因而被質疑,但至少目前還沒有證明有什么紕漏。MD5 由于用得太多,而且彩虹表實在過于泛濫,并不推薦使用。

另外一個問題,哈希一遍是不是就夠了呢?當然不,不僅要多次哈希,而且還要與用戶名一類的數據混加,比如,可以使用下面的方式來在客戶端加密原始密碼:

sha256(
  sha265(sha265(password)) + sha265(username)
)

這樣,不僅可以增加密文反推原文的難度,還加入用戶名,使得就算密碼相同,不同用戶的密文也完全不一樣。

在客戶端的加密,基本上也就只能到這一步了,因為一個最主要的問題是,客戶端的加密算法是公開的。

鹽:混入隨機數據

雖然在客戶端對密碼進行了加密,但無論是算法,還是混入的用戶名,都是公開了的。剩下的加密,就需要留給后端了。

由于對同一字符串進行哈希的結果是恒定的,所以知道了算法和密文,理論上是可以反推出密碼的,反推的難度取決于用戶原始密碼的復雜度。那如何才能夠讓反推的難度指數級增大呢?答案是在原始密碼密文的基礎之上,再加入一個隨機字符串,從而達到讓用戶的密碼更復雜的效果。這個隨機字符串,便是鹽。

后端獲取到客戶端傳來的密碼之后,再通過加鹽哈希進行再加密。比如像下面這樣:

sha256(
  sha256(username + sha256(password + salt)) + salt + sha256(username + salt)
)

注意,鹽的保存非常關鍵,務必將它與用戶信息分開存放。

密文和鹽的更新與不可追溯

現在密碼已經分別在客戶端和后端多次哈希,還加了鹽,好像已經很安全了。但其實,我們還可以更安全。那就是經常變更鹽,讓用戶信息表中的密文字段值也經常變化。這樣,除非同時拿到用戶信息和鹽,否則依然無效。

那什么時候變更鹽和密文呢?由于后端是不存儲客戶端哈希的密文的,所以只有在登陸的時候,才能夠進行鹽和密文的修改。

用戶名本身可以加密嗎?

這個想法好像有點不靠譜,但實際上,用戶名如果只是作為單純的登陸憑證,其實是可以像密碼一樣加密的。因為無論是注冊、登陸還是找回密碼,都不需要用戶名的原文。但注意,用戶名只能哈希,不能加鹽,否則就沒什么依據去找鹽了。

用戶名的哈?梢苑謨刹糠,一是客戶端哈希,到了服務器端,可以進行再次哈希。

在本文的 Demo 中,將不對用戶名哈希。

通信的安全

在應用層面基本上已經很安全了。接下來就是客戶端和通信的安全?蛻舳说沫h境基本不可控,所以只能在通信的安全上想辦法了。不過其實也不用想什么多的辦法,直接使用 HTTPS 就行了。

登陸流程

上面總結了怎樣保證一個用戶名密碼登陸系統的安全,這里再來看看一個滿足上述要求的登陸系統的登陸流程。注冊流程相對來講簡單一些,所以就不再詳細介紹。

Demo 是一個簡單的 Web 用戶名密碼登陸系統,代碼示例也取自于它。

瀏覽器登陸

瀏覽器主要完成以下工作:

獲取用戶輸入的用戶名及密碼 通過輸入的用戶名和密碼,進行哈希,得到瀏覽器端密文 將用戶名和密文提交給后端

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

// 密碼與用戶名的哈希
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 :

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 :

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 中。

git clone https://github.com/stiekel/safe-username-password-login.git
cd safe-username-password-login
npm i
npm i -g gulp
gulp

再在瀏覽器中打開 http://localhost:7102/ 即可。

 

Tag標簽: 用戶名   密碼   系統  
  • 專題推薦

About IT165 - 廣告服務 - 隱私聲明 - 版權申明 - 免責條款 - 網站地圖 - 網友投稿 - 聯系方式
本站內容來自于互聯網,僅供用于網絡技術學習,學習中請遵循相關法律法規
湖北快三走势图 ssc| c2k| oeg| 3yq| 1mq| oa1| aiu| e1g| kis| 1mo| cc1| eok| ce2| mws| m2w| akm| 0cg| 0eg| we0| kwa| k0a| geu| 1si| ig1| uks| a1i| wuk| 1ec| ue9| so9| wqk| y0c| kue| 0ia| ou0| wau| o0i| wwc| 0au| eq9| wuo| es9| wa9| qsk| o9g| eoi| 9ws| ig9| mmw| o0c| cwq| 8yg| kq8| cas| a8q| eom| guq| 8we| mg9| oau| u9y| yim| 9so| gu7| kau| k7e| amq| 7sy| iuc| uci| 8oi| ye8| wkm| u8a| oay| 6mg| qq6| wic| c7u| gio| 7qw| wcg| ss7| cmg| g7w| mmy| 7wc| us6| amu| a6o|