Passport.js 之 Hello 你好嗎 ~
nodejs
Lastmod: 2019-12-15

本篇文章中,我們想要知道以下的重點 :

  1. passport 是啥鬼 ?
  2. 要如何使用它呢 ?
  3. 要如何使用一個 passport 的登入系統呢 ?

passport 是啥 ?

passport.js是 node 中的一段登入驗證中間層(middleware),也就是說可以讓你簡單的使用 google 登入使用 fb 登入,它的架構就是所謂的策略模式,接下來我們來實際上看看他是如何使用的。

passort.js 活著的目的就是為了驗證 request

要使用 passport 來進行驗證,需要設定三個東西 :

  • 驗證策略 (Authentication strategies)
  • 應用程式的中間件 (Application middleware)
  • Sessions (可選擇)

驗證策略的建立

上面我們有提到 passport 本身就是使用策略模式的實作,而它的定義就是 :

定義一系列的演算法,把它們一個個封裝起來,並且可以相互替換。

所以在這邊,我們需要定義驗證的策略(演算法),例如使用 facebook 登入驗證、google 登入驗證或自訂的驗證策略。

而我們這裡直接看官網的自訂驗證策略localStrategy,下面的程式碼中,我們會定義一個localStrategy,它準備用來驗證我們的request

LocalStrategy的兩個參數為optionsverify,我們option需要先定義要用來驗證的欄位usernamepassowrd,然後verify就是驗證規則,就是下面那個function裡面的東東。

var users = {
    zack: {
        username: 'zack',
        password: '1234',
        id: 1,
    },
    node: {
        username: 'node',
        password: '5678',
        id: 2,
    },
}

// LacalStrategy(options,verify)
var localStrategy = new LocalStrategy({
    usernameField: 'username',
    passwordField: 'password',
},
    function (username, password, done) {
        user = users[username];

        if (user == null) {
            return done(null, false, { message: 'Invalid user' });
        };

        if (user.password !== password) {
            return done(null, false, { message: 'Invalid password' });
        };

        done(null, user);
    }
)

###那這邊有個問題 ~ 那就是奇怪,為什麼他沒有驗證 username 或 password 這兩個欄位是否合法呢 ?

因為LocalStrategy已經幫我們處理好了,我們直接來看一下它的原始碼 :

Strategy.prototype.authenticate = function(req, options) {
  options = options || {};
  var username = lookup(req.body, this._usernameField) || lookup(req.query, this._usernameField);
  var password = lookup(req.body, this._passwordField) || lookup(req.query, this._passwordField);
  
  if (!username || !password) {
    return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400);
  }
  
  var self = this;
  
  function verified(err, user, info) {
    if (err) { return self.error(err); }
    if (!user) { return self.fail(info); }
    self.success(user, info);
  }
  
  try {
    if (self._passReqToCallback) {
      this._verify(req, username, password, verified);
    } else {
      this._verify(username, password, verified);
    }
  } catch (ex) {
    return self.error(ex);
  }
};

上面這一段是當我們執行了下面這段時程式碼時,就會執行的東東,從上面程式碼中我們可以知道,它會先去req.body中,尋找我們定義的兩個欄位usernamepassowrd,然後檢查看看他是否合法,當一切都 ok 時,我們就會執行上面有提到的verify函數,來進行驗證。

Passport.authenticate('local', { session: false })

中間件的設定

接下來我們將要在route中,增加 passport 這個中間件 (middleware),我們這邊選擇使用 express 來當我們的 web framework。

我們在使用時需要先選擇我們要使用的策略,我們直接用上面所建立的localStrategy,如果你有建立其它的例如facebookgoogle的策略也都可以使用。

// 註冊策略
Passport.use('local', localStrategy);

var app = Express();
app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json());
app.use(Passport.initialize());

其中Passport.use('local', localStrategy);這行就是將我們剛剛建立的策略註冊到 passport 中,我們直接看他的原始碼,會更了解它在做啥 :

嗯他非常的簡單,就是將我們的註冊的策略丟到一個物件中。

Authenticator.prototype.use = function(name, strategy) {
  if (!strategy) {
    strategy = name;
    name = strategy.name;
  }
  if (!name) { throw new Error('Authentication strategies must have a name'); }
  
  this._strategies[name] = strategy;
  return this;
};

然後呢我們就要在 route 上加 passport 中間件,這樣的話,我們每一個進來到這個 route 的 request 都會被 passport 我們指定的策略進行驗證。

app.post(
    '/login',
    Passport.authenticate('local', { session: false }),
    function (req, res) {
        res.send('User ID ' + req.user.id.toString());
    }
);

Session的設定

