feishu-login-integration.md 24 KB

飞书登录集成指南

完整的飞书登录解决方案,支持客户端免登录、二维码扫码登录和账号密码登录

目录

概述

本项目实现了完整的飞书登录功能,支持三种登录方式:

  1. 飞书客户端内免登录:在飞书 App 内自动获取用户信息
  2. 飞书二维码扫码登录:在浏览器中使用飞书扫码登录
  3. 账号密码登录:传统的用户名密码登录方式

所有登录方式都与 Parse 用户系统无缝集成,提供统一的用户认证体验。

功能特性

  • ✅ 环境自动检测(飞书客户端 vs 浏览器)
  • ✅ 三种登录方式无缝切换
  • ✅ Parse 用户系统集成
  • ✅ 会话自动保存和恢复
  • ✅ 路由守卫保护
  • ✅ 统一的 OAuth2 接口
  • ✅ 优雅的 UI 和动画效果
  • ✅ 响应式设计
  • ✅ TypeScript 类型支持

架构设计

核心组件

src/app/
├── core/
│   ├── services/
│   │   ├── feishu-auth.service.ts    # 飞书认证服务
│   │   └── auth.service.ts           # 应用认证服务
│   └── guards/
│       └── auth.guard.ts             # 路由守卫
└── modules/
    └── login/
        ├── login.component.ts        # 登录组件
        ├── login.component.html      # 登录模板
        └── login.component.scss      # 登录样式

登录流程

1. 飞书客户端内免登录

用户在飞书 App 内打开应用
    ↓
检测到飞书环境
    ↓
调用飞书 JSSDK 获取授权码
    ↓
发送授权码到后端 API
    ↓
后端返回用户信息和 sessionToken
    ↓
使用 sessionToken 登录 Parse
    ↓
更新 AuthService 认证状态
    ↓
跳转到首页

2. 飞书二维码扫码登录

用户在浏览器中打开应用
    ↓
显示飞书二维码
    ↓
用户使用飞书扫码
    ↓
二维码 SDK 返回 tmp_code
    ↓
重定向到飞书授权页面(携带 tmp_code)
    ↓
飞书服务器处理后重定向回应用(携带 code)
    ↓
发送 code 到后端 API
    ↓
后端返回用户信息和 sessionToken
    ↓
使用 sessionToken 登录 Parse
    ↓
更新 AuthService 认证状态
    ↓
跳转到首页

3. 账号密码登录

用户输入用户名和密码
    ↓
调用 Parse.User.logIn()
    ↓
登录成功,获取 Parse User
    ↓
更新 AuthService 认证状态
    ↓
跳转到首页

SDK 详细说明

概述

feishu-auth.service.ts 依赖三个核心 SDK 来实现完整的登录功能:

  1. 飞书客户端 JSSDK - 用于飞书 App 内免登录
  2. 飞书二维码登录 SDK - 用于浏览器扫码登录
  3. Parse SDK - 用于用户系统集成

1. 飞书客户端 JSSDK

SDK 信息

  • CDN 地址: https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.44.js
  • 全局对象: window.tt
  • 用途: 在飞书客户端内获取用户授权码
  • 版本: 1.5.44

加载方式

private loadFeishuSDK(): Promise<void> {
  return new Promise((resolve, reject) => {
    // 检查飞书客户端 JSSDK 是否已加载
    if (window['tt']) {
      resolve();
      return;
    }

    const script = document.createElement('script');
    script.src = 'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.44.js';
    script.onload = () => resolve();
    script.onerror = () => reject(new Error('Failed to load Feishu SDK'));
    document.head.appendChild(script);
  });
}

核心方法

tt.requestAccess()

获取飞书客户端内的用户授权码。

参数:

{
  appID: string;           // 飞书应用 ID
  scopeList: string[];     // 权限范围列表(可为空)
  success: (res: any) => void;  // 成功回调
  fail: (error: any) => void;   // 失败回调
}

