背景
随着微信的广泛使用,针对微信服务号的网页开发也是个常见需求。其中最常见的一个环节就是微信的 oAuth 登录,获取到用户的微信信息,来免去用户在微信场景下反复注册的麻烦。
微信官方给出了 oAuth 登录的详细介绍:developers.weixin.qq.com/doc/offiacc…
在实际开发中,通常也是按照该流程,从网页上跳转页面,往微信服务器发起 oAuth 请求,获取 code 以后再跳转回来,获取 access_token,再获取用户信息。
但是微信处于安全考虑,要求添加信任域名,只有在信任域名上发起的跳转,才能获取到 oAuth 信息。这样就对开发环境里的微信登录带来了问题。
如果我们想统一在本地的开发环境中,也能微信登录,该怎么做呢?
方法
思路就是能把微信相关的登录服务抽离出去,固定部署在一个域名的服务器上。让我们无论从哪里来的登录请求,都可以统一从一个线上固定域名的服务器往微信服务器发出跳转。就可以在本地也进行微信登录啦。
大概步骤如下:
- 前端发起登录请求,尝试获取用户信息
- 服务器收到请求,在 state 中带上当前页面以及处理得到用户信息以后的 auth_callBack 地址,跳转到微信 oAuth 登录页面。并将 callBack 设置成专门处理微信动作的 bolt 服务器得到 code 以后的 API 接口。
- 用户完成授权登录动作后,跳转到 bolt 服务器,利用 code 获取到用户信息。
- bolt 服务器利用 state 中的 auth_callBack 地址,跳转到应用服务器的对应接口,并在查询参数中带上用户信息和用户访问页面。
- 应用服务器在对应接口中,根据得到的用户信息,在 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 春招闯关活动」, 点击查看 活动详情