feat:增加企业微信接口博客
This commit is contained in:
451
docs/Others/WeCom.md
Normal file
451
docs/Others/WeCom.md
Normal file
@@ -0,0 +1,451 @@
|
||||
---
|
||||
title: 企业微信工作台开发
|
||||
date: 2026-01-23
|
||||
---
|
||||
|
||||
  接口文档地址:[接口](https://developer.work.weixin.qq.com/document/path/90664)
|
||||
|
||||
# 一、建立工作台
|
||||
1. 进入企业微信后台管理系统,选择应用管理->应用管理。
|
||||
2. 选择自建->创建应用。
|
||||
3. 输入名称、图标等信息,分配可见范围,完成应用创建.
|
||||
4. 点击应用,选择开发者接口->网页授权,配置可信域名和企业可信IP。
|
||||
5. 查看并保存AgentId和Secret。
|
||||
|
||||
::: tip
|
||||
可信域名不能是IP形式,必须是和企业微信备案主体一致的服务器。
|
||||
:::
|
||||
|
||||
# 二、JS-SDK开发
|
||||
## 2.1 接口鉴权
|
||||
  [官方文档](https://developer.work.weixin.qq.com/document/path/90514)
|
||||
  参考第三节企业微信接口开发
|
||||
|
||||
## 2.2 打开默认浏览器
|
||||
  使用系统浏览器打开指定 URL,支持传入 oauth2 链接,从而实现在系统浏览器内免登录的效果。
|
||||
|
||||
# 三、接口开发
|
||||
  本教程以node.js为例。
|
||||
## 3.1 通用企业微信服务端API
|
||||
```js
|
||||
const axios = require('axios');
|
||||
|
||||
const TOKEN_URL = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
|
||||
const JSAPI_TICKET_URL = 'https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket'
|
||||
const MESSAGE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
|
||||
|
||||
// 获取access_token
|
||||
async function getAccessTokenApi(corpId, corpSecret) {
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
const url = `${TOKEN_URL}?corpid=${corpId}&corpsecret=${corpSecret}`
|
||||
const response = await axios.get(url);
|
||||
|
||||
if (response.data.errcode === 0) {
|
||||
return {
|
||||
accessToken: response.data.access_token,
|
||||
expires: now + (response.data.expires_in - 300) * 1000 // 提前5分钟过期
|
||||
};
|
||||
} else {
|
||||
throw new Error(`获取access_token失败: ${response.data.errmsg}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取access_token错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取jsapi_ticket
|
||||
async function getJsapiTicketApi(accessToken) {
|
||||
try {
|
||||
const url = `${JSAPI_TICKET_URL}?access_token=${accessToken}`
|
||||
const response = await axios.get(url);
|
||||
|
||||
if (response.data.errcode === 0) {
|
||||
return response.data.ticket;
|
||||
} else {
|
||||
throw new Error(`获取jsapi_ticket失败: ${response.data.errmsg}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取jsapi_ticket错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送应用消息
|
||||
async function sendMessageApi(agentid, accessToken, touser, content) {
|
||||
try {
|
||||
const url = `${MESSAGE_URL}?access_token=${accessToken}`
|
||||
const response = await axios.post(url, {
|
||||
touser: touser,
|
||||
msgtype: 'text',
|
||||
agentid: agentid,
|
||||
text: {
|
||||
content: content
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.errcode === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(`发送应用消息失败: ${response.data.errmsg}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送应用消息错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAccessTokenApi,
|
||||
getJsapiTicketApi,
|
||||
sendMessageApi
|
||||
};
|
||||
```
|
||||
|
||||
## 3.2 自建应用鉴权接口
|
||||
```js
|
||||
const {getAccessTokenApi, getJsapiTicketApi, sendMessageApi} = require("./wecom");
|
||||
|
||||
const config = {
|
||||
corpId: '', // 企业id
|
||||
corpSecret: '', // 应用密码
|
||||
agentId: '', // 应用id
|
||||
domain: '', // 跳转域名
|
||||
password: '' // 登录默认密码
|
||||
};
|
||||
|
||||
// 存储access_token和jsapi_ticket
|
||||
let tokenCache = {
|
||||
accessToken: null,
|
||||
jsapiTicket: null,
|
||||
expires: 0
|
||||
};
|
||||
|
||||
// 获取access_token
|
||||
async function getRepairAccessToken() {
|
||||
const now = Date.now();
|
||||
|
||||
if (tokenCache.accessToken && tokenCache.expires > now) {
|
||||
return tokenCache.accessToken;
|
||||
}
|
||||
|
||||
const result = await getAccessTokenApi(config.corpId, config.corpSecret)
|
||||
tokenCache.accessToken = result.accessToken;
|
||||
tokenCache.expires = result.expires
|
||||
}
|
||||
|
||||
// 获取jsapi_ticket
|
||||
async function getRepairJsapiTicket() {
|
||||
const now = Date.now();
|
||||
|
||||
if (tokenCache.jsapiTicket && tokenCache.expires > now) {
|
||||
return tokenCache.jsapiTicket;
|
||||
}
|
||||
|
||||
await getRepairAccessToken();
|
||||
tokenCache.jsapiTicket = await getJsapiTicketApi(tokenCache.accessToken);
|
||||
}
|
||||
|
||||
// 发送应用消息
|
||||
async function sendRepairMessage(touser, content) {
|
||||
await getRepairAccessToken();
|
||||
await sendMessageApi(config.agentId, tokenCache.accessToken, touser, content);
|
||||
}
|
||||
|
||||
function getRepairDomain() {
|
||||
return config.domain
|
||||
}
|
||||
|
||||
function getRepairPassword() {
|
||||
return config.password
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRepairAccessToken,
|
||||
getRepairJsapiTicket,
|
||||
sendRepairMessage,
|
||||
getRepairDomain,
|
||||
getRepairPassword
|
||||
};
|
||||
```
|
||||
|
||||
## 3.3 NodeJS API
|
||||
```js
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const cors = require('cors');
|
||||
const {generateNonceStr, generateSignature} = require("./utils");
|
||||
const {sendRepairMessage, getRepairJsapiTicket, getRepairAccessToken, getRepairDomain, getRepairPassword} = require("./repair");
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const corpId = 'ww48932728388c9e3a'
|
||||
const USER_INFO_URL = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'
|
||||
|
||||
// API接口:生成JS-SDK配置
|
||||
app.post('/auth/config/repair', async (req, res) => {
|
||||
await configService(req, res, 'repair')
|
||||
});
|
||||
|
||||
async function configService(req, res, appName) {
|
||||
try {
|
||||
const {url} = req.body;
|
||||
|
||||
if (!url) {
|
||||
return res.status(400).json({errcode: -1, errmsg: '缺少url参数'});
|
||||
}
|
||||
|
||||
let jsapiTicket = ''
|
||||
|
||||
if (appName === 'repair') {
|
||||
jsapiTicket = await getRepairJsapiTicket();
|
||||
}
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const noncestr = generateNonceStr();
|
||||
const signature = generateSignature(jsapiTicket, noncestr, timestamp, url);
|
||||
|
||||
res.json({
|
||||
errcode: 0,
|
||||
errmsg: 'ok',
|
||||
appId: corpId,
|
||||
timestamp,
|
||||
nonceStr: noncestr,
|
||||
signature
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('生成JS-SDK配置错误:', error);
|
||||
res.status(500).json({errcode: -1, errmsg: '服务器内部错误'});
|
||||
}
|
||||
}
|
||||
|
||||
// API接口:OAuth回调处理
|
||||
app.get('/auth/callback/repair', async (req, res) => {
|
||||
await callback(req, res, 'repair')
|
||||
});
|
||||
|
||||
async function callback(req, res, appName) {
|
||||
try {
|
||||
const {code, state} = req.query;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).send('缺少code参数');
|
||||
}
|
||||
|
||||
// 通过code获取用户信息
|
||||
let accessToken = ''
|
||||
if (appName === 'repair') {
|
||||
accessToken = await getRepairAccessToken();
|
||||
}
|
||||
|
||||
const url = `${USER_INFO_URL}?access_token=${accessToken}&code=${code}`
|
||||
const userResponse = await axios.get(url);
|
||||
|
||||
if (userResponse.data.errcode !== 0) {
|
||||
throw new Error(`获取用户信息失败: ${userResponse.data.errmsg}`);
|
||||
}
|
||||
|
||||
const userInfo = userResponse.data;
|
||||
|
||||
// 重定向到前端页面
|
||||
let redirectUrl = ''
|
||||
if (appName === 'repair') {
|
||||
redirectUrl = `${getRepairDomain()}?username=${userInfo.UserId}&password=${getRepairPassword()}&type=repair`;
|
||||
}
|
||||
|
||||
res.redirect(redirectUrl);
|
||||
|
||||
} catch (error) {
|
||||
console.error('OAuth回调处理错误:', error);
|
||||
res.status(500).send(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>登录失败</h1>
|
||||
<p>${error.message}</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// API接口:发现应用消息
|
||||
app.post('/message/repair', async (req, res) => {
|
||||
const {touser, content} = req.body;
|
||||
await sendRepairMessage(touser, content)
|
||||
res.json({
|
||||
errcode: 0,
|
||||
errmsg: 'ok'
|
||||
});
|
||||
});
|
||||
|
||||
// 启动服务
|
||||
app.listen(PORT, () => {
|
||||
console.log(`企业微信登录服务运行在端口 ${PORT}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
```
|
||||
|
||||
## 3.4 工具类
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
|
||||
// 工具函数:生成随机字符串
|
||||
function generateNonceStr(length = 16) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 工具函数:生成签名
|
||||
function generateSignature(jsapiTicket, noncestr, timestamp, url) {
|
||||
const string = `jsapi_ticket=${jsapiTicket}&noncestr=${noncestr}×tamp=${timestamp}&url=${url}`;
|
||||
return crypto.createHash('sha1').update(string).digest('hex');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateNonceStr,
|
||||
generateSignature
|
||||
};
|
||||
```
|
||||
|
||||
# 四、应用主页
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>企业微信登录跳转</title>
|
||||
<!-- 引入新版企业微信JS-SDK -->
|
||||
<script src="https://wwcdn.weixin.qq.com/node/open/js/wecom-jssdk-2.3.3.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">正在跳转,请稍候...</div>
|
||||
|
||||
<script>
|
||||
// 配置参数
|
||||
const serverUrl = ''
|
||||
|
||||
const config = {
|
||||
corpId: '', // 企业ID
|
||||
agentId: '', // 应用ID
|
||||
redirectUrl: encodeURIComponent(`${serverUrl}auth/callback/repair`), // OAuth回调地址
|
||||
authUrl: `${serverUrl}auth/config/repair` // 后端授权入口
|
||||
};
|
||||
|
||||
// 主函数
|
||||
async function init() {
|
||||
try {
|
||||
// 获取当前页面URL(用于签名)
|
||||
const currentUrl = window.location.href.split('#')[0];
|
||||
|
||||
// 从后端获取签名信息
|
||||
const signatureData = await getSignatureData(currentUrl);
|
||||
|
||||
const getConfigSignature = function(url) {
|
||||
return {
|
||||
timestamp: signatureData.timestamp,
|
||||
nonceStr: signatureData.nonceStr,
|
||||
signature: signatureData.signature
|
||||
};
|
||||
};
|
||||
|
||||
// 使用ww.register 进行身份注册
|
||||
await ww.register({
|
||||
corpId: config.corpId,
|
||||
agentId: config.agentId,
|
||||
jsApiList: ['openDefaultBrowser', 'closeWindow'],
|
||||
getConfigSignature: getConfigSignature
|
||||
});
|
||||
|
||||
// 生成授权URL
|
||||
const authUrl = await generateAuthUrl();
|
||||
|
||||
const isMobile = /(phone|pad|pod|iphone|ipod|ios|ipad|android|mobile|blackberry|iemobile|mqqbrowser|fennec|wosbrowser|browserng|webos|symbian|windows phone)/i.test(navigator.userAgent);
|
||||
|
||||
if (isMobile) {
|
||||
// 在移动端企业微信中直接打开链接
|
||||
window.location.href = authUrl;
|
||||
} else {
|
||||
// 使用新版API调用系统浏览器
|
||||
ww.openDefaultBrowser({
|
||||
url: authUrl,
|
||||
success: (res) => {
|
||||
// console.log('成功调用系统浏览器:', res);
|
||||
ww.closeWindow({})
|
||||
},
|
||||
fail: (err) => {
|
||||
// console.error('打开浏览器失败:', err);
|
||||
document.getElementById('loading').innerHTML = `打开失败: ${err.errMsg || '未知错误'},请点击右上角菜单选择"在浏览器中打开"`;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error);
|
||||
document.getElementById('loading').innerHTML = '跳转失败,请手动在浏览器中打开: ' + error.errMsg;
|
||||
// window.alert(error.errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取签名数据
|
||||
async function getSignatureData(url) {
|
||||
const response = await fetch(`${config.authUrl}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取签名配置失败');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 确保返回的数据包含必要的字段
|
||||
if (!result.timestamp || !result.nonceStr || !result.signature) {
|
||||
throw new Error('签名数据不完整');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 生成授权URL
|
||||
async function generateAuthUrl() {
|
||||
// 使用URLSearchParams构建参数
|
||||
const authParams = new URLSearchParams({
|
||||
appid: config.corpId,
|
||||
redirect_uri: config.redirectUrl,
|
||||
response_type: 'code',
|
||||
scope: 'snsapi_base', // 静默授权
|
||||
state: 'wecom_redirect',
|
||||
agentid: config.agentId
|
||||
});
|
||||
|
||||
return `https://open.weixin.qq.com/connect/oauth2/authorize?${authParams.toString()}#wechat_redirect`;
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
  将该应用主页的URL放在工作台的应用主页中。
|
||||
::: tip
|
||||
需要在前端工程中配置路由参数访问,即通过ip:port/#/login?username=''&password=''的形式访问。
|
||||
:::
|
||||
Reference in New Issue
Block a user