# 飞书登录集成指南 > 完整的飞书登录解决方案,支持客户端免登录、二维码扫码登录和账号密码登录 ## 目录 - [概述](#概述) - [功能特性](#功能特性) - [架构设计](#架构设计) - [快速开始](#快速开始) - [详细配置](#详细配置) - [使用指南](#使用指南) - [API 参考](#api-参考) - [常见问题](#常见问题) - [最佳实践](#最佳实践) ## 概述 本项目实现了完整的飞书登录功能,支持三种登录方式: 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 #### 加载方式 ```typescript private loadFeishuSDK(): Promise { 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() 获取飞书客户端内的用户授权码。 **参数:** ```typescript { appID: string; // 飞书应用 ID scopeList: string[]; // 权限范围列表(可为空) success: (res: any) => void; // 成功回调 fail: (error: any) => void; // 失败回调 } ``` **使用示例:** ```typescript window.tt.requestAccess({ appID: 'cli_a9253658eef99cd2', scopeList: [], success: (res) => { const code = res.code; // 获取授权码 console.log('授权码:', code); }, fail: (error) => { console.error('获取授权码失败:', error); } }); ``` **返回值:** ```typescript { code: string; // 授权码,用于后端换取用户信息 } ``` #### 环境检测 ```typescript 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 #### 加载方式 ```typescript private loadQRCodeSDK(): Promise { 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() 初始化并显示飞书登录二维码。 **参数:** ```typescript { id: string; // 二维码容器的 DOM ID goto: string; // 授权跳转 URL width: string; // 二维码宽度(如 "300") height: string; // 二维码高度(如 "300") } ``` **使用示例:** ```typescript // 构造授权 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" }); ``` **返回值:** ```typescript { matchOrigin: (origin: string) => boolean; // 验证消息来源 matchData: (data: any) => boolean; // 验证消息数据 } ``` #### 监听扫码结果 二维码 SDK 通过 `postMessage` 发送扫码结果: ```typescript 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= &redirect_uri= &response_type=code &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 提供 #### 初始化 ```typescript import { FmodeParse as Parse } from 'fmode-ng/core/parse'; // 动态获取 Parse 实例 const Parse = FmodeParse.with(PARSE_APP_ID); ``` #### 核心方法 ##### Parse.User.become() 使用 sessionToken 恢复用户会话。 **参数:** ```typescript sessionToken: string // Parse 会话令牌 ``` **使用示例:** ```typescript async loginWithSessionToken(sessionToken: string): Promise { 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() 使用用户名和密码登录。 **参数:** ```typescript username: string // 用户名 password: string // 密码 ``` **使用示例:** ```typescript async webLogin(username: string, password: string): Promise { 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() 退出登录,清除当前用户会话。 **使用示例:** ```typescript async logout(): Promise { // 使用 Parse SDK 登出 await Parse.User.logOut(); // 清除本地存储的用户信息 localStorage.removeItem('feishu_user_info'); localStorage.removeItem('feishu_session_token'); localStorage.removeItem('feishu_login_time'); } ``` ##### Parse.User.current() 获取当前登录的用户。 **使用示例:** ```typescript async getCurrentParseUser(): Promise { try { return Parse.User.current(); } catch (error) { console.warn('获取当前 Parse 用户失败:', error); return null; } } ``` ### SDK 类型声明 为了在 TypeScript 中使用这些 SDK,需要添加全局类型声明: ```typescript // 飞书 SDK 全局类型声明 declare global { interface Window { tt?: any; // 飞书客户端 JSSDK QRLogin?: any; // 飞书二维码登录 SDK } } ``` ### SDK 加载时机 #### 自动加载 `FeishuAuth` 构造函数会自动加载飞书客户端 JSSDK: ```typescript 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 只在需要时加载: ```typescript initQRCodeLogin(containerId: string, onSuccess, onError): void { // 确保 SDK 已加载 this.loadQRCodeSDK().then(() => { // 初始化二维码 const QRLoginObj = window.QRLogin({...}); }); } ``` ### SDK 错误处理 #### SDK 加载失败 ```typescript try { await this.loadFeishuSDK(); } catch (error) { console.error('加载飞书 SDK 失败:', error); // 降级处理:使用网页登录 } ``` #### API 调用失败 ```typescript 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. 安装依赖 确保项目已安装以下依赖: ```bash 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` 中配置飞书应用信息: ```typescript 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` 中添加登录路由: ```typescript export const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'homepage', component: HomepageComponent, canActivate: [authGuard] } ]; ``` ## 详细配置 ### FeishuAuthConfig 接口 ```typescript export interface FeishuAuthConfig { appId: string; // 飞书应用 App ID(必填) redirectUri: string; // 回调地址(必填) apiOauthWeb?: string; // OAuth2 登录接口(可选) } ``` ### 默认配置 如果不指定 `apiOauthWeb`,将使用默认值: ```typescript apiOauthWeb: 'http://localhost:3000/api/feishu-v2/oauth2/login' ``` ### 后端 API 接口 后端需要提供以下接口: #### POST /api/feishu-v2/oauth2/login **请求参数:** ```json { "appId": "cli_a9253658eef99cd2", "code": "授权码", "redirect_uri": "https://your-domain.com/login" } ``` **响应格式:** ```json { "code": 1, "data": { "userInfo": { "objectId": "用户ID", "username": "用户名", "nickname": "昵称", "avatar": "头像URL", "email": "邮箱", "mobile": "手机号", "openId": "飞书 OpenID", "unionId": "飞书 UnionID" }, "sessionToken": "Parse Session Token" }, "mess": "登录成功" } ``` ## 使用指南 ### 在登录组件中使用 ```typescript 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 { // 检查是否有回调参数 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 { const result = await this.feishuAuth.handleAuthCallback(code, state); if (result.success) { await this.handleLoginSuccess(result); } } // 处理登录成功 private async handleLoginSuccess(result: AuthResult): Promise { // 保存用户信息 if (result.userInfo) { this.feishuAuth.saveUserInfo(result.userInfo); } // 刷新认证状态 await this.authService.refreshAuthState(); // 跳转到首页 await this.router.navigate(['/homepage']); } } ``` ### 环境检测 ```typescript // 检查是否在飞书客户端内 if (this.feishuAuth.isInFeishuApp()) { console.log('在飞书客户端内'); } else { console.log('在浏览器中'); } ``` ### 飞书客户端内免登录 ```typescript async handleFeishuLogin(): Promise { const result = await this.feishuAuth.getFeishuAuthCode(); if (result.success) { await this.handleLoginSuccess(result); } else { console.error('登录失败:', result.error); } } ``` ### 二维码登录 ```typescript initQRCodeLogin(): void { this.feishuAuth.initQRCodeLogin( 'qrcode-container', async (result: AuthResult) => { if (result.success) { await this.handleLoginSuccess(result); } }, (error: string) => { console.error('二维码登录失败:', error); } ); } ``` ### 账号密码登录 ```typescript async handleWebLogin(username: string, password: string): Promise { const result = await this.feishuAuth.webLogin(username, password); if (result.success) { await this.handleLoginSuccess(result); } else { console.error('登录失败:', result.error); } } ``` ## API 参考 ### FeishuAuth 类 #### 构造函数 ```typescript constructor(config: FeishuAuthConfig) ``` #### 方法 ##### isInFeishuApp(): boolean 检查是否在飞书客户端环境内。 **返回值:** - `true`:在飞书客户端内 - `false`:在浏览器中 ##### getFeishuAuthCode(): Promise 飞书客户端内免登录,获取授权码并完成登录。 **返回值:** ```typescript { 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` - 状态参数 **返回值:** ```typescript { success: boolean; userInfo?: UserInfo; error?: string; loginType: 'feishu' | 'web' | 'qrcode'; } ``` ##### webLogin(username, password): Promise 账号密码登录。 **参数:** - `username: string` - 用户名 - `password: string` - 密码 **返回值:** ```typescript { 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[] } 验证飞书应用配置。 **返回值:** ```typescript { isValid: boolean; errors: string[]; } ``` ### AuthResult 接口 ```typescript 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()`: ```typescript private async handleLoginSuccess(result: AuthResult): Promise { if (result.userInfo) { this.feishuAuth.saveUserInfo(result.userInfo); } // 刷新认证状态 await this.authService.refreshAuthState(); await this.router.navigate(['/homepage']); } ``` ### Q2: 二维码登录报错 4401? **原因:** 使用了旧版登录流程的 API 端点。 **解决方案:** 确保使用旧版 goto URL(当前实现已正确): ```typescript const goto = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${appId}&redirect_uri=${redirectUri}&response_type=code&state=success_login`; ``` ### Q3: 如何区分二维码登录和客户端登录? **解决方案:** 通过 `state` 参数判断: ```typescript // state 为 'success_login' 表示二维码登录 const loginType = state === 'success_login' ? 'qrcode' : 'feishu'; ``` ### Q4: 如何自定义后端 API 地址? **解决方案:** 在初始化时指定 `apiOauthWeb`: ```typescript 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 小时过期检查: ```typescript if (!this.feishuAuth.isLoggedIn()) { // 登录已过期,跳转到登录页 this.router.navigate(['/login']); } ``` ## 最佳实践 ### 1. 环境配置管理 使用环境变量管理不同环境的配置: ```typescript // 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. 错误处理 统一处理登录错误: ```typescript private async handleLoginError(error: string): Promise { console.error('登录失败:', error); // 显示错误提示 this.errorMessage.set(error); // 记录错误日志 this.logError('login_failed', { error }); } ``` ### 3. 加载状态管理 使用 signal 管理加载状态: ```typescript loading = signal(false); async handleFeishuLogin(): Promise { this.loading.set(true); try { const result = await this.feishuAuth.getFeishuAuthCode(); // 处理结果 } finally { this.loading.set(false); } } ``` ### 4. 路由守卫配置 确保所有需要登录的路由都配置了守卫: ```typescript export const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: '', canActivate: [authGuard], children: [ { path: 'homepage', component: HomepageComponent }, { path: 'profile', component: ProfileComponent } ] } ]; ``` ### 5. 会话恢复 在应用启动时自动恢复会话: ```typescript // app.component.ts async ngOnInit(): Promise { // AuthService 会自动从 Parse 恢复会话 await this.authService.refreshAuthState(); } ``` ### 6. 安全最佳实践 - 不要在前端代码中硬编码 App Secret - 使用 HTTPS 传输敏感信息 - 定期更新 sessionToken - 实现登录失败次数限制 - 记录登录日志用于安全审计 ### 7. 用户体验优化 - 根据环境自动选择登录方式 - 提供清晰的错误提示 - 添加加载动画 - 支持记住登录状态 - 提供退出登录功能 ## 技术栈 - Angular 17+ - TypeScript 5.0+ - Parse SDK - 飞书 JSSDK - 飞书二维码登录 SDK ## 许可证 MIT ## 支持 如有问题或建议,请联系开发团队。