使用示例:

window.tt.requestAccess({
  appID: 'cli_a9253658eef99cd2',
  scopeList: [],
  success: (res) => {
    const code = res.code;  // 获取授权码
    console.log('授权码:', code);
  },
  fail: (error) => {
    console.error('获取授权码失败:', error);
  }
});

返回值:

{
  code: string;  // 授权码,用于后端换取用户信息
}

环境检测

isInFeishuApp(): boolean {
  // 检查 window.tt 是否存在
  if (typeof window['tt'] !== 'undefined' && window['tt'].requestAccess) {
    return true;
  }

  // 检查 User Agent
  const ua = navigator.userAgent.toLowerCase();
  return ua.includes('lark') || ua.includes('feishu');
}

2. 飞书二维码登录 SDK

SDK 信息

  • CDN 地址: https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js
  • 全局函数: window.QRLogin
  • 用途: 在浏览器中生成飞书登录二维码
  • 版本: 1.0.3

加载方式

private loadQRCodeSDK(): Promise<void> {
  return new Promise((resolve, reject) => {
    // 检查二维码 SDK 是否已加载
    if (window['QRLogin']) {
      resolve();
      return;
    }

    const script = document.createElement('script');
    script.src = 'https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js';
    script.onload = () => resolve();
    script.onerror = () => reject(new Error('Failed to load Feishu QRCode SDK'));
    document.head.appendChild(script);
  });
}

核心方法

QRLogin()

初始化并显示飞书登录二维码。

参数:

{
  id: string;      // 二维码容器的 DOM ID
  goto: string;    // 授权跳转 URL
  width: string;   // 二维码宽度(如 "300")
  height: string;  // 二维码高度(如 "300")
}

使用示例:

