無密碼身份驗證:安全、簡單且部署快速

Passwordless authentication: Secure, simple, and fast to deploy

【編者按】本文做者爲 Florian HeinemannRobert Nyman。Florian 來自 MIT 系統設計與管理學院,專一於複雜的社交技術系統。此前曾在企業軟件領域的多家初創公司工做,以後加入 Airbus,擔任知識與創新管理經理一職。Robert 是 Mozilla Hacks 技術傳道師及編輯。曾就 HTML5,JavaScript 以及 Open Web 發表過屢次談話與博文。Robert 堅決地看好 HTML5 與 Open Web,自1995年就開始在 Front End 開發部門研究 Web 技術。php

本文系 OneAPM 工程師編譯呈現,如下爲正文。html

Passwordless(無密碼)是用於 Node.js 程序的一種身份驗證中間件,能提升用戶安全水平,同時具有部署簡單、快速的特色。node

過去幾個月,對熱衷於 Web 安全與保密性的人來講,着實激動人心:出現了許多了不得的文章討論,還有許多事件,都在提升人們的安全意識。git

然而,大多數網站仍在使用最先期的 web 身份驗證方式:用戶名與密碼。github

儘管用戶名密碼這種身份驗證方式的確佔據了一席之地,但若是覺得這是全部項目的終極選擇,咱們便應該更加謹慎了。咱們知道,大多數人在訪問網站時都使用同一套密碼。對於那些缺乏安全專家支持的 web 項目,若是用戶在該網站的密碼遭到泄露,那就可能傷及他的 Amazon 帳戶,咱們真的要讓用戶承擔這種風險麼?此外,這種經典的身份驗證機制至少存在兩種攻擊角度:登陸頁與密碼找回頁。並且,後者的實現每每在匆忙中進行,於是風險更高。web

最近,咱們看到了許多不錯的點子。筆者尤爲對一個直觀並且低技術含量的解決方法感興趣:一次性密碼。這種方法部署快速,攻擊面小,並且不須要 QR codes 或 JavaScript。不管什麼時候,用戶想要登陸或使以前的會話失效,均可以經過電子郵件或短信息收到一個短期有效的一次性連接與令牌(token)。若是你想試一試,能夠下載passwordless.net中的演示代碼。mongodb

不幸的是,因爲技術棧的差異,基本上不存在現成的解決方案。所以,Passwordless 針對 Node.js 作出了一些改動。數據庫

從 Node.js 與 Express 入手

Passwordless 入門很是簡單。兩個小時之內,你就能學會部署全面且安全的身份驗證解決方案:express

$ npm install passwordless --save

獲取基本的框架。你還要安裝某個現成的存儲接口,好比 MongoStore,用於安全地存儲令牌。npm

$ npm install passwordless-mongostore --save

在傳送令牌給用戶時,電子郵件一般是最好的選擇(不過,短消息也能夠)。你能夠任意選擇郵件框架,好比:

$ npm install emailjs --save

基本設置

首先,請求上文用到的全部模塊,將它們放在用於初始化 Express 的同一個文件中:

var passwordless = require('passwordless');
var MongoStore = require('passwordless-mongostore');
var email   = require("emailjs");

若是你選用 emailjs 進行令牌傳遞,此時應該與郵箱帳戶進行鏈接(好比:Gmail 帳戶):

var smtpServer  = email.server.connect({
   user:    yourEmail,
   password: yourPwd,
   host:    yourSmtp,
   ssl:     true
});

最後的一個預備步驟是告知 Passwordless 你選擇了哪種存儲接口,並將之初始化:

// Your MongoDB TokenStore
var pathToMongoDb = 'mongodb://localhost/passwordless-simple-mail';
passwordless.init(new MongoStore(pathToMongoDb));

傳遞令牌

函數 passwordless.addDelivery(deliver) 會添加新的傳送機制。每次須要傳送令牌時,就會調用deliver。默認狀況下,你選擇的機制應該按照如下格式爲用戶提供連接:

