Files
blog-press/docs/Others/WeCom.md
2026-01-29 15:01:35 +08:00

509 lines
15 KiB
Markdown
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.

---
title: 企业微信工作台开发
date: 2026-01-23
---
# 一、建立工作台
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}&timestamp=${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>
```
&emsp;&emsp;将该应用主页的URL放在工作台的应用主页中。
::: tip
需要在前端工程中配置路由参数访问即通过ip:port/#/login?username=''&password=''的形式访问。
:::
::: tip
如果需要在发送应用消息时可以点击消息访问应用绑定的URL链接也是该应用主页的链接。
:::
# 五、使用
&emsp;&emsp;本教程以SpringBoot为例。
## 5.1 配置RestClient
```java
@Configuration
public class RestClientConfig {
final String weiComBaseUrl = "";
@Bean
public RestClient weiComClient() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(5));
factory.setReadTimeout(Duration.ofSeconds(10));
return RestClient.builder()
.requestFactory(factory)
.baseUrl(weiComBaseUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
```
## 5.2 调用发送消息接口
```java
@Service
@Slf4j
public class NoticeService {
@Resource
private RestClient weiComClient;
@Async
public void sendRepairMessage(String touser, String content) {
String link = "";
WeComNoticeRequest request = new WeComNoticeRequest();
request.setTouser(touser);
request.setContent(content + "\n点击打开工作台应用" + "<a href=\"" + link + "\">打开应用</a>");
WeComResponse response = weiComClient.post()
.uri("/message/repair")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(WeComResponse.class);
if (response == null || response.getErrcode() != 0) {
throw new CustomException("企业微信发送消息失败 请联系管理员");
}
}
}
```
# 附录
企业微信官方文档:[接口](https://developer.work.weixin.qq.com/document/path/90664)