eggjs / egg

🥚 Born to build better enterprise frameworks and apps with Node.js & Koa
https://eggjs.org
MIT License
18.9k stars 1.82k forks source link

[RFC] 抽取 passport 模型 #38

Closed fengmk2 closed 7 years ago

fengmk2 commented 8 years ago

RFC: passport

由于业界的 passport 已经足够简单,egg 做起来就是配置。对应应用开发者,更加是配置,基本无需编写代码就能实现。

一个应用的 authenticate 通用过程

  1. 用户会选择应用提供的其中一种 strategy 进行 authenticate 登录,如应用同时提供了 twitter 和 facebook 两种方式。
  2. 用户去 twitter 登录成功后,会返回应用网站,然后 egg-passport-twitter 插件,会使用 verify callback 来完成对统一约定 user 的封装,继续返回给 egg-passport 做后续处理。
  3. egg-passport 会触发 app.passport.verify hook,这样应用层就能做最终的核实,例如做新用户注册,老用户绑定校验等等,这里也是用户数据持久化的地方。
  4. 用户登录之后,每次访问应用都会根据 app.passport.deserializeUserapp.passport.serializeUser 来还原 session。

说明

统一 user 字段约定

ctx.user 是一个 getter,真实数据来自于 ctx.state.user

每个 passport-xxx 插件都需要按照约定封装一个 user。

passport 配置

// config/config.${env}.js
exports.passport = {
  twitter: {
    consumerKey: 'your-consumer-key',
    consumerSecret: 'your-consumer-secret',
    // 更多配置请查看 passport-twitter 插件
  },
};

应用需要关注的3个用户数据处理 hooks

// app.js
module.exports = app => {
  // 1. 核实登录用户信息,自行判断是否需要做数据调整,
  // 也可以在这里由应用自身统一 user 的数据结构,并且保存到数据库。
  // - 必须返回核实后的 user,即 verifiedUser,数据结构可以由应用自行统一约定
  // - 核实失败,throw 一个带有 status = 401 的 error 异常
  // - 其他异常,将会当作服务端内部异常处理
  //
  // passport-xxx 插件已经按约定生成了 user 数据,应用可以在此做最终的用户校验,并且做数据持久化
  // 在 strategy.authenticate 成功之后调用
  app.passport.verify(function* (ctx, user) {

  });

  // 自行处理经过 verify 后的 verifiedUser 数据如何序列化到 session
  // 在 http 请求结束阶段,保存数据到 session 之前调用
  app.passport.serializeUser(function* (ctx, verifiedUser) {
    // you can store profile and handle token here
  });

  // 根据 sesssion 中的 sessionUser 信息,还原出 verifiedUser
  // 在 http 请求开始阶段,还原 session 之后调用
  app.passport.deserializeUser(function* (ctx, sessionUser) {

  });
};
fengmk2 commented 8 years ago

以 oauth 登录作为标准的示例实现。

fengmk2 commented 8 years ago

http://passportjs.org/

popomore commented 8 years ago

哈哈,这个也挺老了

fengmk2 commented 8 years ago

@popomore 我看它是最完善的。

fengmk2 commented 8 years ago

各种插件一直都在更新 http://web.npm.alibaba-inc.com/package/passport-outlook

fengmk2 commented 8 years ago

https://github.com/rkusa/koa-passport

fengmk2 commented 7 years ago

淘宝登录也有需求 egg-taobao-login

atian25 commented 7 years ago

egg-dingtalk-login 🤔

shaoshuai0102 commented 7 years ago

认领 egg-weibo-login,接一个先试试

fengmk2 commented 7 years ago

看来还是得封装 egg-passport-xxx,因为需要统一约定处理 afterAuth,并且将 {token, tokenSecret, profile} 转换为统一字段约定的 user,至少要包含哪些统一命名的字段。

atian25 commented 7 years ago

这些 passport 的 adapter 跟插件有些区别吧,需允许多个共存,不一样的 eggPlugin.name?

popomore commented 7 years ago

感觉直接传 Strategy 实例比较好,不同的 Strategy 参数不一样,也可能会有两个。

exports.passport = {
  strategy: [
    new TwitterStrategy({
      consumerKey: TWITTER_CONSUMER_KEY,
      consumerSecret: TWITTER_CONSUMER_SECRET,
      callbackURL: "http://127.0.0.1:3000/auth/twitter/callback"
    },
    function(token, tokenSecret, profile, cb) {
      User.findOrCreate({ twitterId: profile.id }, function (err, user) {
        return cb(err, user);
      });
    })
  ]
};
popomore commented 7 years ago

