开发环境也需要微信登录|项目复盘

925 阅读3分钟

背景

随着微信的广泛使用,针对微信服务号的网页开发也是个常见需求。其中最常见的一个环节就是微信的 oAuth 登录,获取到用户的微信信息,来免去用户在微信场景下反复注册的麻烦。

微信官方给出了 oAuth 登录的详细介绍:developers.weixin.qq.com/doc/offiacc…

在实际开发中,通常也是按照该流程,从网页上跳转页面,往微信服务器发起 oAuth 请求,获取 code 以后再跳转回来,获取 access_token,再获取用户信息。

但是微信处于安全考虑,要求添加信任域名,只有在信任域名上发起的跳转,才能获取到 oAuth 信息。这样就对开发环境里的微信登录带来了问题。

如果我们想统一在本地的开发环境中,也能微信登录,该怎么做呢?

方法

思路就是能把微信相关的登录服务抽离出去,固定部署在一个域名的服务器上。让我们无论从哪里来的登录请求,都可以统一从一个线上固定域名的服务器往微信服务器发出跳转。就可以在本地也进行微信登录啦。

大概步骤如下:

  1. 前端发起登录请求,尝试获取用户信息
  2. 服务器收到请求,在 state 中带上当前页面以及处理得到用户信息以后的 auth_callBack 地址,跳转到微信 oAuth 登录页面。并将 callBack 设置成专门处理微信动作的 bolt 服务器得到 code 以后的 API 接口。
  3. 用户完成授权登录动作后,跳转到 bolt 服务器,利用 code 获取到用户信息。
  4. bolt 服务器利用 state 中的 auth_callBack 地址,跳转到应用服务器的对应接口,并在查询参数中带上用户信息和用户访问页面。
  5. 应用服务器在对应接口中,根据得到的用户信息,在 cookie 中设置 token,并跳转到原先页面,前端在有 token 的情况下就可以登录了。

代码

关键代码如下:

应用服务器

const wechatLoginRedirect = R.curry(
  async (isOpen: boolean, ctx: IRouterContext) => {
    const protocal = ctx.protocol;
    const host = ctx.host;
    const { state = '/' } = ctx.query;
    const callbackUrl = `${protocal}://${host}/passport/wechat_login_callback`;
    const { AppID, LoginUrl, RedirectUrl } = isOpen
      ? WechatOpen
      : WechatAccount;
    const scope = isOpen ? 'snsapi_login' : 'snsapi_userinfo';
    const redirectUrl = `${RedirectUrl}?callbackUrl=${encodeURIComponent(
      callbackUrl
    )}`;
    const url = getLoginUrl(AppID, LoginUrl, redirectUrl, scope, state);
    ctx.redirect(url);
  }
);

const wechatAccountLoginRedirect = wechatLoginRedirect(false);
const wechatOpenLoginRedirect = wechatLoginRedirect(true);

Passport.get('/wechat_login_open', wechatOpenLoginRedirect);
Passport.get('/wechat_login', wechatAccountLoginRedirect);

const wechatLoginCallback = async (ctx: IRouterContext) => {
  const { userInfo, state = '/' } = ctx.query;
  const jsonUserInfo = JSON.parse(userInfo);
  const userProfile = await getUserProfileByWechat(jsonUserInfo);
  const userId = userProfile.id;
  const userIdToken = jwt.sign(userId, TOKEN_KEY);
  ctx.cookies.set('token', userIdToken, {
    httpOnly: true,
    maxAge: 30 * 24 * 60 * 60 * 1000,
    signed: true,
  });
  ctx.redirect(state);
};

Passport.get('/wechat_login_callback', wechatLoginCallback);

bolt 服务器

router.get('/auth', async (ctx) => {
  const { code, callbackUrl, state } = ctx.query;

  const { AppID, AppSecret } = WechatAccount;
  const redirectUrl = await getRedirectUrlWithWechatUser(
    ctx,
    callbackUrl,
    AppID,
    AppSecret,
    code,
    state
  );
  ctx.redirect(redirectUrl);
  return ctx.response;
});

router.get('/auth/open', async (ctx) => {
  const { code, callbackUrl, state } = ctx.query;
  const { AppID, AppSecret } = WechatOpen;
  const redirectUrl = await getRedirectUrlWithWechatUser(
    ctx,
    callbackUrl,
    AppID,
    AppSecret,
    code,
    state
  );
  ctx.redirect(redirectUrl);
  return ctx.response;
});
const getRedirectUrlWithWechatUser = async (
  ctx,
  callbackUrl = CallBackUrl,
  appId,
  appSecret,
  code,
  state
) => {
  const {
    data: { access_token, openid },
  } = await getWechatAccessToken(ctx, appId, appSecret, code);

  const { data: objUserInfo } = await getWechatUserInfo(
    ctx,
    access_token,
    openid
  );

  const userInfo = JSON.stringify(objUserInfo);
  const strQuery = qs.stringify({ userInfo, state });
  const containQueryAlready = callbackUrl.indexOf('?') !== -1;
  const redirectUtl = `${callbackUrl}${
    containQueryAlready ? '&' : '?'
  }${strQuery}`;
  return redirectUtl;
};

总结

在面对在线调试比较麻烦的第三方接入时,可以仔细考虑一下具体的流程,将核心的部分抽离出来单独抽离。并通过 http 跳转在过程中处理事务。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情