写在前面
前几天我接到了海外平台三方登录的需求,搜索博客后发现大部分都是直接调库,于是参考现有文章从头到尾自己手写了一遍实现了全流程。因网上有太多OAuth配置教程所以在此不在赘述,本文仅详细说明了源码部分。
参考文章
谷歌、微信第三方登录前端vue实现方案
apple oauth 三方登录
前端接入 Google OAuth 2.0 三方授权登录 流程解释
overview流程图如下:
具体步骤
一.视图部分
项目中使用了Vant组件库,我直接调用了van-button
组件。每个按钮都绑定了thirdLogin
函数,为了保证可复用性,我通过传入不同的case参数来实现了相同的逻辑。
login/index.vue:
<van-button block @click="thirdLogin('GOOGLE')">
{{ $t("buttons.googleLogin") }}
</van-button>
<div class="w-full bg-[#707070] opacity-10 my-5"></div>
<van-button block @click="thirdLogin('APPLE')">
{{ $t("buttons.appleLogin") }}
</van-button>
<div class="w-full bg-[#707070] opacity-10 my-5"></div>
<van-button block @click="thirdLogin('FACEBOOK')">
{{ $t("buttons.facebookLogin") }}
</van-button>
二.拉起第三方平台验证
通过thirdLogin()
方法构建好的 URL 可以启动第三方平台进行验证。验证成功后,会重定向到第三方平台中配置好的redirect_url
,即回调地址。在回调地址的 URL 参数部分会携带下一步骤所需的id_token
等令牌。
在这里,我设置的redirect_url
是 conversation 页面,也就是聊天主页面。然而在调试过程中发现,只能跳转到 login 页面。经过检查,发现必须先通过 logincheck()
方法获取自己项目的登录相关参数,才能够成功跳转到 conversation 页面。这个过程实际上是一个多步骤的验证流程,需要确保在每一步都成功之后才能进入到下一步。
参数配置如下,必须和第三方平台中填写的完全一致:
login/index.vue:
const googleConfig = ref({
client_id:"", // 客户端ID
redirect_uri: "", // 跳转回调地址,要进行url编码
response_type: "id_token",//
scope: "https://www.googleapis.com/auth/userinfo.email",
nonce: generateNonce(),
});
const appleConfig = ref({
client_id: "",
redirect_uri: "",
response_type: "",
scope: " ",
response_mode: "",
state: "",
nonce: generateNonce(),
});
function generateNonce() {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
}
接下来就是thirdLogin()
函数,也就是实现拉起三方验证的函数;跳转后页面刷新会丢失当前的js上下文环境,所以要将三方登录存到localstorage里面以便后面流程使用:
const thirdLogin = async (type: string) => {
localStorage.setItem("thirdLogin", type);
let url;
switch (type) {
case "GOOGLE":
localStorage.setItem("clientId", googleConfig.value.client_id);
url = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${
googleConfig.value.client_id
}&redirect_uri=${encodeURIComponent(
googleConfig.value.redirect_uri
)}&response_type=${googleConfig.value.response_type}&scope=${
googleConfig.value.scope
}&nonce=${googleConfig.value.nonce}`;
break;
case "APPLE":
localStorage.setItem("clientId", appleConfig.value.client_id);
url = `https://appleid.apple.com/auth/authorize?client_id=${
appleConfig.value.client_id
}&redirect_uri=${encodeURIComponent(
appleConfig.value.redirect_uri
)}&response_type=${appleConfig.value.response_type}&scope=${
appleConfig.value.scope
}&response_mode=${appleConfig.value.response_mode}`;
break;
case "FACEBOOK":
break;
}
if (url) {
window.location.href = url;
}
};
对于不同的第三方平台,它们在重定向 URL 后面拼接参数和发送身份验证 token 的方式可能是不同的,我们需要针对不同的第三方平台进行具体的实现,所以此处没有进行进一步的抽象封装。
三.在登录验证函数处理后端返回的登录态
当用户通过第三方登录成功后,我们会从第三方服务器获取一个id_token
。接着,我们将这个id_token
发送给我们自己项目的后端服务器,以获取本项目所需的登录相关参数。后端服务器会解析这个id_token
,获取第三方平台注册的昵称和头像等信息,然后生成本项目所需的登录参数。这些参数会被存储到本地,然后通过项目原有的logincheck()
方法进行验证。只有验证通过后,用户才能够被跳转到之前配置的redirect_url
,也就是聊天主页面。
对于从未注册过的用户来说,这次登录就相当于一次注册,所以还需要以第三方平台中用户的某些信息作为注册信息进行登录,当已经注册过的用户使用第三方登录时,要让其正常登录。
发送到后端的请求函数写在src\api\login.ts方便统一管理,项目原有的路由前置守卫和logincheck()
写在了src\layout\index.vue布局页面:
login.ts:
export const PostThirdCode = async (
idToken: string,
registerType: number,
clientId: string
) => {
const response = await request.post(
"account/oauth",
JSON.stringify({
idToken: idToken,
clientId: clientId,
registerType: registerType,
platform: 5,
deviceID: "",
}),
{
headers: {
"Content-Type": "application/json",
},
}
);
const data = await response.data;
return data;
};
layout.vue:
onMounted(() => {
loginCheck();
});
router.beforeEach(async (to, from, next) => {
if (to.path === "/getCode") {
next();
return;
}
if (from.path === "/login") {
const { data } = await IMSDK.getLoginStatus<LoginStatus>();
if (data === LoginStatus.Logout) {
loginCheck();
}
}
next();
});
const GetAndPostThirdCode = async () => {
let currentThird = localStorage.getItem("thirdLogin");
//在localstorage拿到上一步设置好的三方登录模式
let clientId = localStorage.getItem("clientId") || "";
let registerType = null;
let type = null;
const { hash } = router.currentRoute.value;
//拿到redirect_url后面的参数
if (hash) {
if (currentThird === "GOOGLE") {
let idToken = hash.split("id_token=")[1]?.split("&")[0];
//拿到第三方服务器返回的id_token
type = "GOOGLE";
registerType = 3;
localStorage.setItem("GOOGLE_ID_TOKEN", idToken);
const data = await PostThirdCode(idToken, registerType, clientId);
const { chatToken, imToken, userID } = data;
//setIMProfile在原项目用来把登录信息存到本地
setIMProfile({ chatToken, imToken, userID });
localStorage.removeItem("GOOGLE_ID_TOKEN");
}
else if (currentThird === "APPLE") {
let idToken = hash.split("id_token=")[1]?.split("&")[0];
type = "APPLE";
registerType = 4;
localStorage.setItem("APPLE_ID_TOKEN", idToken);
const data = await PostThirdCode(idToken, registerType, clientId);
const { chatToken, imToken, userID } = data;
setIMProfile({ chatToken, imToken, userID });
localStorage.removeItem("APPLE_ID_TOKEN");
}
else if (currentThird === "FACEBOOK") {
type = "FACEBOOK";
registerType = 5;
localStorage.setItem("FACEBOOK_ID_TOKEN", idToken);
const data = await PostThirdCode(idToken, registerType, clientId);
const { chatToken, imToken, userID } = data;
setIMProfile({ chatToken, imToken, userID });
localStorage.removeItem("FACEBOOK_ID_TOKEN");
}
localStorage.removeItem("thirdLogin");
localStorage.removeItem("clientId");
//清除用到的localstorage
}
};
const loginCheck = async () => {
if (localStorage.getItem("thirdLogin")) await GetAndPostThirdCode();
//如果是三方登录就走上面逻辑拿到项目自己的登录相关参数
//拿到后走原有的逻辑
const IMToken = getIMToken();
const IMUserID = getIMUserID();
if (!IMToken || !IMUserID) {
router.push("/login");
return;
}
tryLogin();
};
四.登录成功
到这里三方登录已经成功了,在项目里的myProfile页面应该可以看到第三方平台的昵称和头像。
五.小结
事实上,登录流程远没有这么简单,程序里还没有加上这些判断:当用户取消授权时就不能让其登录;授权失败的第三方平台抛出异常还没有处理,这些都是未来需改进处。
同时因为时间紧迫,截止此文发出时仅跑通了Google登录的相关逻辑,不过一通百通,我的代码组织模式决定了只要修改配置其他的三方登录也很快就能实现。