over 1 year ago

Passport JS是一個Express的中間件(middlewear),專門處理帳號驗證流程,包含社群網站Facebook、Google+等OAuth2等認證方式,相當的方便,以下簡單筆記使用方法。

註冊passport

最基本的需要passport.js註冊於Express的中間件

let passport = require('passport');

// 基本parser

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
//Express使用session(記錄在Redis上)

app.use(cookieParser());
//必須在passport.session()之前喔

app.use(session({
    secret: 'thisismylittlesecret',
    store: RedisDB,
    resave: false,
    saveUninitialized: true,
}));

//必備

app.use(passport.initialize());
//使用session,如果要用OAuth2等建議搭配使用

app.use(passport.session());

這樣就完成最初步的passport註冊

接著,我們會需要在對應的Routing path做不同的驗證,像是最基本的Username&Password、OpenID、Oauth等不同的方式,passport採用插件的方式,讓使用者可以自己選擇需要的方式,以下我會示範Username&Password以及Facebook/Google+的使用方式。
在進入實戰前,必須先瞭解整個驗證的流程,從 參數驗證->查詢資料庫->綁定Session(非必要)->Response

LocalStrategy

LocalStrategy指的就是Username&Password驗證方式,操作passport時,首先要定義該驗證方式的檢驗流程。
定義在passport.use(YOUR_STRATEGY(參數,處理函式)),Strategy中的參數要看該library怎麼設定。
處理函式則是第一手處理驗證的地方,具體傳入的參數會有所不同(後續再看),這裡我定義的流程是 到資料庫找尋相同帳號的資料,如果有比對密碼是否相同,接著就回傳使用者。

let LocalStrategy = require('passport-local').Strategy;

passport.use(new LocalStrategy({
                //別稱,預設為username / password

        usernameField: 'account',
        passwordField: 'password'
    },
    function (account, password, cb) {
        console.log('oauth');
        UserModel.findOne({account: account}, function (err, user) {
            if (err) {
                console.error('DB Error in Passport localstrategy:', err);
                return cb(err, false);
            }
            if (user && user.password != password) {
                return cb(null, false, {message: 'passwordWrong'});
            }
            return cb(null, user);
        });
    }));

這裡要注意 cb中有三個參數,err,user和info,在官方文件中,
如果是一般的驗證錯誤(error)如使用者不存在等,回傳(null, false, {message: '具體錯誤'});
如果是伺服器錯誤(exception),如資料庫連結錯誤等,則回傳(err, false)
如果一切正常,則回傳(null, user)
總之,有錯誤user設為false,系統錯誤放error否則回傳null,驗證錯誤放info。

定義完第一手驗證處理,接著就是處理回傳client的時候。
使用上跟一般的中間件相似,這裡有兩種做法,目前先示範自定義方式
我將localstrategy套用在註冊使用者上,如果有錯誤則直接回傳 -> 接著如果使用者已經存在,則回傳帳號已存在錯誤 -> 創建使用者,成功回傳使用者資料

router.post('/signupByAccount',
    //套用closure

    function (req, res, next) {
        passport.authenticate('local', function (err, user, info) {
            console.log(info);
            if (info) {
                return res.json({error: info.message});
            }
            if (user) {
                return res.json({error: 'accountRepeat'});
            }
            UserModel.create({
                account: req.body.account,
                password: req.body.password
            }).then((user)=> {
                console.log('signup db user', user);

                return res.json({user: user, token:createJWT(user.account)});
            }).catch((err)=> {
                console.error('signup db err', err);
                return res.json({error: err});
            })
        })(req, res, next);
    });

客戶端要傳資料的話,只要以x-www-form-encoded方式夾帶{account,password}(對應別稱或是用預設值)即可,記得Express還是先註冊body-parser喔;如果缺少資料passport會回傳"missing credential的錯誤",算是多一層把關

import request from "superagent";

