NodeJS認證功能實作 - PassportJS介紹

以PassportJS + MongoDB建立一個具有認證機制的網頁應用程式

認證是許多應用程式會有的功能,本文就來介紹NodeJS中最被廣泛使用的認證函式庫-PassportJS。PassportJS基本上是用來作為expressJS中的middleware,負責處理認證的部分,passportJS是一個強大且完整的函式庫,提供多種不同的認證方式讓你依照自己的需求來選擇(稱作strategy)。所謂完整即是說大部份的認證方式(FB, Google, OAuth, OpenID)都已經有寫好的strategy,僅需要把對應的strategy lib抓下來即可使用,而如果有需要自定模組的話也可以自己寫一個strategy來處理。

由於passportJS的doc實在寫的不是很完整,因此我寫了一個範例的project跟這篇文章來解釋一下整個流程,以passport-local的strategy也就是使用自己local的資料庫來做認證,以下先介紹project的大致架構,再來介紹passport的用法。

功能簡介:

  1. 連至此web app的頁面會先被導向登入頁面
  2. 認證之後依照權限(管理員與非管理員)顯示不同功能
  3. 非管理員僅只能看到index頁面,及登出功能
  4. 管理員除了index頁面之外另外可以看到此系統的使用者列表

使用情境:

  1. 使用者連至網頁
  2. 系統檢查該網頁是否需要授權
  3. 如果需要授權且使用者沒登入,則導向登入頁面
  4. 顯示頁面。在這我們把環境簡化一些,使用者的資料是儲存在自己的MongoDB裡;而使用者的權限也僅簡單的分為兩種:一般使用者(user)以及管理者(admin)。

Github Repository

此範例程式修改自passport-local裡的範例
範例程式Github
(註: 此project以coffeescript撰寫,若對coffeescript不了解可以用js2coffee轉換為javascript)

檔案目錄結構

example-passport/
    config/               //project設定黨,以json格式儲存
    public/               //web端需要的圖片, js, css
    lib/                  //coffeescript 產生之js檔
    src/                  //coffeescript 原始碼
        test/             //unit test
        web/              //web app主程式碼
            controllers/  //各頁面的controller
            models/       //models (mongoose)
            util/         //其他
        app.coffee        //程式進入點
    views/                //頁面模板
    Cakefile              //coffeescript版本的makefile
    package.json          //npm config file

檔案目錄很簡單,以MVC為架構區分controller, models跟views,其中controllers及models是coffeescript file,故放在src底下。而views是以jade撰寫,並不需要額外compile,故放在src之外。其他各檔案基本參照nodejs慣例用法存放。接下來看看要如何加入認證的功能。

網頁路徑設定

針對要認證的頁面加入認證的middleware
app.coffee 65行之後是web path routing設定。程式碼如下:
#WEB Route
app.get('/', pass.ensureAuthenticated, views.index)
app.get('/login', user.getlogin)
app.post('/login', user.postlogin)
app.get('/logout', user.logout)
app.get('/admin/users', pass.ensureAuthenticated, pass.ensureAdmin(), user.listUser)
#app.post('/admin/users', user_routes.createUser)
app.get('/admin/users/:id', pass.ensureAuthenticated, pass.ensureAdmin(), user.editUser)
可以看到這裡對//admin/users以及/admin/users/:id三個路徑安插了認證的middleware。注意/admin/users以及/admin/users/:id這兩個頁面屬於管理員(admin)層級才看得到的,故多了一個pass.ensureAdmin()的middleware來確保是管理員身份,相較之下/的路徑就只需要確保使用者有登入即可。

兩個middleware程式碼其實都很簡單,在src/web/util/pass.coffee 看得到。

另外如果認證是透過session來存取時,要記得加上express的session支援(app.coffee的 58 & 60行)

如何認證

首先,我們需要先定義Strategy (放在src/web/util/pass.coffee
passport.use new LocalStrategy((username, password, done) ->
    User.findOne
        username: username
    , (err, user) ->
        return done(err)  if err
        unless user
            return done(null, false,
                message: "Invalid username or password"
            )
        user.comparePassword password, (err, isMatch) ->
            return done(err)  if err
            if isMatch
                done null, user
            else
                done null, false,
                    message: "Invalid username or password"
)

Strategy簡單的說,就是定義一個認證的流程,在這project中由於我們是採用local資料庫儲存使用者資料,故可以看到程式碼中的user.comparePassword其實是從資料庫中取得使用者資料來比對是否為合法物件。

要定義一個LocalStrategy,語法是這樣:(src/web/util/pass.coffee )

passport.use(new LocalStrategy((username, password, done) ->
    #在此確認username/password是否正確
    #如果正確 則回傳 done(null, user), user為使用者物件
    #如果賬號密碼錯誤,則回傳 done(null, false, {message:"失敗訊息"})
    #如果有其他錯誤則回傳 done(err),err是錯誤物件
)

Strategy定義好了之後還需要使用,而會使用到的地方當然就是登入頁面 src/web/controllers/users.coffee

# POST /login
#   This is an alternative implementation that uses a custom callback to
#   acheive the same functionality.
exports.postlogin = (req, res, next) ->
    passport.authenticate("local", (err, user, info) ->
        return next(err)  if err
        unless user
            req.session.messages = [info.message]
            return res.redirect("/login")
        req.logIn user, (err) ->
            return next(err)  if err
            res.redirect "/"
    ) req, res, next

passport.authenticate的第一個參數是用來選擇strategy,local代表的是使用LocalStrategy,也就是我們之前定義的。第二個參數對應的就是之前定義的done,我們可以看到三個參數err, user, info有互相對應。在這裡要注意的是req.logIn這個function,他會把第一個參數(user)設定為req.user,這代表了req.isAuthenticated會回傳true,這樣就可以通過認證了。

至此我們已經有一個具有認證功能的web app了,之後會再介紹如何不使用session的情況下完成使用者認證。(適用於REST API)

參考資料

沒有留言:

張貼留言