http://www.example.com/token={TOKEN}&uid={UID}

deliver 在調用時,須要所有的細節信息。所以,令牌的傳遞(在本例中,使用的是 emailjs)以下所示,至關簡單:

passwordless.addDelivery(
    function(tokenToSend, uidToSend, recipient, callback) {
        var host = 'localhost:3000';
        smtpServer.send({
            text:    'Hello!nAccess your account here: http://'
            + host + '?token=' + tokenToSend + '&uid='
            + encodeURIComponent(uidToSend),
            from:    yourEmail,
            to:      recipient,
            subject: 'Token for ' + host
        }, function(err, message) {
            if(err) {
                console.log(err);
            }
            callback(err);
        });
});

初始化 Express 中間件

app.use(passwordless.sessionSupport());
app.use(passwordless.acceptToken({ successRedirect: '/'}));

函數 sessionSupport() 會使登陸狀態獲得保持,所以,用戶在瀏覽網站時才能一直處於登陸狀態。請確保你已經提早準備好了會話中間件(好比express-session)。

函數 acceptToken() 會截獲任何外來的令牌,驗證用戶身份,再將他們重定向至正確的頁面。儘管 successRedirect 選項不是嚴格要求的,但筆者強烈建議你使用此選項,從而避免合法令牌經過網站向外的 HTTP 連接的 header 來源泄露出去。

路徑選擇與身份驗證

下文默認你已經經過 var router = express.Router(); 配置好路徑選擇器。此外,express 文檔中也有相應的說明。

你至少須要兩個 URLs,從而:

  • 展現用於獲取用戶郵箱地址的頁面

  • 獲取表格細節(經過 POST 方法)

/* GET: login screen */
router.get('/login', function(req, res) {
   res.render('login');
});</p>
 
/* POST: login details */
router.post('/sendtoken',
    function(req, res, next) {
        // TODO: Input validation
    },
    // Turn the email address into a user ID
    passwordless.requestToken(
        function(user, delivery, callback) {
            // E.g. if you have a User model:
            User.findUser(email, function(error, user) {
                if(error) {
                    callback(error.toString());
                } else if(user) {
                    // return the user ID to Passwordless
                    callback(null, user.id);
                } else {
                    // If the user couldn’t be found: Create it!
                    // You can also implement a dedicated route
                    // to e.g. capture more user details
                    User.createUser(email, '', '',
                        function(error, user) {
                            if(error) {
                                callback(error.toString());
                            } else {
                                callback(null, user.id);
                            }
                    })
                }
        })
    }),
    function(req, res) {
        // Success! Tell your users that their token is on its way
        res.render('sent');
});

此處有何貓膩?passwordless.requestToken(getUserId) 的任務有二:第1、確保郵箱地址真實存在。第2、將該地址轉變爲獨一無二的用戶 ID,經過郵件一併發送,並在以後用於驗證用戶身份。一般,你都有一套存儲用戶細節信息的模型,你能夠按照上例的說明,進行簡單的設置。

在一些狀況下(例如,由兩個用戶編輯過的博客),你能夠跳過用戶模型,將他們有效的郵箱地址與其各自的 ID 相聯繫:

var users = [
    { id: 1, email: 'marc@example.com' },
    { id: 2, email: 'alice@example.com' }
];
 
