# 钉钉登录配置完整文档 本文档详细说明了项目中钉钉登录功能的完整配置,可用于在其他项目中复制该功能。 --- ## 一、概述 本项目实现了完整的钉钉扫码登录功能,支持: - ✅ 钉钉扫码登录(Web端) - ✅ 钉钉内免密登录 - ✅ 多组织切换 - ✅ 自动创建用户 - ✅ 用户信息同步 --- ## 二、钉钉应用配置 ### 2.1 钉钉开放平台配置 需要在钉钉开放平台创建应用,并获取以下信息: #### 应用类型 - **H5微应用** 或 **第三方企业应用** #### 必需的配置参数 ```javascript { clientId: 'dingxxxxxxxxxx', // 应用的 AppKey/ClientId clientSecret: 'xxxxxxxxxxxxxx', // 应用的 AppSecret/ClientSecret corpId: 'dingxxxxxxxxxx' // 企业 CorpId(仅内部免密登录需要) } ``` #### 权限配置 需要申请以下权限: - `Contact.User.Read` - 通讯录只读权限 - `openid` - 获取用户OpenID #### 回调域名配置 在钉钉开放平台配置以下回调地址: ``` http://your-domain.com/dingtalk-callback.html http://your-domain.com/dingtalk.html ``` ### 2.2 项目中的配置 #### 后端配置文件:`server/routes/dingtalk.js` ```javascript // 钉钉应用配置 const DINGTALK_CONFIG = { test: { clientId: 'dingmnpddoxnelhouyiq', clientSecret: '9efbE4X-Nmk5rl21FZ3iGa6gQtQWXk4xMquy6Lmt2adN3OJQMBrCN_40DZg-6xPK', }, jianshang: { clientId: 'ding1bs1gxwrcfu16x0f', clientSecret: 'qUmj_gBDzpPEYR3RA3hRqKzwu11a986O-wKfP8V5lDHXn7XJrXxW79KIeP5G-mcY', } }; ``` > ⚠️ **安全提示**:生产环境应将这些配置移至环境变量 `.env` 文件中 --- ## 三、数据库配置 ### 3.1 用户模型扩展 在 `server/models/User.js` 中添加钉钉登录相关字段: ```javascript const userSchema = new mongoose.Schema({ // ... 其他字段 // 钉钉登录相关字段 dingtalk_userid: { type: String, sparse: true, // 允许多个null值,但不允许重复的非null值 unique: true }, loginType: { type: String, enum: ['normal', 'dingtalk'], default: 'normal' }, phone: { type: String, sparse: true, unique: true }, realName: { type: String, trim: true }, isEmailVerified: { type: Boolean, default: false } }); ``` --- ## 四、后端路由配置 ### 4.1 主服务器配置 在 `server/server.js` 中引入钉钉路由: ```javascript // 导入钉钉登录路由 const dingtalkRoutes = require('./routes/dingtalk'); // 注册路由 app.use('/api/dingtalk', dingtalkRoutes); ``` ### 4.2 钉钉登录路由 创建文件 `server/routes/dingtalk.js`,包含以下接口: #### 接口1:OAuth2验证接口 ```javascript GET /api/dingtalk/oauth2 参数: - code: 授权码 - company: 公司标识(默认mistong) ``` #### 接口2:钉钉登录接口 ```javascript POST /api/dingtalk/login 请求体: { "code": "授权码", "state": "公司标识" } 响应: { "success": true, "message": "登录成功", "user": {...}, "token": "JWT令牌" } ``` #### 接口3:钉钉免密登录接口(用于钉钉内部) ```javascript POST /api/dingtalk/autologin 请求体: { "code": "免登授权码", "state": "公司标识" } ``` ### 4.3 核心函数实现 ```javascript // 获取用户访问令牌 async function getUserAccessToken(code, company) { const config = DINGTALK_CONFIG[company]; const url = 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken'; const params = { clientId: config.clientId, clientSecret: config.clientSecret, code: code, grantType: 'authorization_code' }; const response = await axios.post(url, params, { headers: { 'Content-Type': 'application/json' } }); return response.data; } // 获取应用访问令牌 async function getAppAccessToken(company) { const config = DINGTALK_CONFIG[company]; const url = 'https://oapi.dingtalk.com/gettoken'; const response = await axios.get(url, { params: { appkey: config.clientId, appsecret: config.clientSecret } }); return response.data.access_token; } // 根据token获取用户unionId async function getUnionId(token) { const url = 'https://api.dingtalk.com/v1.0/contact/users/me'; const response = await axios.get(url, { headers: { 'x-acs-dingtalk-access-token': token } }); return response.data.unionId; } // 根据unionId获取userId async function getUserIdByUnionId(unionId, appToken) { const url = `https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=${appToken}`; const response = await axios.post(url, { unionid: unionId }); return response.data.result.userid; } // 根据userId获取用户信息 async function getUserInfo(userId, appToken) { const url = `https://oapi.dingtalk.com/topapi/v2/user/get?access_token=${appToken}`; const response = await axios.post(url, { userid: userId }); return response.data.result; } ``` --- ## 五、前端配置 ### 5.1 登录页面配置 #### 文件:`public/login.html` **1. 引入钉钉JS-SDK** ```html ``` **2. 钉钉组织配置** ```javascript // 钉钉组织配置 const dingTalkOrgList = [ { id: 'jianshang', name: '简尚', code: 'jianshang', app_key: 'ding1bs1gxwrcfu16x0f', logo: null, desc: '' } ]; ``` **3. 钉钉登录按钮UI** ```html
钉钉登录
``` **4. 钉钉登录核心JavaScript** ```javascript function openDTLogin(org) { const param = { redirect_uri: encodeURIComponent(location.origin + '/dingtalk-callback.html'), client_id: org.app_key, scope: 'openid', response_type: 'code', state: org.code, prompt: 'consent' }; const redirectUrl = 'https://login.dingtalk.com/oauth2/auth?' + Object.keys(param).map(key => key + '=' + param[key]).join('&'); // 在iframe中打开钉钉登录页面 showDingtalkIframe(redirectUrl); } // 显示钉钉登录iframe function showDingtalkIframe(url) { const overlay = document.getElementById('dingtalkIframeOverlay'); const iframe = document.getElementById('dingtalkIframe'); iframe.src = url; overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; } // 监听iframe消息 window.addEventListener('message', function (event) { if (event.data && event.data.type === 'dingtalk_login_success') { closeDingtalkLogin(); // 保存用户信息 localStorage.setItem('isLoggedIn', 'true'); localStorage.setItem('username', event.data.userInfo.username); localStorage.setItem('userRole', event.data.userInfo.role); // 跳转到首页 window.location.href = '/'; } }); ``` ### 5.2 钉钉回调页面 #### 文件:`public/dingtalk-callback.html` 这是钉钉OAuth2回调页面,处理授权码并完成登录: ```javascript document.addEventListener('DOMContentLoaded', function() { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); const state = urlParams.get('state') || 'mistong'; if (!code) { showError('授权失败:缺少授权码'); return; } // 调用登录接口 login(code, state); }); async function login(code, company) { try { const response = await fetch('/api/dingtalk/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, state: company }), credentials: 'include' }); const data = await response.json(); if (response.ok && data.success) { // 保存登录状态 localStorage.setItem('isLoggedIn', 'true'); localStorage.setItem('username', data.user.username); localStorage.setItem('token', data.token); // 通知父窗口登录成功 if (window.parent !== window) { window.parent.postMessage({ type: 'dingtalk_login_success', userInfo: data.user, token: data.token }, '*'); } else { window.location.href = '/'; } } else { throw new Error(data.message || '登录失败'); } } catch (error) { showError(error.message); } } ``` ### 5.3 钉钉内免密登录页面 #### 文件:`public/dingtalk.html` 用于在钉钉客户端内部实现免密登录: ```html ``` --- ## 六、环境变量配置 在 `.env` 文件中添加以下配置: ```env # JWT密钥(必需) JWT_SECRET=your_jwt_secret_key_here # 钉钉应用配置(可选,也可以在代码中配置) DINGTALK_TEST_CLIENT_ID=dingmnpddoxnelhouyiq DINGTALK_TEST_CLIENT_SECRET=your_secret_here DINGTALK_JIANSHANG_CLIENT_ID=ding1bs1gxwrcfu16x0f DINGTALK_JIANSHANG_CLIENT_SECRET=your_secret_here DINGTALK_JIANSHANG_CORP_ID=dingd590aa10d5b1efad35c2f4657eb6378f # 会话密钥 SESSION_SECRET=your_session_secret_key_here # Node环境 NODE_ENV=production ``` --- ## 七、依赖包 需要安装以下npm包: ```json { "dependencies": { "express": "^4.21.2", "axios": "^1.8.4", "jsonwebtoken": "^9.0.2", "bcrypt": "^6.0.0", "mongoose": "^6.13.8", "cookie-parser": "^1.4.7", "express-session": "^1.18.1", "dotenv": "^16.4.7" } } ``` 安装命令: ```bash npm install express axios jsonwebtoken bcrypt mongoose cookie-parser express-session dotenv ``` --- ## 八、完整登录流程 ### 8.1 Web扫码登录流程 ```mermaid sequenceDiagram participant User as 用户 participant Web as Web页面 participant DT as 钉钉服务器 participant Backend as 后端服务器 participant DB as 数据库 User->>Web: 点击钉钉登录按钮 Web->>DT: 跳转到钉钉授权页面 User->>DT: 扫码授权 DT->>Web: 返回授权码(code) Web->>Backend: POST /api/dingtalk/login {code, state} Backend->>DT: 用授权码换取用户token DT->>Backend: 返回access_token Backend->>DT: 用token获取unionId DT->>Backend: 返回unionId Backend->>DT: 用unionId获取userId DT->>Backend: 返回userId Backend->>DT: 用userId获取用户详细信息 DT->>Backend: 返回用户信息 Backend->>DB: 查找或创建用户 DB->>Backend: 返回用户数据 Backend->>Web: 返回JWT token和用户信息 Web->>User: 登录成功,跳转首页 ``` ### 8.2 钉钉内免密登录流程 ```mermaid sequenceDiagram participant User as 用户 participant DTClient as 钉钉客户端 participant Web as Web页面 participant Backend as 后端服务器 participant DB as 数据库 User->>DTClient: 在钉钉中打开应用 DTClient->>Web: 加载页面 Web->>DTClient: 调用dd.runtime.permission.requestAuthCode DTClient->>Web: 返回免登授权码 Web->>Backend: POST /api/dingtalk/autologin {code, state} Backend->>DTClient: 用免登码获取userId Backend->>DTClient: 获取用户详细信息 Backend->>DB: 查找或创建用户 Backend->>Web: 返回JWT token Web->>User: 自动登录成功 ``` --- ## 九、关键代码说明 ### 9.1 用户创建逻辑 当钉钉用户首次登录时,系统会自动创建账户: ```javascript // 首先通过钉钉userid查找用户 let user = await User.findOne({ dingtalk_userid: userId }); if (!user) { // 创建新用户 const randomPassword = generateRandomPassword(10); const hashedPassword = await bcrypt.hash(randomPassword, 10); user = new User({ username: userInfo.job_number || userInfo.name, email: userInfo.email || generateRandomEmail(), password: hashedPassword, realName: userInfo.name, avatar: userInfo.avatar, phone: userInfo.mobile || null, role: 'user', status: 'active', isEmailVerified: true, // 钉钉用户默认已验证 loginType: 'dingtalk', dingtalk_userid: userId }); await user.save(); } ``` ### 9.2 JWT Token生成 ```javascript // 生成JWT令牌 const token = jwt.sign( { userId: user._id, username: user.username, role: user.role }, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '7d' } ); // 设置cookie res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 7 * 24 * 60 * 60 * 1000 // 7天 }); ``` --- ## 十、安全建议 ### 10.1 配置安全 - ✅ 将所有敏感配置(clientSecret等)移至环境变量 - ✅ 不要将 `.env` 文件提交到版本控制 - ✅ 使用强随机的JWT_SECRET ### 10.2 传输安全 - ✅ 生产环境使用HTTPS - ✅ 启用Cookie的secure标志 - ✅ 使用httpOnly防止XSS攻击 ### 10.3 数据安全 - ✅ 密码使用bcrypt加密 - ✅ Token设置合理的过期时间 - ✅ 定期清理过期的token --- ## 十一、常见问题 ### Q1: 回调页面显示"授权失败:缺少授权码" **原因**:钉钉回调地址配置错误或未在钉钉开放平台配置 **解决**:检查钉钉开放平台的回调地址配置 ### Q2: 登录后提示"获取用户信息失败" **原因**:应用权限不足 **解决**:在钉钉开放平台检查是否已申请通讯录读取权限 ### Q3: 钉钉内免密登录失败 **原因**:corpId配置错误 **解决**:确认corpId是正确的企业ID ### Q4: Token验证失败 **原因**:JWT_SECRET配置不一致 **解决**:确保所有服务使用相同的JWT_SECRET --- ## 十二、测试清单 部署前请完成以下测试: - [ ] Web端钉钉扫码登录 - [ ] 钉钉客户端内免密登录 - [ ] 新用户自动创建 - [ ] 已有用户登录 - [ ] Token有效性验证 - [ ] 登出功能 - [ ] 用户信息同步 - [ ] 多组织切换(如有) --- ## 十三、部署步骤 ### 13.1 后端部署 1. 复制 `server/routes/dingtalk.js` 到新项目 2. 更新 `server/models/User.js` 添加钉钉相关字段 3. 在 `server/server.js` 中注册钉钉路由 4. 配置环境变量 ### 13.2 前端部署 1. 复制 `public/login.html` 中的钉钉登录部分 2. 复制 `public/dingtalk-callback.html` 3. 复制 `public/dingtalk.html` 4. 更新钉钉组织配置 ### 13.3 钉钉配置 1. 在钉钉开放平台创建应用 2. 配置回调地址 3. 申请必要权限 4. 获取clientId和clientSecret ### 13.4 测试验证 1. 启动服务器 2. 访问登录页面 3. 点击钉钉登录按钮 4. 扫码完成登录 5. 验证用户信息正确保存 --- ## 十四、技术支持 ### 参考文档 - [钉钉开放平台](https://open.dingtalk.com/) - [钉钉OAuth2.0](https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information) - [钉钉JS-API](https://open.dingtalk.com/document/orgapp/h5-development-overview) ### API文档 - 用户访问令牌:`https://api.dingtalk.com/v1.0/oauth2/userAccessToken` - 应用访问令牌:`https://oapi.dingtalk.com/gettoken` - 获取用户信息:`https://api.dingtalk.com/v1.0/contact/users/me` - 根据unionId获取userId:`https://oapi.dingtalk.com/topapi/user/getbyunionid` - 获取用户详情:`https://oapi.dingtalk.com/topapi/v2/user/get` --- ## 十五、版本信息 - **文档版本**:v1.0 - **更新日期**:2025-11-05 - **适用项目**:yedir (视频魔方AI) - **Node.js版本**:≥14.0.0 - **Express版本**:^4.21.2 --- **注意**:本文档包含敏感配置信息示例,实际使用时请替换为自己的配置,并妥善保管。