Node js開發入門 使用cookie保持登入
這次來做一個網站登入的小例子,後面會用到。這個示例會用到Cookie、HTML表單、POST資料體(body)解析。
第一個版本,我們的使用者資料就寫死在js檔案裡。第二個版本會引入MongoDB來儲存使用者資料。
示例準備
1. 使用express建立應用
就下面的命令序列:
express LoginDemocd LoginDemonpm install
- 1
- 2
- 3
2. 登入頁面
登入頁面的jade模板為login.jade,內容如下:
doctype htmlhtml head meta(charset='UTF-8') title 登入 link(rel='stylesheet', href='/stylesheets/login.css') body .form-container p.form-header 登入 form(action='login', method='POST', align='center') table tr td label(for='user') 賬號: td input#user(type='text', name='login_username') tr td label(for='pwd') 密碼: td input#pwd(type='password', name='login_password') tr td(colspan='2', align='right') input(type='submit', value='登入') p #{msg}
- 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
login.jade放在views目錄下。我在login.jade裡硬編碼了漢字,注意檔案用UTF-8編碼。
這個模板的最後是一條動態訊息,用於顯示登入錯誤資訊,msg變數由應用程式傳入。
我給login頁面寫了個簡單的CSS,login.css檔案,內容如下:
form { margin: 12px;}a { color: #00B7FF;}div.form-container { display: inline-block; border: 6px solid steelblue; width: 280px; border-radius: 10px; margin: 12px;}p.form-header { margin: 0px; font: 24px bold; color: white; background: steelblue; text-align: center;}input[type=submit]{ font: 18px bold; width: 120px; margin-left: 12px;}
- 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
請把login.css放在public/stylesheets目錄下。
3. profile頁面
登入成功後會顯示配置頁面,profile.jade頁面內容:
doctype htmlhtml head meta(charset='UTF-8') title= title body p #{msg} p #{lastTime} p a(href='/logout') 退出
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
profile.jade放在views目錄下。profile頁面顯示一條登入成功的訊息,還顯示上次登入時間,最後提供了一個退出連結。
4. app.js改動
我改動了app.js,以便使用者在沒有登入時訪問網站自動跳轉到login頁面。新的app.js內容如下:
var express = require('express');var path = require('path');var favicon = require('serve-favicon');var logger = require('morgan');var cookieParser = require('cookie-parser');var bodyParser = require('body-parser');var users = require('./routes/users');var app = express();// view engine setupapp.set('views', path.join(__dirname, 'views'));app.set('view engine', 'jade');// uncomment after placing your favicon in /public//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));app.use(logger('dev'));app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));app.use(cookieParser());app.use(express.static(path.join(__dirname, 'public')));app.all('*', users.requireAuthentication);app.use('/', users);// catch 404 and forward to error handlerapp.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err);});// error handlers// development error handler// will print stacktraceif (app.get('env') === 'development') { app.use(function(err, req, res, next) { res.status(err.status || 500); res.render('error', { message: err.message, error: err }); });}// production error handler// no stacktraces leaked to userapp.use(function(err, req, res, next) { res.status(err.status || 500); res.render('error', { message: err.message, error: {} });});module.exports = app;
- 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
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
5. users.js
我修改了users.js,把認證、登入、登出等邏輯放在裡面,首先要把users.js轉為UTF-8編碼(sorry,硬編碼了漢字哈)。內容:
var express = require('express');var router = express.Router();var crypto = require('crypto');function hashPW(userName, pwd){ var hash = crypto.createHash('md5'); hash.update(userName + pwd); return hash.digest('hex');}// just for tutorial, it's bad reallyvar userdb = [ { userName: "admin", hash: hashPW("admin", "123456"), last: "" }, { userName: "foruok", hash: hashPW("foruok", "888888"), last: "" } ];function getLastLoginTime(userName){ for(var i = 0; i < userdb.length; ++i){ var user = userdb[i]; if(userName === user.userName){ return user.last; } } return "";}function updateLastLoginTime(userName){ for(var i = 0; i < userdb.length; ++i){ var user = userdb[i]; if(userName === user.userName){ user.last = Date().toString(); return; } }}function authenticate(userName, hash){ for(var i = 0; i < userdb.length; ++i){ var user = userdb[i]; if(userName === user.userName){ if(hash === user.hash){ return 0; }else{ return 1; } } } return 2;}function isLogined(req){ if(req.cookies["account"] != null){ var account = req.cookies["account"]; var user = account.account; var hash = account.hash; if(authenticate(user, hash)==0){ console.log(req.cookies.account.account + " had logined."); return true; } } return false;};router.requireAuthentication = function(req, res, next){ if(req.path == "/login"){ next(); return; } if(req.cookies["account"] != null){ var account = req.cookies["account"]; var user = account.account; var hash = account.hash; if(authenticate(user, hash)==0){ console.log(req.cookies.account.account + " had logined."); next(); return; } } console.log("not login, redirect to /login"); res.redirect('/login?'+Date.now());};router.post('/login', function(req, res, next){ var userName = req.body.login_username; var hash = hashPW(userName, req.body.login_password); console.log("login_username - " + userName + " password - " + req.body.login_password + " hash - " + hash); switch(authenticate(userName, hash)){ case 0: //success var lastTime = getLastLoginTime(userName); updateLastLoginTime(userName); console.log("login ok, last - " + lastTime); res.cookie("account", {account: userName, hash: hash, last: lastTime}, {maxAge: 60000}); res.redirect('/profile?'+Date.now()); console.log("after redirect"); break; case 1: //password error console.log("password error"); res.render('login', {msg:"密碼錯誤"}); break; case 2: //user not found console.log("user not found"); res.render('login', {msg:"使用者名稱不存在"}); break; }});router.get('/login', function(req, res, next){ console.log("cookies:"); console.log(req.cookies); if(isLogined(req)){ res.redirect('/profile?'+Date.now()); }else{ res.render('login'); }});router.get('/logout', function(req, res, next){ res.clearCookie("account"); res.redirect('/login?'+Date.now());});router.get('/profile', function(req, res, next){ res.render('profile',{ msg:"您登入為:"+req.cookies["account"].account, title:"登入成功", lastTime:"上次登入:"+req.cookies["account"].last });});module.exports = router;
- 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
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
如你所見,我內建了兩個賬號,admin和foruok,登入時就驗證這兩個賬號,不對就報錯。
好了,執行“npm start”,然後在瀏覽器裡開啟“http://localhost:3000”,可以看到下面的效果:
折騰幾次,登入,退出,再次登入,效果如下:
好啦,這就是這個示例的效果。接下來我們來解釋一下用到概念和部分程式碼。
處理POST正文資料
我們在示例中使用了HTML表單來接收使用者名稱和密碼,當input元素的型別為submit時,點選它,瀏覽器會把表單內的資料按一定的格式組織之後編碼進body,POST到指定的伺服器地址。使用者名稱和密碼,在伺服器端,可以通過HTML元素的名字屬性的值找出來。
伺服器解析表單資料這一過程,我們不用擔心,用了express的body-parser中介軟體,它會幫我們做這件事,只要做簡單的配置即可。而且這些配置程式碼,express generator都幫我們完成了,如下:
//載入body-parser模組var bodyParser = require('body-parser');...//應用中介軟體app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));
- 1
- 2
- 3
- 4
- 5
- 6
我們處理/login路徑上的POST請求的程式碼在users.js裡,從“router.post(‘/login’…”開始(94行,要是markdown能自動給程式碼插入行號就好了)。引用登入表單內的使用者名稱的程式碼如下:
var userName = req.body.login_username;
- 1
注意到了吧,express.Request物件req內有解析好的body,我們使用login_username來訪問使用者名稱。而login_username就是我們在HTML裡的input元素的name屬性的值。就這麼關聯的。password也類似。
cookie
cookie,按我的理解,就是伺服器發給瀏覽器的一張門票,要訪問伺服器內容,可以憑票入場,享受某種服務。伺服器可以在門票上記錄一些資訊,從技術角度講,想記啥記啥。當瀏覽器訪問伺服器時,HTTP頭部把cookie資訊帶到伺服器,伺服器解析出來,校驗當時記錄在cookie裡的資訊。
HTTP協議本身是無狀態的,而應用伺服器往往想儲存一些狀態,cookie應運而生,由伺服器頒發,通過HTTP頭部傳給瀏覽器,瀏覽器儲存到本地。後續訪問伺服器時再通過HTTP頭部傳遞給伺服器。這樣的互動,伺服器就可以在cookie裡記錄一些使用者相關的資訊,比如是否登入了,賬號了等等,然後就可以根據這些資訊做一些動作,比如我們示例中的持久登入的實現,就利用了cookie。還有一些電子商務網站,實現購物車時也可能用到cookie。
cookie儲存的是一些key-value對。在express裡,Request和Response都有cookie相關的方法。Request例項req的cookies屬性,儲存瞭解析出的cookie,如果瀏覽器沒傳送cookie,那這個cookies物件就是一個空物件。
express有個外掛,cookie-parser,可以幫助我們解析cookie。express生成的app.js已經自動為我們配置好了。相關程式碼:
var cookieParser = require('cookie-parser');...app.use(cookieParser());
- 1
- 2
- 3
express的Response物件有一個cookie方法,可以回寫給瀏覽器一個cookie。
下面的程式碼傳送了一個名字叫做“account”的cookie,這個cookie的值是一個物件,物件內有三個屬性。
res.cookie("account", {account: userName, hash: hash, last: lastTime}, {maxAge: 60000});
- 1
res.cookie()方法原型如下:
res.cookie(name, value [, options])
- 1
瀏覽器會解析HTTP頭部裡的cookie,根據過期時間決定儲存策略。當再次訪問伺服器時,瀏覽器會把cookie帶給伺服器。伺服器使用cookieParser解析後儲存在Request物件的cookies屬性裡,req.cookies本身是一個物件,解析出來的cookie,會被關聯到req.cookies的以cookie名字命名的屬性上。比如示例給cookie起的名字叫account,服務端解析出的cookie,就可以通過req.cookies.account來訪問。注意req.cookies.account本身既可能是簡單的值也可能是一個物件。在示例中通過res.cookie()傳送的名為account的cookie,它的值是一個物件,在這種情況下,伺服器這邊從HTTP請求中解析出的cookie也會被組裝成一個物件,所以我們通過req.cookies.account.account就可以拿到瀏覽器通過cookie發過來的使用者名稱。但如果瀏覽器沒有傳送名為“account”的cookie,那req.cookies.account.hash這種訪問就會拋異常,所以我在程式碼裡使用req.cookies[“account”]這種方式來檢測是否有account這個cookie。
持久登入
如果使用者每次訪問一個需要鑑權的頁面都要輸入使用者名稱和密碼來登入,那就太麻煩了。所以,很多現代的網站都實現了持久登入。我的示例使用cookie簡單實現了持久登入。
在處理/login路徑上的POST請求時,如果登入成功,就把使用者名稱、一個hash值、還有上次登入時間儲存在cookie裡,並且設定cookie的有效期為60秒。這樣在60秒有效期內,瀏覽器後續的訪問就會帶cookie,服務端程式碼從cookie裡驗證使用者名稱和hash值,讓使用者保持登入狀態。當過了60秒,瀏覽器就不再發送cookie,服務端就認為需要重新登入,將使用者重定向到login頁面。
現在服務端的使用者資訊就簡單的放在js程式碼裡了,非常醜陋,下次我們引入MongoDB,把使用者資訊放在資料庫裡。
其它文章: