Node.js開發入門—使用cookie保持登錄


這次來做一個站點登錄的小樣例,后面會用到。

這個演示樣例會用到Cookie、HTML表單、POST數據體(body)解析。

第一個版本號,我們的用戶數據就寫死在js文件中。

第二個版本號會引入MongoDB來保存用戶數據。

演示樣例准備

1. 使用express創建應用

就以下的命令序列:

express LoginDemo
cd LoginDemo
npm install

2. 登錄頁面

登錄頁面的jade模板為login.jade,內容例如以下:

doctype html
html
  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}

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;
}

請把login.css放在public/stylesheets文件夾下。

3. profile頁面

登錄成功后會顯示配置頁面。profile.jade頁面內容:

doctype html
html
  head
    meta(charset='UTF-8')
    title= title
  body
    p #{msg}
    p #{lastTime}
    p 
      a(href='/logout') 退出

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 setup
app.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 handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (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 user
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: {}
  });
});


module.exports = app;

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 really
var 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;

如你所見,我內置了兩個賬號。admin和foruok,登錄時就驗證這兩個賬號,不正確就報錯。

好了,運行“npm start”,然后在瀏覽器里打開“http://localhost:3000”,能夠看到以下的效果:

view-logindemo.png

折騰幾次,登錄,退出。再次登錄,效果例如以下:

view-logindemo-profile.png

好啦,這就是這個演示樣例的效果。

接下來我們來解釋一下用到概念和部分代碼。

處理POST正文數據

我們在演示樣例中使用了HTML表單來接收username與password。當input元素的類型為submit時。點擊它,瀏覽器會把表單內的數據按一定的格式組織之后編碼進body,POST到指定的server地址。

username與password,在server端,能夠通過HTML元素的名字屬性的值找出來。

server解析表單數據這一過程,我們不用操心,用了express的body-parser中間件。它會幫我們做這件事。僅僅要做簡單的配置就可以。

並且這些配置代碼,express generator都幫我們完畢了,例如以下:

//載入body-parser模塊
var bodyParser = require('body-parser');
...
//應用中間件
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

我們處理/login路徑上的POST請求的代碼在users.js里,從“router.post(‘/login’…”開始(94行,要是markdown能自己主動給代碼插入行號就好了)。

引用登錄表單內的用戶名的代碼例如以下:

var userName = req.body.login_username;

注意到了吧,express.Request對象req內有解析好的body,我們使用login_username來訪問用戶名。而login_username就是我們在HTML里的input元素的name屬性的值。就這么關聯的。password也相似。

cookie

cookie,按我的理解。就是server發給瀏覽器的一張門票。要訪問server內容,能夠憑票入場,享受某種服務。server能夠在門票上記錄一些信息。從技術角度講,想記啥記啥。當瀏覽器訪問server時,HTTP頭部把cookie信息帶到server。server解析出來。校驗當時記錄在cookie里的信息。

HTTP協議本身是無狀態的,而應用server往往想保存一些狀態,cookie應運而生。由server頒發。通過HTTP頭部傳給瀏覽器。瀏覽器保存到本地。興許訪問server時再通過HTTP頭部傳遞給server。這樣的交互,server就能夠在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());

express的Response對象有一個cookie方法,能夠回寫給瀏覽器一個cookie。

以下的代碼發送了一個名字叫做“account”的cookie,這個cookie的值是一個對象,對象內有三個屬性。

res.cookie("account", {account: userName, hash: hash, last: lastTime}, {maxAge: 60000});

res.cookie()方法原型例如以下:

res.cookie(name, value [, options])

文檔在這里:http://expressjs.com/4x/api.html#res.cookie

瀏覽器會解析HTTP頭部里的cookie,依據過期時間決定保存策略。當再次訪問server時,瀏覽器會把cookie帶給server。

server使用cookieParser解析后保存在Request對象的cookies屬性里。req.cookies本身是一個對象。解析出來的cookie,會被關聯到req.cookies的以cookie名字命名的屬性上。比方演示樣例給cookie起的名字叫account,服務端解析出的cookie,就能夠通過req.cookies.account來訪問。注意req.cookies.account本身既可能是簡單的值也可能是一個對象。在演示樣例中通過res.cookie()發送的名為account的cookie,它的值是一個對象,在這樣的情況下,server這邊從HTTP請求中解析出的cookie也會被組裝成一個對象。所以我們通過req.cookies.account.account就能夠拿到瀏覽器通過cookie發過來的用戶名。但假設瀏覽器沒有發送名為“account”的cookie,那req.cookies.account.hash這樣的訪問就會拋異常。所以我在代碼里使用req.cookies[“account”]這樣的方式來檢測是否有account這個cookie。

持久登錄

假設用戶每次訪問一個須要鑒權的頁面都要輸入username與password來登錄。那就太麻煩了。所以,非常多現代的站點都實現了持久登錄。我的演示樣例使用cookie簡單實現了持久登錄。

在處理/login路徑上的POST請求時。假設登錄成功。就把用戶名、一個hash值、還有上次登錄時間保存在cookie里,並且設置cookie的有效期為60秒。這樣在60秒有效期內。瀏覽器興許的訪問就會帶cookie,服務端代碼從cookie里驗證用戶名和hash值,讓用戶保持登錄狀態。當過了60秒。瀏覽器就不再發送cookie,服務端就覺得須要又一次登錄。將用戶重定向到login頁面。

如今服務端的用戶信息就簡單的放在js代碼里了。非常丑陋,下次我們引入MongoDB,把用戶信息放在數據庫里。


其他文章:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM