背景
这段时间我在开发后台管理系统,遇到一个挺有意思的需求。甲方要求系统必须支持飞书扫码登录。查阅飞书的相关文档后,发现这个功能非常有趣,但在实现过程中也遇到了一些坑点。借此机会跟大家分享一下,希望能帮大家避免踩坑,关键的时刻能派上用场
前期准备
-
注册飞书开发者账号
访问飞书开放平台,注册并登录开发者账号
-
创建应用 在开发者后台创建一个企业自建应用,创建完成之后点击应用,记录一下App ID 和 App Secret
-
配置权限 在“权限管理”中配置以下权限,直接复制下面的
JSON
导入即可{ "scopes": { "tenant": [ "im:message:send_as_bot", "docx:document:readonly" ], "user": [ "docx:document:readonly" ] } }
-
配置重定向 URI 在“安全设置”中配置扫码登录的回调地址,根据项目实际地址添加(开发和生产环境都需要)
扫码登录流程
飞书扫码登录的流程如下
- 获取二维码:前端页面首先向后端请求生成飞书扫码登录的二维码,并将二维码展示给用户
- 用户扫码:用户使用飞书 App 扫描二维码。此时会有两种情况:
- 如果二维码已过期,系统会重新获取二维码,提示用户刷新二维码后再次扫码
- 如果扫码用户不是企业成员,则扫码失败,提示用户无法登录
- 用户授权:企业成员扫码后,若用户同意授权,飞书会将用户重定向到预先配置的登录回调地址,并携带授权 code
- 后端处理:后端收到回调请求后,通过 code 向飞书服务器换取用户 token
- 获取用户信息:后端再通过 token 获取用户的详细信息,完成登录流程
前端代码实现
引入飞书官方提供的扫码登录二维码生成 SDK
// index.html
<script src="https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js"></script>
登录页面
// login.vue
<script setup lang="ts">
import {
ref,
reactive,
toRaw,
onMounted,
onBeforeUnmount,
} from "vue";
const loading = ref(false);
const error = ref("");
const QRLoginObj = ref(null);
const appId = ref("cli_a779fa0e7ff9100e");
const redirectUri = ref("http://localhost:8848/callback");
// 监听扫码登录消息
const handleMessage = event => {
// 使用 matchOrigin 和 matchData 方法判断消息和来源是否合法
if (
QRLoginObj.value.matchOrigin(event.origin) &&
QRLoginObj.value.matchData(event.data)
) {
const loginTmpCode = event.data.tmp_code;
const goto = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${appId.value}&redirect_uri=${redirectUri.value}&response_type=code&state=custom_state`;
window.location.href = `${goto}&tmp_code=${loginTmpCode}`;
}
};
// 初始化二维码登录
const initQrLogin = async () => {
try {
QRLoginObj.value = window.QRLogin({
id: "login_container",
goto: `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${appId.value}&redirect_uri=${redirectUri.value}&response_type=code&state=custom_state`,
width: "250",
height: "300",
style: "margin: 0 auto;"
});
window.addEventListener("message", handleMessage, false);
} catch (err) {
console.error("初始化失败:", err);
error.value = "二维码加载失败,请刷新页面重试";
}
};
// 登录
const handleLogin = (code: string) => {
loading.value = true;
// 清空 URL 中的参数
// window.history.replaceState({}, '', window.location.pathname);
// 这里可以调用你的后端接口处理 code
}
onMounted(() => {
// 检查 URL 中是否有 code 参数
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
if (code) {
// 登录
handleLogin(code);
} else {
// 如果没有 code 参数,初始化二维码登录
initQrLogin();
}
});
onBeforeUnmount(() => {
window.removeEventListener("message", handleMessage);
});
</script>
<template>
<div class="select-none">
<h2 class="outline-none">飞书扫码登录</h2>
<div id="login_container" />
<div v-if="error" class="error-message">{{ error }}</div>
</div>
</template>
<style lang="scss" scoped>
.select-none {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
主要有以下三个方法构成:
initQrLogin:初始化飞书扫码登录二维码的方法
- 调用
window.QRLogin
方法,在页面指定的容器(login_container
)中渲染二维码 - 配置二维码的参数(如
client_id
、redirect_uri
、样式等) - 注册
message
事件监听器,监听扫码后的消息 - 如果初始化失败,设置错误提示
handleMessage:这是一个事件监听函数,用于接收来自飞书二维码登录组件的消息。当用户用飞书 App 扫码后,二维码组件会通过 postMessage
发送消息到页面
- 该方法首先通过
matchOrigin
和matchData
校验消息来源和内容的合法性,确保安全 - 如果校验通过,提取消息中的
tmp_code
,并拼接 OAuth 授权 URL,带上tmp_code
跳转到飞书授权页面,完成后续的登录流程
handleLogin:处理登录逻辑的方法
当页面 URL 中检测到有 code 参数时(即用户扫码并授权后,飞书回调带回的 code
),调用此方法
页面效果
初始化二维码
授权中
授权成功
后端代码实现
后端以 node 举例
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
// 用于接收前端传来的 tmp_code
app.post('/api/feishu/login', async (req, res) => {
const { tmp_code } = req.body;
try {
// 1. 用 tmp_code 换取 access_token
const tokenRes = await axios.post('https://passport.feishu.cn/suite/passport/oauth/token', {
grant_type: 'authorization_code',
code: tmp_code,
client_id: 'cli_a779fa0e7ff9100e', // 你的应用 ID
client_secret: 'xO1bZeQYlAM1nPYQS8Q9shdhj8FjQme5', // 你的应用密钥
}, {
headers: {
'Content-Type': 'application/json'
}
});
const access_token = tokenRes.data.access_token;
// 2. 用 access_token 获取用户信息
const userInfoRes = await axios.get('https://open.feishu.cn/open-apis/authen/v1/user_info', {
headers: {
'Authorization': `Bearer ${access_token}`
}
});
const userInfo = userInfoRes.data;
// 3. 这里可以根据 userInfo 进行你自己的登录逻辑
// 例如:查找或创建本地用户,生成 session/token 等
res.json({
success: true,
user: userInfo.data
});
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: '飞书登录失败' });
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
获取token接口返回值
字段名 | 含义 |
---|---|
access_token | 访问令牌 |
refresh_token | 刷新令牌 |
token_type | 令牌类型 |
expires_in | access_token 的有效期 |
refresh_expires_in | refresh_token 的有效期 |
scope | 权限范围 |
获取飞书用户信息接口返回值
字段名 | 含义说明 |
---|---|
avatar_big | 用户大尺寸头像图片的 URL(如 640x640 像素),适合在大头像展示场景使用。 |
avatar_middle | 用户中等尺寸头像图片的 URL(如 240x240 像素),适合在普通头像展示场景使用。 |
avatar_thumb | 用户小尺寸头像图片的 URL(如 72x72 像素),适合在缩略图、列表等小头像场景使用。 |
avatar_url | 用户头像图片的 URL,通常为默认尺寸(一般与 avatar_thumb 相同)。 |
用户的邮箱地址(如未设置则为空)。 | |
en_name | 用户的英文名。 |
mobile | 用户的手机号,通常带有国家区号(如 +86 开头)。 |
name | 用户的中文名或显示名。 |
open_id | 用户在当前应用下的唯一标识(Open ID),用于标识用户身份,适合单应用内唯一性需求。 |
tenant_key | 用户所属企业(租户)的唯一标识。 |
union_id | 用户在同一开放平台下所有应用的唯一标识(Union ID),适合多应用间用户身份打通场景。 |
补充
飞书上面加入企业的可以忽略
上述步骤如果你的飞书上面没有加入到企业,那么这个二维码只能自己使用
其他用户扫码会出现以下提示
如果你没有企业而又想让别人也可以扫码授权,你可以在“测试企业和人员”中创建一个测试企业,并邀请其他人加入测试企业即可(注意:要使用测试企业身份下的账号扫码登录,飞书 APP 点击用户头像可以切换身份)
以上就是整个项目接入飞书扫码登录的全过程,后续大家有同样的需求希望可以用到,感谢大家的支持!!!