ai-chatbot/钉钉配置文档
Your Name 53ddb7e585
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Initial commit - clean repository
2025-12-17 20:15:18 +08:00

701 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 钉钉登录配置完整文档
本文档详细说明了项目中钉钉登录功能的完整配置,可用于在其他项目中复制该功能。
---
## 一、概述
本项目实现了完整的钉钉扫码登录功能,支持:
- ✅ 钉钉扫码登录(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
<!-- 不需要额外引入,使用iframe方式 -->
```
**2. 钉钉组织配置**
```javascript
// 钉钉组织配置
const dingTalkOrgList = [
{
id: 'jianshang',
name: '简尚',
code: 'jianshang',
app_key: 'ding1bs1gxwrcfu16x0f',
logo: null,
desc: ''
}
];
```
**3. 钉钉登录按钮UI**
```html
<div class="dingtalk-login-section">
<div class="dingtalk-login-container" id="dingtalkContainer">
<div class="dingtalk-main-btn" id="dingtalkMainBtn">
<svg style="width: 28px; height: 28px" viewBox="0 0 1024 1024">
<path d="..." fill="#3296FA"></path>
</svg>
<span style="font-size: 18px">钉钉登录</span>
</div>
</div>
</div>
```
**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
<script src="https://g.alicdn.com/dingding/dingtalk-jsapi/3.1.0/dingtalk.open.js"></script>
<script>
const isDingTalk = () => {
const ua = navigator.userAgent;
return ua.includes('DingTalk');
};
if (isDingTalk()) {
// 在钉钉中使用免登授权码
dd.ready(function () {
dd.runtime.permission.requestAuthCode({
corpId: "dingd590aa10d5b1efad35c2f4657eb6378f", // 企业id
onSuccess: function (info) {
const code = info.code;
window.open("/dingtalk.html?code=" + code);
}
});
});
} else {
// 在外部浏览器中使用授权码登录
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state') || 'jianshang';
login(code, state);
}
async function login(code, company) {
const response = await fetch('/api/dingtalk/autologin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, state: company }),
credentials: 'include'
});
const data = await response.json();
if (data.success) {
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('token', data.token);
window.location.href = '/';
}
}
</script>
```
---
## 六、环境变量配置
在 `.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
---
**注意**:本文档包含敏感配置信息示例,实际使用时请替换为自己的配置,并妥善保管。