把 passport 的 callback API 都要改成 promise 的

fengmk2 commented 7 years ago

auto0 还有一个 params 参数: accessToken, refreshToken, extraParams, profile https://github.com/auth0/passport-auth0#configuration

fengmk2 commented 7 years ago

@popomore 还是改成 passport-xx 模式了,每个插件都需要按照统一 user 数据格式实现 verify 方法,生成一个 user,再由应用自行实现总的 verify 来做最终用户校验。没办法每个插件能使用到数据库实现 verify 的。

popomore commented 7 years ago

但这个就不算 passport 了,用什么来存储用户信息和 passport 没关系。内部可以直接在 Strategy 里面封装获取用户信息。

fengmk2 commented 7 years ago

内部可以直接在 Strategy 里面封装获取用户信息。

@popomore 对啊,内部应用代码就不需要实现 verify 了

popomore commented 7 years ago

app.passport.verify 的 hook 就是替换 strategy 里的 verify callback 么?

fengmk2 commented 7 years ago

@popomore 不完全是,strategy verify callback 有 passport-xxx 插件实现。而 app.passport.verify 是我们封装过的,给应用使用的。这样一个应用同时接入多种 strategy,都可以通过同一个 app.passport.verify 实现最终核实过程。

popomore commented 7 years ago

恩,功能是一样的,只是数据格式是否统一。他的 passport-stragery 基类其实什么都没做,约定比较松散。

fengmk2 commented 7 years ago

只是我们约定插件来实现 verify callback

fengmk2 commented 7 years ago

demo https://github.com/eggjs/examples/pull/14

fengmk2 commented 7 years ago

对于每次都去缓存读取用户信息的 user,需要默认关闭 passport-session 策略。

const auth = app.passport.authenticate('alipaysession', { 
  session: false, 
  successReturnToOrRedirect: false,
  successRedirect: false,
});

// 将 auth 加入全局中间件,让每个请求都过 alipaysession
app.use(auth);

但是要追求高性能的话,alipaysession 应该直接走普通中间件,不走 passport 这台,因为它是每次请求都去缓存读取数据的。

fengmk2 commented 7 years ago

@shaoshuai0102 https://github.com/eggjs/egg-passport-weibo 基本不需要做,我需要想想怎么 mock 方便写单元测试才是王道。

atian25 commented 7 years ago

这个是不是还缺篇文档?

popomore commented 7 years ago

恩,要加一下

yandongxu commented 7 years ago

passport-local 该怎么做?有参考么?

dangyanglim commented 7 years ago

@yandongxu 我也不会这个 passport-local

leiyanggz commented 7 years ago

@yandongxu @dangyanglim https://github.com/leiyanggz/egg-passport-local

popomore commented 7 years ago

@leiyanggz 这个没写测试?

jackhutu commented 7 years ago

为什么在egg-passport中, 重写passport的方法时, 要把callback去掉, 是暂时不支持还是不打算支持呀?

popomore commented 7 years ago

@jackhutu 我们最佳实践是 generator function 和 async function,所有的代码应该看不到 callback。

zsbox commented 6 years ago
async login() {
  const { body } = this.ctx.request;
  const { email: _email, password: _password } = body;
  const rule = {
    email: {
      type: 'email',
    },
    password: {
      type: 'password',
    },
  };
  try {
     this.ctx.validate(rule);
  } catch (err) {
    console.log(err);
    this.ctx.redirect('/user');
    return;
  }
  const user = await this.app.mysql.get('users', { email: _email });
  const dbpw = user.password;
  const fmpw = crypto.MD5(_password).toString();
  if (user && dbpw === fmpw) {
     console.log('登陆成功');
     this.app.passport.serializeUser(async (ctx, user) => {
       console.log('000000', user);
       // 保存用户信息到session
     });
    this.ctx.redirect('/user');
  } else {
    console.log('登陆失败');
  }
}

@fengmk2 本地鉴权怎么用啊?感觉调用this.app.passport.serializeUser并没有成功啊 session里没有东西 this.app.passport.deserializeUser方法也没用 教程省略了太多 对小白太难了 求指点

atian25 commented 6 years ago

app.passport.serializeUser 是写在 app.js 的