『登錄鑒權(quán)』 是一個常見的業(yè)務(wù)場景,包括『賬號密碼登錄方式』和『第三方統(tǒng)一登錄』。
其中,后者我們經(jīng)常使用到,如 Google, GitHub,QQ 統(tǒng)一登錄,它們都是基于 OAuth 規(guī)范。
Passport 是一個擴展性很強的認證中間件,支持 Github,Twitter,F(xiàn)acebook 等知名服務(wù)廠商的 Strategy,同時也支持通過賬號密碼的方式進行登錄授權(quán)校驗。
Egg 在它之上提供了 egg-passport 插件,把初始化、鑒權(quán)成功后的回調(diào)處理等通用邏輯封裝掉,使得開發(fā)者僅需調(diào)用幾個 API 即可方便的使用 Passport 。
Passport 的執(zhí)行時序如下:
- 用戶訪問頁面
- 檢查 Session
- 攔截跳鑒權(quán)登錄頁面
- Strategy 鑒權(quán)
- 校驗和存儲用戶信息
- 序列化用戶信息到 Session
- 跳轉(zhuǎn)到指定頁面
使用 egg-passport
下面,我們將以 GitHub 登錄為例,來演示下如何使用。
安裝
$ npm i --save egg-passport $ npm i --save egg-passport-github
|
更多插件參見 GitHub Topic - egg-passport 。
配置
開啟插件:
// config/plugin.js module.exports.passport = { enable: true, package: 'egg-passport', };
module.exports.passportGithub = { enable: true, package: 'egg-passport-github', };
|
配置:
注意:egg-passport 標(biāo)準(zhǔn)化了配置字段,統(tǒng)一為 key 和 secret 。
// config/default.js config.passportGithub = { key: 'your_clientID', secret: 'your_clientSecret', // callbackURL: '/passport/github/callback', // proxy: false, };
|
注意:
- 創(chuàng)建一個 GitHub OAuth Apps,得到 clientID 和 clientSecret 信息。
- 填寫 callbackURL,如 http://127.0.0.1:7001/passport/github/callback線上部署時需要更新為對應(yīng)的域名路徑為配置的 options.callbackURL,默認為 /passport/${strategy}/callback
- 如應(yīng)用部署在 Nginx/HAProxy 之后,需設(shè)置插件 proxy 選項為 true, 并檢查以下配置:代理附加 HTTP 頭字段:x-forwarded-proto 與 x-forwarded-host配置中 config.proxy 應(yīng)設(shè)置為 true
掛載路由
// app/router.js module.exports = app => { const { router, controller } = app;
// 掛載鑒權(quán)路由 app.passport.mount('github');
// 上面的 mount 是語法糖,等價于 // const github = app.passport.authenticate('github', {}); // router.get('/passport/github', github); // router.get('/passport/github/callback', github); }
|
用戶信息處理
接著,我們還需要:
- 首次登錄時,一般需要把用戶信息進行入庫,并記錄 Session 。
- 二次登錄時,從 OAuth 或 Session 拿到的用戶信息,讀取數(shù)據(jù)庫拿到完整的用戶信息。
// app.js module.exports = app => { app.passport.verify(async (ctx, user) => { // 檢查用戶 assert(user.provider, 'user.provider should exists'); assert(user.id, 'user.id should exists');
// 從數(shù)據(jù)庫中查找用戶信息 // // Authorization Table // column | desc // --- | -- // provider | provider name, like github, twitter, facebook, weibo and so on // uid | provider unique id // user_id | current application user id const auth = await ctx.model.Authorization.findOne({ uid: user.id, provider: user.provider, }); const existsUser = await ctx.model.User.findOne({ id: auth.user_id }); if (existsUser) { return existsUser; } // 調(diào)用 service 注冊新用戶 const newUser = await ctx.service.user.register(user); return newUser; });
// 將用戶信息序列化后存進 session 里面,一般需要精簡,只保存?zhèn)€別字段 app.passport.serializeUser(async (ctx, user) => { // 處理 user // ... // return user; });
// 反序列化后把用戶信息從 session 中取出來,反查數(shù)據(jù)庫拿到完整信息 app.passport.deserializeUser(async (ctx, user) => { // 處理 user // ... // return user; }); };
|
至此,我們就完成了所有的配置,完整的示例可以參見:eggjs/examples/passport
API
egg-passport 提供了以下擴展:
- ctx.user - 獲取當(dāng)前已登錄的用戶信息
- ctx.isAuthenticated() - 檢查該請求是否已授權(quán)
- ctx.login(user, [options]) - 為用戶啟動一個登錄的 session
- ctx.logout() - 退出,將用戶信息從 session 中清除
- ctx.session.returnTo= - 在跳轉(zhuǎn)驗證前設(shè)置,可以指定成功后的 redirect 地址
還提供了 API:
- app.passport.verify(async (ctx, user) => {}) - 校驗用戶
- app.passport.serializeUser(async (ctx, user) => {}) - 序列化用戶信息后存儲進 session
- app.passport.deserializeUser(async (ctx, user) => {}) - 反序列化后取出用戶信息
- app.passport.authenticate(strategy, options) - 生成指定的鑒權(quán)中間件options.successRedirect - 指定鑒權(quán)成功后的 redirect 地址options.loginURL - 跳轉(zhuǎn)登錄地址,默認為 /passport/${strategy}options.callbackURL - 授權(quán)后回調(diào)地址,默認為 /passport/${strategy}/callback
- app.passport.mount(strategy, options) - 語法糖,方便開發(fā)者配置路由
注意:
- app.passport.authenticate 中,未設(shè)置 options.successRedirect 或者 options.successReturnToOrRedirect 將默認跳轉(zhuǎn) /
使用 Passport 生態(tài)
Passport 的中間件很多,不可能都進行二次封裝。 接下來,我們來看看如何在框架中直接使用 Passport 中間件。 以『賬號密碼登錄方式』的 passport-local 為例:
安裝
$ npm i --save passport-local
|
配置
// app.js const LocalStrategy = require('passport-local').Strategy;
module.exports = app => { // 掛載 strategy app.passport.use(new LocalStrategy({ passReqToCallback: true, }, (req, username, password, done) => { // format user const user = { provider: 'local', username, password, }; debug('%s %s get user: %j', req.method, req.url, user); app.passport.doVerify(req, user, done); }));
// 處理用戶信息 app.passport.verify(async (ctx, user) => {}); app.passport.serializeUser(async (ctx, user) => {}); app.passport.deserializeUser(async (ctx, user) => {}); };
|
掛載路由
// app/router.js module.exports = app => { const { router, controller } = app; router.get('/', controller.home.index);
// 鑒權(quán)成功后的回調(diào)頁面 router.get('/authCallback', controller.home.authCallback);
// 渲染登錄頁面,用戶輸入賬號密碼 router.get('/login', controller.home.login); // 登錄校驗 router.post('/login', app.passport.authenticate('local', { successRedirect: '/authCallback' })); };
|
如何開發(fā)一個 egg-passport 插件
在上一節(jié)中,我們學(xué)會了如何在框架中使用 Passport 中間件,我們可以進一步把它封裝成插件,回饋社區(qū)。
初始化:
$ npm init egg --type=plugin egg-passport-local
|
在 package.json 中配置依賴:
{ "name": "egg-passport-local", "version": "1.0.0", "eggPlugin": { "name": "passportLocal", "dependencies": [ "passport" ] }, "dependencies": { "passport-local": "^1.0.0" } }
|
配置:
// {plugin_root}/config/config.default.js // https://github.com/jaredhanson/passport-local exports.passportLocal = { };
|
注意:egg-passport 標(biāo)準(zhǔn)化了配置字段,統(tǒng)一為 key 和 secret,故若對應(yīng)的 Passport 中間件屬性名不一致時,開發(fā)者應(yīng)該進行轉(zhuǎn)換。
注冊 passport 中間件:
// {plugin_root}/app.js const LocalStrategy = require('passport-local').Strategy;
module.exports = app => { const config = app.config.passportLocal; config.passReqToCallback = true;
app.passport.use(new LocalStrategy(config, (req, username, password, done) => { // 把 Passport 插件返回的數(shù)據(jù)進行清洗處理,返回 User 對象 const user = { provider: 'local', username, password, }; // 這里不處理應(yīng)用層邏輯,傳給 app.passport.verify 統(tǒng)一處理 app.passport.doVerify(req, user, done); })); }; |
更多建議: