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

15 KiB
Raw Blame History

title, date
title date
企业微信工作台开发 2026-01-23

一、建立工作台

  1. 进入企业微信后台管理系统,选择应用管理->应用管理。
  2. 选择自建->创建应用。
  3. 输入名称、图标等信息,分配可见范围,完成应用创建.
  4. 点击应用,选择开发者接口->网页授权配置可信域名和企业可信IP。
  5. 查看并保存AgentId和Secret。

::: tip 可信域名不能是IP形式必须是和企业微信备案主体一致的服务器。 :::

二、JS-SDK开发

2.1 接口鉴权

官方文档
  参考第三节企业微信鉴权接口开发

2.2 打开默认浏览器

  使用系统浏览器打开指定 URL支持传入 oauth2 链接,从而实现在系统浏览器内免登录的效果。

三、鉴权接口开发

本教程以node.js为例。

3.1 通用企业微信服务端API

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 自建应用鉴权接口

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

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 工具类

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
};

四、应用主页

<!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=''的形式访问。
::: ::: tip 如果需要在发送应用消息时可以点击消息访问应用绑定的URL链接也是该应用主页的链接。
:::

五、使用

本教程以SpringBoot为例。

5.1 配置RestClient

@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 调用发送消息接口

@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("企业微信发送消息失败 请联系管理员");
        }
    }
}

附录

企业微信官方文档:接口