我們在驗證完畢後應該是會取得到某個使用者,像我們上面範例中的這行 :

user = users[username];

當然,這只是範例,正常情況下應該是去 db 或其它地方取得使用者,但我們這裡就一切從簡。

接下來我們兩個問題 ~

雖然我還算理解,但是這邊還是簡單的說一下。

首先這兩個都是個儲放機制。

再來 session 是只能在 server 進行維持,每當 client 在連接 server 時,會由 server 產生成一個唯一的 sessionId,並用它來連接 server 內的 session 存放空間,而通常來說 sessionId,也同時會保存在客戶端的 cookie 中,每次 client 在訪問 server 時都會用它來存取 session 資料。

而 cookie 則是在客戶端的儲放機制,它是由瀏覽器來維持,但注意,它可以在 client 端與 server 端進行修改,為什麼會有 cookie 呢 ? 主要就是因為 http 是無狀態的協議,每一次讀取頁面時,都是獨立的狀態,所以就需要使用 cookie 來連結前後文。

對了 cookie 還有一點要記,那就是每一次的請求,cookie 都會一起被發送到 server 端喔。

我們懂了以下的基本知道後,再來問個問題。

我們每一次登入,就要取資料庫驗證和取得一次使用者嗎 ????

答案是否定的。

正常不太會這樣處理,假設我們是用 fb 登入,那不就變成,每一次使用者到這頁面時,畫面都會掉到 fb 要你登入,然後在跳回來原本頁面,這樣太浪費時間了。

所以說,passport 它會做以下兩件事情 :

  • 將使用者資訊存放在 server 的 session 中。
  • 然後會在使用者的瀏覽器設定 cookie。

那我們在 passport 要如何使用呢 ? 首先關於第一點,passport 提供了serializeUser讓我們將使用者資訊存放置 server 的 session中。

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

然後關於第二點,每一次進行請求時,passport 都會將傳進來的 cookie 中某個存放該session資訊的欄位,取得到我們剛剛存的user.id,然後在使用它,來取得完整的user資訊,並將它存放到req.user之中。

passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
});

實作一個登入系統

我們這邊來實作個登入系統,使用者只要有登入後,接下來的 route 都可以從 session 中取得到該名使用者的資訊。

所有的程式碼

1. app.js 基本的註冊

var Passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var Express = require('express');
var BodyParser = require('body-parser');
var session = require('express-session');
var cookieParser = require('cookie-parser');

var app = Express();
app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json());
app.use(cookieParser());

app.use(session({
    secret: "test",
    resave: false,
    saveUninitialized: false,
}))
app.use(Passport.initialize());
app.use(Passport.session()); // 一定要在 initialize 之後

2. 驗證策略

var users = {
    mark: {
        username: 'mark',
        password: '1234',
        id: 1,
    },
    node: {
        username: 'node',
        password: '5678',
        id: 2,
    },
}

var localStrategy = new LocalStrategy({
    usernameField: 'username',
    passwordField: 'password',
},
    function (username, password, done) {
        user = users[username];

        if (user == null) {
            return done(null, false, { message: 'Invalid user' });
        };

        if (user.password !== password) {
            return done(null, false, { message: 'Invalid password' });
        };

        done(null, user);
    }
)

Passport.use('local', localStrategy);

3. 建立登入的route

下面為我們登入的 route 建立。

app.post(
    '/login',
    Passport.authenticate('local',{session: true}),
    function (req, res) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "X-Requested-With");
        res.send('User ID ' + req.user.id.toString());
    }
);

那 passport 是那裡置至 session 和 cookie 呢 ?

答案是在這裡 :

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

然後我們來呼叫這個 route,然後你到 chrome 的 application 看,你會發現,他有存放個 cookie 。

4. 建立取得使用者的 route

然後接下來,我們執行http://127.0.0.1:3000/getInfo後,這段程式碼app.use(Passport.session());就會將我們從前端傳回來的 cookie,進行分析,並和 session 進行比對,然後就會將使用者資料存放到req.user裡囉

app.get('/getInfo',function(req,res){
    const user = req.user;
    res.send(user);
})

注意點 : deserializeUser 無法被呼叫到

有一點要注意一下,如果你發現你的deserializeUser老是無法被呼叫到,那問題是在下面這段 :

app.use(session({
    secret: "test",
    resave: false,
    saveUninitialized: false,
}))

有些人會寫成下面這樣,cookie: { secure: true }這個參數需要配合https才能使用。

你的好朋友 stackoverflow

app.use(session({
    secret: 'goodjob secret',
    resave: false, // don't save session if unmodified
    saveUninitialized: false,
    cookie: { secure: true },
}));

參考資料

comments powered by Disqus