// 构造授权 URL
const goto = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${appId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&state=success_login`;

// 初始化二维码
const QRLoginObj = window.QRLogin({
  id: 'qrcode-container',
  goto: goto,
  width: "300",
  height: "300"
});

返回值:

{
  matchOrigin: (origin: string) => boolean;  // 验证消息来源
  matchData: (data: any) => boolean;         // 验证消息数据
}

监听扫码结果

二维码 SDK 通过 postMessage 发送扫码结果:

window.addEventListener('message', (event: MessageEvent) => {
  // 验证消息来源和数据
  if (QRLoginObj.matchOrigin(event.origin) &&
      QRLoginObj.matchData(event.data)) {

    const tmp_code = event.data.tmp_code;  // 临时码

    // 使用 tmp_code 构造重定向 URL
    const redirectUrl = `${goto}&tmp_code=${tmp_code}`;
    window.location.href = redirectUrl;
  }
}, false);

重要说明:

  • tmp_code 不是最终的授权码,只是临时码
  • 需要用 tmp_code 重定向到飞书服务器
  • 飞书服务器处理后会重定向回应用,携带真正的授权码 code

授权 URL 格式

https://passport.feishu.cn/suite/passport/oauth/authorize
  ?client_id=<APP_ID>
  &redirect_uri=<REDIRECT_URI>
  &response_type=code
  &state=<STATE>

参数说明:

  • client_id: 飞书应用 ID
  • redirect_uri: 授权回调地址(需 URL 编码)
  • response_type: 固定为 code
  • state: 状态参数,用于区分登录类型(如 success_login 表示二维码登录)

3. Parse SDK

SDK 信息

  • 导入方式: import { FmodeParse as Parse } from 'fmode-ng/core/parse'
  • 用途: 用户系统集成和会话管理
  • 版本: 由 fmode-ng 提供

初始化

import { FmodeParse as Parse } from 'fmode-ng/core/parse';

// 动态获取 Parse 实例
const Parse = FmodeParse.with(PARSE_APP_ID);

核心方法

Parse.User.become()

使用 sessionToken 恢复用户会话。

参数:

sessionToken: string  // Parse 会话令牌

使用示例:

async loginWithSessionToken(sessionToken: string): Promise<any> {
  try {
    // 使用 sessionToken 成为当前用户
    const user = await Parse.User.become(sessionToken);

    console.log('Parse 用户登录成功:', user.id);

    return {
      parseUser: user,
      sessionToken: sessionToken,
      userId: user.id,
      username: user.get('username')
    };
  } catch (error) {
    console.error('Parse 用户登录失败:', error);
    throw error;
  }
}
Parse.User.logIn()

使用用户名和密码登录。

参数:

username: string  // 用户名
password: string  // 密码

使用示例:

async webLogin(username: string, password: string): Promise<AuthResult> {
  try {
    // 使用 Parse.User.logIn() 进行登录
    const user = await Parse.User.logIn(username, password);

    // 获取用户 sessionToken
    const sessionToken = user.getSessionToken();

    return {
      success: true,
      userInfo: {
        userId: user.id,
        username: user.get('username'),
        sessionToken: sessionToken
      },
      loginType: 'web'
    };
  } catch (error) {
    return {
      success: false,
      error: error.message,
      loginType: 'web'
    };
  }
}
Parse.User.logOut()

退出登录,清除当前用户会话。

使用示例:

async logout(): Promise<void> {
  // 使用 Parse SDK 登出
  await Parse.User.logOut();

  // 清除本地存储的用户信息
  localStorage.removeItem('feishu_user_info');
  localStorage.removeItem('feishu_session_token');
  localStorage.removeItem('feishu_login_time');
}
Parse.User.current()

获取当前登录的用户。

使用示例:

async getCurrentParseUser(): Promise<any> {
  try {
    return Parse.User.current();
  } catch (error) {
    console.warn('获取当前 Parse 用户失败:', error);
    return null;
  }
}

SDK 类型声明

为了在 TypeScript 中使用这些 SDK,需要添加全局类型声明:

// 飞书 SDK 全局类型声明
declare global {
  interface Window {
    tt?: any;       // 飞书客户端 JSSDK
    QRLogin?: any;  // 飞书二维码登录 SDK
  }
}

SDK 加载时机

自动加载

FeishuAuth 构造函数会自动加载飞书客户端 JSSDK:

constructor(config: FeishuAuthConfig) {
  this.config = {
    appId: config.appId,
    redirectUri: config.redirectUri,
    apiOauthWeb: config.apiOauthWeb || 'http://localhost:3000/api/feishu-v2/oauth2/login'
  };

  // 自动加载飞书 SDK
  this.loadFeishuSDK();
}

按需加载

二维码 SDK 只在需要时加载:

initQRCodeLogin(containerId: string, onSuccess, onError): void {
  // 确保 SDK 已加载
  this.loadQRCodeSDK().then(() => {
    // 初始化二维码
    const QRLoginObj = window.QRLogin({...});
  });
}

SDK 错误处理

SDK 加载失败

try {
  await this.loadFeishuSDK();
} catch (error) {
  console.error('加载飞书 SDK 失败:', error);
  // 降级处理:使用网页登录
}

API 调用失败

try {
  window.tt.requestAccess({
    appID: this.config.appId,
    scopeList: [],
    success: (res) => {
      // 处理成功
    },
    fail: (error) => {
      console.error('获取授权码失败:', error);
      // 错误处理
    }
  });
} catch (error) {
  console.error('飞书 JSSDK 调用失败:', error);
  // 降级处理
}

SDK 兼容性

浏览器兼容性

  • 飞书客户端 JSSDK: 仅在飞书 App 内可用
  • 飞书二维码登录 SDK: 支持所有现代浏览器(Chrome, Firefox, Safari, Edge)
  • Parse SDK: 支持所有现代浏览器

移动端支持

  • ✅ 飞书客户端内免登录:支持 iOS 和 Android
  • ✅ 二维码扫码登录:支持移动浏览器
  • ✅ 账号密码登录:支持所有平台

快速开始

1. 安装依赖

确保项目已安装以下依赖:

npm install fmode-ng

2. 配置飞书应用

在飞书开发者后台配置:

  1. 应用凭证

    • 获取 App ID
    • 获取 App Secret
  2. 安全设置

    • 配置重定向 URL:https://your-domain.com/login
    • 配置 H5 可信域名:your-domain.com
  3. 权限配置

    • 开通 auth:user.id:read
    • 开通 auth:user:email:read(如需邮箱)

3. 复制核心文件

将以下文件复制到你的项目:

src/app/core/services/feishu-auth.service.ts
src/app/modules/login/login.component.ts
src/app/modules/login/login.component.html
src/app/modules/login/login.component.scss

4. 配置应用

login.component.ts 中配置飞书应用信息:

this.feishuAuth = new FeishuAuth({
  appId: 'YOUR_FEISHU_APP_ID',
  redirectUri: window.location.origin + '/login',
  apiOauthWeb: 'https://your-backend.com/api/feishu-v2/oauth2/login'
});

5. 配置路由

app.routes.ts 中添加登录路由:

export const routes: Routes = [
  {
    path: 'login',
    component: LoginComponent
  },
  {
    path: 'homepage',
    component: HomepageComponent,
    canActivate: [authGuard]
  }
];

详细配置

FeishuAuthConfig 接口

export interface FeishuAuthConfig {
  appId: string;           // 飞书应用 App ID(必填)
  redirectUri: string;     // 回调地址(必填)
  apiOauthWeb?: string;    // OAuth2 登录接口(可选)
}

默认配置

如果不指定 apiOauthWeb,将使用默认值:

apiOauthWeb: 'http://localhost:3000/api/feishu-v2/oauth2/login'

后端 API 接口

后端需要提供以下接口:

POST /api/feishu-v2/oauth2/login

请求参数:

{
  "appId": "cli_a9253658eef99cd2",
  "code": "授权码",
  "redirect_uri": "https://your-domain.com/login"
}

响应格式:

{
  "code": 1,
  "data": {
    "userInfo": {
      "objectId": "用户ID",
      "username": "用户名",
      "nickname": "昵称",
      "avatar": "头像URL",
      "email": "邮箱",
      "mobile": "手机号",
      "openId": "飞书 OpenID",
      "unionId": "飞书 UnionID"
    },
    "sessionToken": "Parse Session Token"
  },
  "mess": "登录成功"
}

使用指南

在登录组件中使用

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthService } from '../../core/services/auth.service';
import { FeishuAuth, AuthResult } from '../../core/services/feishu-auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
  private feishuAuth: FeishuAuth;

  constructor(
    private authService: AuthService,
    private router: Router,
    private activatedRoute: ActivatedRoute
  ) {
    // 初始化飞书认证服务
    this.feishuAuth = new FeishuAuth({
      appId: 'YOUR_FEISHU_APP_ID',
      redirectUri: window.location.origin + '/login'
    });
  }

  async ngOnInit(): Promise<void> {
    // 检查是否有回调参数
    const code = this.activatedRoute.snapshot.queryParamMap.get('code');
    const state = this.activatedRoute.snapshot.queryParamMap.get('state');

    if (code) {
      await this.handleAuthCallback(code, state || '');
    }
  }

  // 处理认证回调
  private async handleAuthCallback(code: string, state: string): Promise<void> {
    const result = await this.feishuAuth.handleAuthCallback(code, state);

    if (result.success) {
      await this.handleLoginSuccess(result);
    }
  }

  // 处理登录成功
  private async handleLoginSuccess(result: AuthResult): Promise<void> {
    // 保存用户信息
    if (result.userInfo) {
      this.feishuAuth.saveUserInfo(result.userInfo);
    }

    // 刷新认证状态
    await this.authService.refreshAuthState();

    // 跳转到首页
    await this.router.navigate(['/homepage']);
  }
}

环境检测

// 检查是否在飞书客户端内
if (this.feishuAuth.isInFeishuApp()) {
  console.log('在飞书客户端内');
} else {
  console.log('在浏览器中');
}

飞书客户端内免登录

async handleFeishuLogin(): Promise<void> {
  const result = await this.feishuAuth.getFeishuAuthCode();

  if (result.success) {
    await this.handleLoginSuccess(result);
  } else {
    console.error('登录失败:', result.error);
  }
}

二维码登录

initQRCodeLogin(): void {
  this.feishuAuth.initQRCodeLogin(
    'qrcode-container',
    async (result: AuthResult) => {
      if (result.success) {
        await this.handleLoginSuccess(result);
      }
    },
    (error: string) => {
      console.error('二维码登录失败:', error);
    }
  );
}

账号密码登录

async handleWebLogin(username: string, password: string): Promise<void> {
  const result = await this.feishuAuth.webLogin(username, password);

  if (result.success) {
    await this.handleLoginSuccess(result);
  } else {
    console.error('登录失败:', result.error);
  }
}

API 参考

FeishuAuth 类

构造函数

constructor(config: FeishuAuthConfig)

方法

isInFeishuApp(): boolean

检查是否在飞书客户端环境内。

返回值:

  • true:在飞书客户端内
  • false:在浏览器中
getFeishuAuthCode(): Promise

飞书客户端内免登录,获取授权码并完成登录。

返回值:

{
  success: boolean;
  userInfo?: UserInfo;
  error?: string;
  loginType: 'feishu' | 'web' | 'qrcode';
}
initQRCodeLogin(containerId, onSuccess, onError): void

初始化二维码登录。

参数:

  • containerId: string - 二维码容器的 DOM ID
  • onSuccess: (result: AuthResult) => void - 登录成功回调
  • onError: (error: string) => void - 登录失败回调
handleAuthCallback(code, state): Promise

处理飞书授权回调。

参数:

  • code: string - 授权码
  • state: string - 状态参数

返回值:

{
  success: boolean;
  userInfo?: UserInfo;
  error?: string;
  loginType: 'feishu' | 'web' | 'qrcode';
}
webLogin(username, password): Promise

账号密码登录。

参数:

  • username: string - 用户名
  • password: string - 密码

返回值:

{
  success: boolean;
  userInfo?: UserInfo;
  error?: string;
  loginType: 'web';
}
saveUserInfo(userInfo): void

保存用户信息到 localStorage。

参数:

  • userInfo: any - 用户信息对象
getUserInfo(): any

从 localStorage 获取用户信息。

返回值:

  • 用户信息对象,如果未登录则返回 null
isLoggedIn(): boolean

检查用户是否已登录(基于 localStorage)。

返回值:

  • true:已登录且未过期(24小时内)
  • false:未登录或已过期
logout(): Promise

退出登录,清除所有本地存储和 Parse 会话。

validateConfig(): { isValid: boolean; errors: string[] }

验证飞书应用配置。

返回值:

{
  isValid: boolean;
  errors: string[];
}

AuthResult 接口

interface AuthResult {
  success: boolean;
  code?: string;
  userInfo?: {
    userId: string;
    username: string;
    avatar: string;
    nickname?: string;
    email?: string;
    mobile?: string;
    sessionToken?: string;
    openId?: string;
    unionId?: string;
    rawUserInfo?: any;
    parseUser?: any;
  };
  error?: string;
  loginType: 'feishu' | 'web' | 'qrcode';
}

常见问题

Q1: 二维码登录后无法跳转到首页?

原因: AuthService 的认证状态没有更新,路由守卫拒绝跳转。

解决方案: 在登录成功后调用 authService.refreshAuthState()

private async handleLoginSuccess(result: AuthResult): Promise<void> {
  if (result.userInfo) {
    this.feishuAuth.saveUserInfo(result.userInfo);
  }

  // 刷新认证状态
  await this.authService.refreshAuthState();

  await this.router.navigate(['/homepage']);
}

Q2: 二维码登录报错 4401?

原因: 使用了旧版登录流程的 API 端点。

解决方案: 确保使用旧版 goto URL(当前实现已正确):

const goto = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${appId}&redirect_uri=${redirectUri}&response_type=code&state=success_login`;

Q3: 如何区分二维码登录和客户端登录?

解决方案: 通过 state 参数判断:

// state 为 'success_login' 表示二维码登录
const loginType = state === 'success_login' ? 'qrcode' : 'feishu';

Q4: 如何自定义后端 API 地址?

解决方案: 在初始化时指定 apiOauthWeb

this.feishuAuth = new FeishuAuth({
  appId: 'YOUR_FEISHU_APP_ID',
  redirectUri: window.location.origin + '/login',
  apiOauthWeb: 'https://your-backend.com/api/feishu-v2/oauth2/login'
});

Q5: 如何处理登录过期?

解决方案: FeishuAuth 内置了 24 小时过期检查:

if (!this.feishuAuth.isLoggedIn()) {
  // 登录已过期,跳转到登录页
  this.router.navigate(['/login']);
}

最佳实践

1. 环境配置管理

使用环境变量管理不同环境的配置:

// environment.ts
export const environment = {
  production: false,
  feishu: {
    appId: 'dev_app_id',
    apiOauthWeb: 'http://localhost:3000/api/feishu-v2/oauth2/login'
  }
};

// environment.prod.ts
export const environment = {
  production: true,
  feishu: {
    appId: 'prod_app_id',
    apiOauthWeb: 'https://api.your-domain.com/api/feishu-v2/oauth2/login'
  }
};

// 使用
this.feishuAuth = new FeishuAuth({
  appId: environment.feishu.appId,
  redirectUri: window.location.origin + '/login',
  apiOauthWeb: environment.feishu.apiOauthWeb
});

2. 错误处理

统一处理登录错误:

private async handleLoginError(error: string): Promise<void> {
  console.error('登录失败:', error);

  // 显示错误提示
  this.errorMessage.set(error);

  // 记录错误日志
  this.logError('login_failed', { error });
}

3. 加载状态管理

使用 signal 管理加载状态:

loading = signal(false);

async handleFeishuLogin(): Promise<void> {
  this.loading.set(true);

  try {
    const result = await this.feishuAuth.getFeishuAuthCode();
    // 处理结果
  } finally {
    this.loading.set(false);
  }
}

4. 路由守卫配置

确保所有需要登录的路由都配置了守卫:

export const routes: Routes = [
  {
    path: 'login',
    component: LoginComponent
  },
  {
    path: '',
    canActivate: [authGuard],
    children: [
      { path: 'homepage', component: HomepageComponent },
      { path: 'profile', component: ProfileComponent }
    ]
  }
];

5. 会话恢复

在应用启动时自动恢复会话:

// app.component.ts
async ngOnInit(): Promise<void> {
  // AuthService 会自动从 Parse 恢复会话
  await this.authService.refreshAuthState();
}

6. 安全最佳实践

  • 不要在前端代码中硬编码 App Secret
  • 使用 HTTPS 传输敏感信息
  • 定期更新 sessionToken
  • 实现登录失败次数限制
  • 记录登录日志用于安全审计

7. 用户体验优化

  • 根据环境自动选择登录方式
  • 提供清晰的错误提示
  • 添加加载动画
  • 支持记住登录状态
  • 提供退出登录功能

技术栈

  • Angular 17+
  • TypeScript 5.0+
  • Parse SDK
  • 飞书 JSSDK
  • 飞书二维码登录 SDK

许可证

MIT

支持

如有问题或建议,请联系开发团队。