request.post(serverURL + '/signupByAccount')
            .withCredentials()
            .type('form')
            .send({account, password})

使用OAuth2:Facebook/Google+

流程圖大致長這樣


同樣要先註冊strategy,這裡可以看到第一手驗證處理會依據strategy而有所不同,我希望可以取出使用者基本的id、暱稱、大頭照等,先做預處理,handleOauth是我自己寫的,主要也是做一些基本的資料庫操作。
接著,如果熟悉OAuth2驗證流程都知道,使用者一開始會先導向驗證畫面,接著才會重新導回伺服器,此時如果沒有Session幫助伺服器無法確認使用者的身份。
先前我已經先註冊了app.session(),接著需要passport內建的serializeUser、deserializeUser,兩個函式分別對應將資料存入Session(放入req.session.passport.user,express.session()中間件會處理session存放)存Session取出資料並放入req.user中,這兩個操作不論路徑有沒有註冊都會視為套用(可以理解為Session處理的中間件)。
最後就是路徑註冊。

let FacebookStrategy = require('passport-facebook').Strategy;
let GithubStrategy = require('passport-github').Strategy;

passport.use(new FacebookStrategy({
        clientID: '~',
        clientSecret: '~',
        callbackURL: serverURL + "/auth/facebook/callback"
    },
    function (accessToken, refreshToken, profile, cb) {
        superAgent.get('https://graph.facebook.com/v2.8/' + profile.id +"/picture?access_token=" + accessToken)
            .end((err, fbres)=>{
                if(err){
                    cb(err, false);
                }
                profile.photo = fbres.redirects[0];
                handleOauth(cb, profile, 'facebook');
            });
    }
));

passport.use(new GoogleStrategy({
        clientID: '~',
        clientSecret: '~',
        callbackURL: serverURL + "/auth/google/callback"
    },
    function (accessToken, refreshToken, profile, cb) {
        handleOauth(cb,profile, 'google');
    }
));

function handleOauth(cb, profile, type){
    UserModel.findOne({
        account: profile.id
    }, function(err, user){
        console.log('handle oauth', err, user);
        if(err) return cb(null, false,{message: 'db error'});
        if(!user){
            UserModel.create({
                account: profile.id,
                avatar: type=='facebook'?profile.photo:profile.photos[0].value,
                nickname: profile.displayName
            }, function(err, user){
                if(err || !user){
                    return cb(err, false);
                }
                return cb(null, {user: user, token: createJWT(user.account)});
            })else{
                return cb(null, {user: user, token: createJWT(user.account)});
            }
        }
    });
}

// 從strategy那得到輸入值,設定到req.session.passport.user

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

// 此時輸入info為req.session.passport.user值,接著done傳入的值會放入 req.user中

// 官網是說為了讓session小一點,serializeUser中放user.id,deserializeUser再到DB用id撈資料

// 但我這邊就直接放整個user資料,沒有再多處理,所以才會看起來有多此一舉的感覺

passport.deserializeUser(function (info, done) {
    console.log('info',info);
    done(null, info);
});

router.get('/auth/google/callback',
    passport.authenticate('google', {
        failureRedirect: 'http://localhost:8080',
        session: true, successRedirect: 'http://localhost:8080'
    }));

router.get('/auth/facebook', passport.authenticate('facebook', {scope: ['public_profile', 'user_friends']}));

router.get('/auth/facebook/callback',
    passport.authenticate('facebook', {
        failureRedirect: 'http://localhost:8080',
        session: true, successRedirect: 'http://localhost:8080'
    }));
    
    
//之後要主動向Server拿資料

router.get('/getInitData', (req, res)=> {
    console.log(req.user);
    if(req.session && req.session.passport){
        return res.json({user:req.user, token: createJWT(req.user.account)});
    }
    return res.json({error:'noData'});
});
← AWS-IAM設定與ECS Repo使用 Chichat - 使用JS全端打造了一個失敗的IM Web App →
 
comments powered by Disqus