# 钉钉登录配置完整文档
本文档详细说明了项目中钉钉登录功能的完整配置,可用于在其他项目中复制该功能。
---
## 一、概述
本项目实现了完整的钉钉扫码登录功能,支持:
- ✅ 钉钉扫码登录(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
---
**注意**:本文档包含敏感配置信息示例,实际使用时请替换为自己的配置,并妥善保管。