飞书扫码登录全流程,扫码一秒变大佬!

775 阅读6分钟

背景

这段时间我在开发后台管理系统,遇到一个挺有意思的需求。甲方要求系统必须支持飞书扫码登录。查阅飞书的相关文档后,发现这个功能非常有趣,但在实现过程中也遇到了一些坑点。借此机会跟大家分享一下,希望能帮大家避免踩坑,关键的时刻能派上用场

前期准备

  1. 注册飞书开发者账号

    访问飞书开放平台,注册并登录开发者账号

  2. 创建应用 在开发者后台创建一个企业自建应用,创建完成之后点击应用,记录一下App ID 和 App Secret

    image.png

    image.png

  3. 配置权限 在“权限管理”中配置以下权限,直接复制下面的 JSON 导入即可

    {
      "scopes": {
        "tenant": [
          "im:message:send_as_bot",
          "docx:document:readonly"
        ],
        "user": [
          "docx:document:readonly"
        ]
      }
    }
    

    image.png

  4. 配置重定向 URI 在“安全设置”中配置扫码登录的回调地址,根据项目实际地址添加(开发和生产环境都需要)

    image.png

扫码登录流程

飞书扫码登录的流程如下

  1. 获取二维码:前端页面首先向后端请求生成飞书扫码登录的二维码,并将二维码展示给用户
  2. 用户扫码:用户使用飞书 App 扫描二维码。此时会有两种情况:
    • 如果二维码已过期,系统会重新获取二维码,提示用户刷新二维码后再次扫码
    • 如果扫码用户不是企业成员,则扫码失败,提示用户无法登录
  3. 用户授权:企业成员扫码后,若用户同意授权,飞书会将用户重定向到预先配置的登录回调地址,并携带授权 code
  4. 后端处理:后端收到回调请求后,通过 code 向飞书服务器换取用户 token
  5. 获取用户信息:后端再通过 token 获取用户的详细信息,完成登录流程

image.png

前端代码实现

引入飞书官方提供的扫码登录二维码生成 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_idredirect_uri、样式等)
  • 注册 message 事件监听器,监听扫码后的消息
  • 如果初始化失败,设置错误提示

handleMessage:这是一个事件监听函数,用于接收来自飞书二维码登录组件的消息。当用户用飞书 App 扫码后,二维码组件会通过 postMessage 发送消息到页面

  • 该方法首先通过 matchOrigin 和 matchData 校验消息来源和内容的合法性,确保安全
  • 如果校验通过,提取消息中的 tmp_code,并拼接 OAuth 授权 URL,带上 tmp_code 跳转到飞书授权页面,完成后续的登录流程

handleLogin:处理登录逻辑的方法

当页面 URL 中检测到有 code 参数时(即用户扫码并授权后,飞书回调带回的 code),调用此方法

页面效果

初始化二维码

image.png

授权中

image.png

授权成功

image.png

后端代码实现

后端以 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_inaccess_token 的有效期
refresh_expires_inrefresh_token 的有效期
scope权限范围

获取飞书用户信息接口返回值

字段名 含义说明 
avatar_big 用户大尺寸头像图片的 URL(如 640x640 像素),适合在大头像展示场景使用。 
avatar_middle用户中等尺寸头像图片的 URL(如 240x240 像素),适合在普通头像展示场景使用。
avatar_thumb用户小尺寸头像图片的 URL(如 72x72 像素),适合在缩略图、列表等小头像场景使用。 
avatar_url 用户头像图片的 URL,通常为默认尺寸(一般与 avatar_thumb 相同)。 
email 用户的邮箱地址(如未设置则为空)。
en_name 用户的英文名。 
mobile 用户的手机号,通常带有国家区号(如 +86 开头)。
name用户的中文名或显示名。 
open_id 用户在当前应用下的唯一标识(Open ID),用于标识用户身份,适合单应用内唯一性需求。 
tenant_key 用户所属企业(租户)的唯一标识。 
union_id 用户在同一开放平台下所有应用的唯一标识(Union ID),适合多应用间用户身份打通场景。

补充

飞书上面加入企业的可以忽略

上述步骤如果你的飞书上面没有加入到企业,那么这个二维码只能自己使用

其他用户扫码会出现以下提示 af7aea2fdc180133ccd6223f54573e4.png

如果你没有企业而又想让别人也可以扫码授权,你可以在“测试企业和人员”中创建一个测试企业,并邀请其他人加入测试企业即可(注意:要使用测试企业身份下的账号扫码登录,飞书 APP 点击用户头像可以切换身份

image.png

以上就是整个项目接入飞书扫码登录的全过程,后续大家有同样的需求希望可以用到,感谢大家的支持!!!