-
Notifications
You must be signed in to change notification settings - Fork 0
Description
应用场景
在日常的业务使用场景中,涉及到获取用户信息的场景基本流程是,用户登录后系统才能获取到该信息。
假设用户使用系统 A,如果需要展示系统 A 存储的用户信息,那只需提供在系统 A 注册的用户名和密码即可。如果需要在系统 A 上展示用户的 GitHub 基本信息,系统 A 是没有该用户的 GitHub 登录名和密码的,需要用户提供,这种情况下存在以下弊端:
- 系统 A 为了后续能够继续展示用户的
GitHub信息,会保存用户的登录信息 - 系统 A 能够读取用户在
GitHub上的所有信息,包括公开信息和私密信息,用户无法限制系统 A 访问GitHub的权限和范围 - 如果用户想收回系统 A 访问
GitHub的权限,只能修改登录信息,这样会影响该用户之前授权给的其他第三方服务 - 一旦系统 A 被破解了,所有存储的用户信息都会泄露,包括通过账号密码授权方式给系统 A 的应用信息也会被泄露
基于以上问题,OAuth 诞生了。
OAuth
Wiki 的定义:
OAuth是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
简单来说,OAuth 在客户端与服务提供商之间,设置了一个授权层(authorization layer)。客户端不能直接登录服务提供商,只能登录授权层,以此将用户与客户端区分开来。客户端登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
客户端登录授权层以后,服务提供商根据令牌的权限范围和有效期,向客户端开放用户储存的资料。
Passport
Passport 是用于 Node.js 程序的中间件鉴权方式。它在 OAuth 的基础上,以中间件的方式插入到 Node.js 程序当中,非常的灵活简便;通过 Passport 开发者可以使用鉴权方式访问 Github,Twitter,Facebook 等服务厂商;也可以通过用户名和密码的方式进行登录授权校验。
passport 提供了很多知名服务厂商的 strategy 如:passport-github, passport-twitter 等。
Egg 的解决方案
Egg 推荐使用 egg-passport ,egg-passport 是基于 Passport 的 Egg 插件,屏蔽了裸写 passport 的一些细节,如初始化、鉴权成功后的回调处理等,并且提供了方便开发者使用的 API。
和 passport 一样,egg-passport 提供了通过用户名密码授权方式和各大服务商授权的方式:
- 通过用户名密码的方式鉴权
各个步骤使用的 API:
进行登录验证:passport 对鉴权的用户名和密码的处理
业务方校验和存储用户:app.passport.verify()
序列化用户信息存入 session:app.passport.serializeUser()
反序列化用户信息从 session 取出:app.passport.deserializeUser
- 通过
strategy的授权方式鉴权
以 GitHub 为例:
如何使用 egg-passport
安装
npm install egg-passport --save配置
在 Egg 中开启该插件
// config/plugin.js
exports.passport = {
enable: true,
package: 'egg-passport',
};直接使用 passport-${strategy}
以 Github 为例:
-
需要创建一个 OAuth Apps,填写好基本信息之后会得到
clientID和clientSecret,注意callbackURL也是必不可少的,这里假设callbackURL是 http://127.0.0.1:7002/auth/github/callback -
然后安装
GitHub的strategy插件
npm install passport-github --save// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/authCallback', controller.home.githubCallback);
// 使用 app.passport.authenticate(),指定 'github' 为鉴权 strategy,对请求进行授权
const githubAuth = app.passport.authenticate('github', {
// 鉴权成功后的重定向
successReturnToOrRedirect: '/authCallback',
});
// 申请鉴权 url
router.get('/passport/github', githubAuth);
// 鉴权后的回调函数,可以处理成功 / 失败的情况
router.get('/auth/github/callback', githubAuth);
}// app.js
module.exports = app => {
// 配置 strategy
const GitHubStrategy = require('passport-github').Strategy;
app.passport.use(new GitHubStrategy({
clientID: '填写你的 clientID',
clientSecret: '填写你的 clientSecret',
callbackURL: '/auth/github/callback',
passReqToCallback: true,
}, (req, accessToken, refreshToken, params, profile, done) => {
const user = {
provider: 'github',
id: profile.id,
name: profile.username,
};
app.passport.doVerify(req, user, done);
}
));
};鉴权通过后,应用层用户一般还需要处理一些事项,如校验和存储用户信息等。
// 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');
// 从数据库中查找用户信息
//
// 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;
}
// 调用 service 注册新用户
const newUser = await ctx.service.user.register(user);
return newUser;
});
// 为了保持数据量小,只把 id 序列化后存进 session 里面
app.passport.serializeUser(async (ctx, user) => {
return user.id;
});
// 反序列化后,根据 id 把用户信息从数据库中取出来
app.passport.deserializeUser(async (ctx, id) => {
db.users.findById(id, function (err, user) {
if (err) { return cb(err); }
return user;
});
});
};使用 egg-passport-${strategy}
如果觉得上面的这种配置方式很繁琐,可以通过安装 egg-passport-github 来简化配置流程:
npm install egg-passport-github --save开启插件:
// config/plugin.js
exports.passportGithub = {
enable: true,
package: 'egg-passport-github',
};
// config/default.js
config.passportGithub = {
key: '填写你的 clientID',
secret: '填写你的 clientSecret'
};注意这里配置的字段是 key 和 secret
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/authCallback', controller.home.githubCallback);
app.passport.mount('github', {
callbackURL: '/auth/github/callback',
successRedirect: '/authCallback'
});
// 上面是简便的写法,等价于以下写法
// const github = app.passport.authenticate('github', {
// successRedirect: '/authCallback',
// });
// app.get('/passport/github', github);
// app.get('/auth/github/callback', github);
}校验和存储用户信息:
// app.js
module.exports = app => {
app.passport.verify(async (ctx, user) => {});
// 为了保持数据量小,只把 id 序列化后存进 session 里面
app.passport.serializeUser(async (ctx, user) => {});
// 反序列化后,根据 id 把用户信息从数据库中取出来
app.passport.deserializeUser(async (ctx, id) => {});
};使用用户名密码方式鉴权
- 安装
passport-local
npm install passport-local --save- 设置路由
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
// 鉴权成功后的回调页面
router.get('/authCallback', controller.home.authCallback);
// 登录
router.get('/login', controller.home.render);
// 鉴权
router.post('/login', app.passport.authenticate('local', { successReturnToOrRedirect: '/authCallback' }));
}- 配置
strategy
module.exports = app => {
const LocalStrategy = require('passport-local').Strategy;
const config = {};
config.passReqToCallback = true;
app.passport.use(new LocalStrategy(config, (req, username, password, done) => {
const user = {
username,
password,
};
app.passport.doVerify(req, user, done);
}));
app.passport.verify(async (ctx, user) => {});
// 为了保持数据量小,只把 id 序列化后存进 session 里面
app.passport.serializeUser(async (ctx, user) => {});
// 反序列化后,根据 id 把用户信息从数据库中取出来
app.passport.deserializeUser(async (ctx, id) => {});
}这里页面模板以 egg-view-ejs 为例
npm install egg-view-ejs --save配置信息
// config/config.default.js
config.view = {
mapping: {
'.ejs': 'ejs',
},
};
// 为了演示方便这里把 csrf 暂时关闭
config.security = {
csrf: {
enable: false
},
};// config/plugin
exports.ejs = {
enable: true,
package: 'egg-view-ejs',
};// app/router.js
router.get('/', controller.home.login);// app/controller/home
async render() {
await this.ctx.render('login.ejs', {
data: JSON.stringify(this.ctx.user) || 'login page',
});
}// app/view/login.ejs
<form method="post" action="/login">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>APIs
extend application
app.passport.mount(strategy, options): 用指定的strategy来设置登录鉴权路由和鉴权后回调方法的路由配置/** * @param {String} startegy - 策略提供商名称 * @param {Object} [options] - 可选的选项配置,loginURL:登录鉴权的 url,默认值为 /passport/${strategy};callbackURL 鉴权后的回调 url,默认值为 /passport/${strategy}/callback;其他的配置项可查看 http://www.passportjs.org/docs/ */ function mount(startegy, options){}
注意:egg-passport 2.0.0 版本不支持 post 方法,所以 mount 方法对用户名密码登录方式不支持
app.passport.authenticate(strategy, options):配置strategy和options来创建一个中间件,用于鉴权使用/** * @param {String} startegy - 策略提供商名称 * @param {Object} [options] - 可选的选项配置,配置项可查看 http://www.passportjs.org/docs/ */ function authenticate(startegy, options){}
app.passport.verify(async (ctx, user) => {}):校验用户app.passport.serializeUser(handler): 序列化用户信息后存储进sessionapp.passport.deserializeUser(handler):反序列化后取出用户信息
extend context
ctx.user:获取当前已登录的用户信息ctx.isAuthenticated():检查该请求是否已授权ctx.login(user[, options]):为用户启动一个登录的sessionctx.logout():退出,将用户信息从session中清除
如何开发 egg-passport-${provider} 插件
以上面的 egg-passport-github 为例
- 插件依赖
egg-passport,主要是使用其APIs
// package.json
{
"eggPlugin": {
"name": "passportGithub",
"dependencies": [
"passport"
]
},
}- 配置信息
// config/default.js
config.passportGithub = {
key: '填写你的 clientID',
secret: '填写你的 clientSecret'
};注意:必须使用 key 和 secret 代替 consumerKey|clientID 和 consumerSecret|clientSecret,这样有利于保持一致的开发体验
- 在
app.js中初始化Strategy并且在verify callback中格式化用户信息
// app.js
const debug = require('debug')('egg-passport-github');
const assert = require('assert');
const Strategy = require('passport-github').Strategy;
module.exports = app => {
const config = app.config.passportGithub;
// 必须将 passReqToCallback 设置为 true,以便于在回调函数中能使用 req
config.passReqToCallback = true;
assert(config.key, '[egg-passport-github] config.passportGithub.key required');
assert(config.secret, '[egg-passport-github] config.passportGithub.secret required');
// 将 key 和 secret 转换成各个 provider 所需的 consumerKey 和 consumerSecret
// 如果 Github 使用的是 clientID 和 clientSecret,这里将其一一替换
config.clientID = config.key;
config.clientSecret = config.secret;
// register github strategy into `app.passport`
// must require `req` params
app.passport.use('github', new Strategy(config, (req, token, tokenSecret, params, profile, done) => {
// format user
const user = {
provider: 'github',
id: profile.id,
name: profile.username,
displayName: profile.displayName,
photo: profile.photos && profile.photos[0] && profile.photos[0].value,
token,
tokenSecret,
params,
profile,
};
debug('%s %s get user: %j', req.method, req.url, user);
// let passport do verify and call verify hook
app.passport.doVerify(req, user, done);
}));
};如何开发 egg-passport-local
以下以本地开发为例,后续发布为插件形式:
// /lib/plugin/egg-passport-local/package.json
{
"eggPlugin": {
"name": "passportLocal",
"dependencies": [
"passport"
]
},
"dependencies": {
"passport-local": "^1.0.0"
}
}// /lib/plugin/egg-passport-local/config
// 配置可参考 https://github.com/jaredhanson/passport-local
exports.passportLocal = {
};// /lib/plugin/egg-passport-local/app
module.exports = app => {
const LocalStrategy = require('passport-local').Strategy;
const config = app.config.passportLocal;
config.passReqToCallback = true;
app.passport.use(new LocalStrategy(config, (req, username, password, done) => {
const user = {
username,
password,
};
// 这里不处理应用层逻辑,传给 app.passport.verify 统一处理
app.passport.doVerify(req, user, done);
}));
};// app/view/login.ejs
<form method="post" action="/login">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>// config/plugin.js
const path = require('path');
exports.passportLocal = {
enable: true,
path: path.join(__dirname, '../lib/plugin/egg-passport-local'),
};好了,到这已经开发好了,如何使用呢?
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/authCallback', controller.home.githubCallback);
const localStrategy = app.passport.authenticate('local', {
successRedirect: '/authCallback',
});
router.get('/login', controller.home.render);
router.post('/login', localStrategy);
}为什么这里不能使用 app.passport.mount('local')?
egg-passport 2.0.0 版本不支持 post 方法,所以 mount 方法对用户名密码登录方式不支持
参考文档
egg-passport