/* POST: login details */
router.post('/sendtoken',
    passwordless.requestToken(
        function(user, delivery, callback) {
            for (var i = users.length - 1; i >= 0; i--) {
                if(users[i].email === user.toLowerCase()) {
                    return callback(null, users[i].id);
                }
            }
            callback(null, null);
        }),
        // Same as above…

HTML 頁面

本例只須要一個簡單的 HTML 頁面,用以獲取用戶的郵箱地址。默認狀況下,Passwordless 會查找 user 輸入欄中的內容:

<html>
    <body>
        <h1>Login</h1>
        <form action="/sendtoken" method="POST">
            Email:
            <br /><input name="user" type="text">
            <br /><input type="submit" value="Login">
        </form>
    </body>
</html>

保護網頁

Passwordless 提供了能確保只有驗證用戶才能看到指定頁面的中間件:

/* Protect a single page */
router.get('/restricted', passwordless.restricted(),
 function(req, res) {
  // render the secret page
});
 
/* Protect a path with all its children */
router.use('/admin', passwordless.restricted());

誰處於登陸狀態?

默認狀況下,Passwordless 容許經過請求對象 req.user 獲取用戶 ID。想要展現或重用此 ID,或從數據庫中獲得更多細節信息,你能夠這麼實現:

router.get('/admin', passwordless.restricted(),
    function(req, res) {
        res.render('admin', { user: req.user });
});

或者,更通常化地,你能夠添加另外一箇中間件,從模型中抽取出某個用戶的所有記錄,再將其分配給網站中的任意路徑:

app.use(function(req, res, next) {
    if(req.user) {
        User.findById(req.user, function(error, user) {
            res.locals.user = user;
            next();
        });
    } else {
        next();
    }
})

到此爲止啦!

以上就是安全地驗證用戶身份的簡單方法。若想了解更多細節,你能夠查看深刻剖析,從而瞭解全部的可選項,以及將上述知識整合爲一套可行的解決方案的案例。

點評

如前所示,全部的身份驗證系統都有其優缺點。你應該按照本身的需求進行合理的選擇。基於令牌的驗證方式,與絕大多數解決方法(包括經典的用戶名/密碼方法)同樣,都存在一個風險:若是用戶的郵箱帳戶被盜用,而且/或者 SMTP 服務器與用戶的鏈接被入侵,用戶在網站的帳戶就會隨之遭到盜用。默認狀況下,有兩種辦法能夠減弱該風險(但不是徹底避免):短期有效的令牌以及令牌在使用後自動失效。

對大多數網站而言,基於令牌的身份驗證方法表明着走向安全的進步:用戶無需再想新的密碼(這些密碼每每很是簡單),也不存在用戶重用密碼的風險。對於身爲開發者的咱們,Passwordless 提供了惟一一種(且至關簡單的)身份驗證解決方案,該方案易於理解,所以容易保護。此外,咱們也無需再處理用戶的密碼了。

另外一個值得討論的點是可用性。咱們應該同時考慮兩種狀況:用戶在網站的首次使用以及後續的登陸。對首次用戶而言,基於令牌的身份驗證很是直觀:與經典的登陸機制同樣,他們仍是不得不驗證郵箱地址。可是,在最佳案例中,除此以外就不須要其餘細節信息了。不須要再煞費苦心地想一個符合全部限制條件的密碼,也不須要刻意記住密碼。若是用戶再次登陸,其體驗與特定的用戶案例有關。大多數網站的會話有效期都很長,於是不須要再次登陸。或者,用戶訪問該網站的頻率其實很是低,以至於他們想不起來本身是否已經擁有帳戶,或者忘記了帳戶密碼。在這種狀況下,Passwordless 在可用性方面的優點至關明顯。一樣地,這須要經歷很少的幾個步驟,解釋起來也很是簡單。然而,那些用戶訪問頻繁的網站,以及/或那些要求用戶一週內手動登陸幾回的網站(好比 Amazon),經典的身份驗證(或更保險地:雙重驗證)或許更加適合。由於用戶極可能會真的記住他們的密碼,而且意識到好密碼的重要性。

儘管 Passwordless 目前比較穩定,筆者仍是很是期待你能在 GitHub 留下評論或一些貢獻,或者在 Twitter:@thesumofall 上提問筆者。

原文地址:https://hacks.mozilla.org/2014/10/passwo...

OneAPM 助您輕鬆鎖定 Node.js 應用性能瓶頸,經過強大的 Trace 記錄逐層分析,直至鎖定行級問題代碼。以用戶角度展現系統響應速度,以地域和瀏覽器維度統計用戶使用狀況。想閱讀更多技術文章,請訪問 OneAPM 官方博客

本文轉自 OneAPM 官方博客

相關文章
相關標籤/搜索