使用 Spring Boot 快速构建企业微信 JS-SDK 权限签名后端服务

使用 Spring Boot 快速构建企业微信 JS-SDK 权限签名后端服务

本篇文章将介绍如何使用 Spring Boot 快速构建一个用于支持企业微信 JS-SDK 权限校验的后端接口,并提供一个简单的 HTML 页面进行功能测试。适用于需要在企业微信网页端使用扫一扫、定位、录音等接口的场景。


一、项目目标

我们希望实现一个包含以下功能的服务:

  • 提供获取企业微信 access_token 的接口
  • 提供获取部门成员信息的接口(需要带 token)
  • 提供 JS-SDK 前端初始化所需签名参数的接口(wx.config() 配置)
  • 提供一个前端页面用于测试扫码、定位、数据表格展示等功能

二、开发环境与依赖

  • JDK 17
  • IDEA
  • Spring Boot 3.2.5
  • Maven 3.x

三、项目结构

text 复制代码
DemoAPI
├── pom.xml                       		  // 项目依赖配置
├── src
│   └── main
│       ├── java
│       │   └── org.example
│       │       ├── Main.java             // 项目启动类
│       │       ├── WeComController.java  // 控制器:处理请求
│       │       └── WeComService.java     // 服务类:处理逻辑
│       └── resources
│           └── static
│               └── index.html            // 测试前端页面

说明: 本项目未配置 application.yml,Spring Boot 默认即可运行。


四、完整功能实现

第一步:修改 pom.xml,添加 Spring Boot 配置

pom.xml 中我们引入了:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

可能遇到的问题:

  • 依赖下载失败,可通过加速器优化下载速度。
  • 注意 Spring Boot 3.x 要使用 JDK 17+。

第二步:刷新依赖

你可以点击 IntelliJ 右侧 "Maven" 工具窗口的刷新按钮(🔄),或者右键 pom.xml → Add as Maven Project,IDE 会自动下载 Spring Boot 依赖。

第三步:修改你的 Main.java,变成 Spring Boot 启动类

java 复制代码
package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

问题回顾: 如果你忘记添加 @SpringBootApplication,将导致 ApplicationContext 启动失败,同时控制台可能提示找不到 Web 容器类(如 Tomcat)或无法创建 Controller Bean。解决办法:确保注解已加。

第四步:创建一个服务类 WeComService.java

提供 access_token 缓存获取、jsapi_ticket 缓存、JS-SDK 签名生成逻辑:

java 复制代码
String raw = String.format(
    "jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s",
    jsapiTicket, nonceStr, timestamp, url);

MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(raw.getBytes(StandardCharsets.UTF_8));

注意:

  • 签名计算必须严格按参数顺序和格式
  • access_tokenjsapi_ticket 建议缓存,避免频繁请求
  • 返回格式需包括 appIdtimestampnonceStrsignature

JS-SDK 参数生成

  • 参数组成:jsapi_ticketnonceStrtimestampurl
  • 算法:SHA-1(raw字符串) 生成签名
  • 返回结构:包含 appIdtimestampnonceStrsignature

第五步:控制器类 WeComController.java

提供如下接口:

接口地址 请求方法 功能描述
/wecom/token GET 获取 access_token
/wecom/department/users GET 获取指定部门的成员列表
/wecom/js-sdk-config GET 获取 JS-SDK 初始化配置信息

常见问题:

  • 若自动注入失败,请确认 @Service@RestController 注解是否添加
  • 如果依赖注入失败,控制台会提示 UnsatisfiedDependencyException

第六步:创建前端测试页面 index.html

功能:

  • 获取 Token 并展示
  • 获取部门成员并展示表格(含滚动条)
  • 初始化 JS SDK,支持扫码、定位等测试按钮
js 复制代码
wx.config({
  appId: config.appId,
  timestamp: config.timestamp,
  nonceStr: config.nonceStr,
  signature: config.signature,
  jsApiList: ["scanQRCode", "getLocation"]
});

wx.ready(function() {
  alert("✅ 企业微信 JS SDK 初始化成功");
});

失败处理:

js 复制代码
wx.error(function (err) {
    alert("❌ SDK 初始化失败: " + JSON.stringify(err));
});

页面结构清晰,所有逻辑通过 window.onload 初始化即可。

第七步:运行你的 Spring Boot 应用

在 IntelliJ 中右键 Main.java → Run 'Main',或点击绿色的 ▶ 按钮。

看到类似:

scss 复制代码
Tomcat started on port(s): 8080
Started Main in x.xxx seconds

说明服务已成功启动。

第八步:界面展示

http://localhost:8080/index.html

运行 & 测试(可选)

启动 Spring Boot 项目后,浏览器访问可访问下面的接口:

http://localhost:8080/wecom/token
http://localhost:8080/wecom/department/users?id=1


六、常见问题总结

问题 说明 解决办法
SDK 初始化失败 签名无效、时间戳不一致等 保证 URL 不带 #,参数顺序正确
Bean 注入失败 启动报错找不到 Controller Bean 检查是否缺少 @SpringBootApplication@Service 注解
依赖无法拉取 Maven 仓库连接慢 配置阿里云镜像源,提高稳定性
HTML 无法访问 资源路径未设置正确 放到 resources/static/ 下由 Spring Boot 自动映射

❌ 错误核心提示:

scss 复制代码
APPLICATION FAILED TO START

Web application could not be started as there was no
org.springframework.boot.web.servlet.server.ServletWebServerFactory bean defined in the context.

原因解释:Spring Boot 应用是一个 Web 项目,但 缺少内嵌 Servlet 容器(比如 Tomcat)依赖,也就是没有 ServletWebServerFactory,Spring Boot 启动 Web 服务失败。

最常见的原因:

  • pom.xml 中 缺失或拼错了 spring-boot-starter-web 依赖
  • Maven 没有下载成功依赖(网络或仓库问题)
  • 没有添加 @SpringBootApplication

七、后续可扩展方向

  • 接入企业微信身份认证(OAuth2)
  • 支持更多 JS API(如录音、语音识别、打开地图)
  • 使用 Redis 缓存 token,提升性能与健壮性
  • 前后端分离,使用 Vue、React 等框架

八、结语

通过本项目我们实现了从零搭建一个企业微信 JS-SDK 权限校验服务,具备了完整的后端支持和前端测试页面。如果想正常使用企业微信的扫描等功能需要在企业微信内部访问,那么就需要设置 IP 白名单、域名、网页授权及JS-SDK、企业微信授权登录和应用主页等。


九、推荐

Maven Central(Maven 中央仓库 Web 版)

这是最常用、几乎所有 Java 开发者都会用的网站 ------ 一个图形化的 Maven 中央仓库检索平台:

👉 网站地址:

🌐 https://mvnrepository.com

使用 Spring Initializr 官网 创建项目(图形化窗口版)

这个网站会自动帮你生成一个可运行的 Spring Boot 项目,并打包成一个 zip 文件。解压 zip,然后用 IDEA 打开即可。

👉 地址:

🌐 https://start.spring.io


附录:完整文件(可自行补全代码)

pom.xml ✅

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>DemoAPI</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- Spring Boot 父项目 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Spring Boot Web 模块(包含内嵌 Tomcat) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 开发工具(自动重启,非必须) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

index.html ✅

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>企业微信接口测试</title>
    <style>
        body {
            font-family: "微软雅黑", sans-serif;
            margin: 20px;
        }
        table {
            border-collapse: collapse;
            width: 100%;
            margin-top: 10px;
        }
        th, td {
            border: 1px solid #ccc;
            padding: 6px 12px;
            text-align: center;
        }
        th {
            background-color: #f5f5f5;
        }
        pre {
            background-color: #eee;
            padding: 10px;
        }
        .scroll-box {
            max-height: 160px;
            overflow-y: auto;
            border: 1px solid #ccc;
        }
    </style>

    <!-- 引入企业微信 JS SDK -->
    <script src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>

    <script>
        // 初始化企业微信 JS SDK
        async function initWeComJsSdk() {
            const url = window.location.href.split('#')[0];
            const res = await fetch('/wecom/js-sdk-config?url=' + encodeURIComponent(url));
            const config = await res.json();

            wx.config({
                beta: true,
                debug: false,
                appId: config.appId,
                timestamp: config.timestamp,
                nonceStr: config.nonceStr,
                signature: config.signature,
                jsApiList: ["scanQRCode", "getLocation"]
            });

            wx.ready(function () {
                console.log("企业微信 JS SDK 就绪");
                alert("✅ 企业微信 JS SDK 初始化成功!");

                document.getElementById('scanBtn').onclick = function () {
                    wx.scanQRCode({
                        needResult: 1,
                        scanType: ["qrCode", "barCode"],
                        success: function (res) {
                            alert("扫码结果:" + res.resultStr);
                        }
                    });
                };

                document.getElementById('locBtn').onclick = function () {
                    wx.getLocation({
                        type: 'wgs84',
                        success: function (res) {
                            alert("当前位置:经度 " + res.longitude + ",纬度 " + res.latitude);
                        }
                    });
                };
            });

            wx.error(function (err) {
                console.error("JS SDK 初始化失败:", err);
                alert("❌ 企业微信 JS SDK 初始化失败!\n" + JSON.stringify(err));
            });
        }

        async function getToken() {
            const res = await fetch('/wecom/token');
            const token = await res.text();
            document.getElementById('token').innerText = token;
        }

        async function getUsers() {
            const deptId = document.getElementById('dept').value || '1';
            const res = await fetch(`/wecom/department/users?id=${deptId}`);
            const json = await res.json();

            document.getElementById('result').innerText = JSON.stringify(json, null, 2);

            if (json.userlist) {
                renderTable(json.userlist);
            } else {
                document.getElementById('userTableBody').innerHTML = "<tr><td colspan='6'>无成员数据</td></tr>";
            }
        }

        function renderTable(users) {
            const tbody = document.getElementById("userTableBody");
            tbody.innerHTML = "";
            users.forEach(user => {
                const row = document.createElement("tr");
                row.innerHTML = `
                    <td>${user.name}</td>
                    <td>${user.userid}</td>
                    <td>${(user.department || []).join(',')}</td>
                    <td>${user.isleader === 1 ? '是' : '否'}</td>
                    <td>${translateStatus(user.status)}</td>
                    <td>${user.telephone || ''}</td>
                `;
                tbody.appendChild(row);
            });
        }

        function translateStatus(status) {
            switch (status) {
                case 1: return "正常";
                case 2: return "已禁用";
                case 4: return "未激活";
                default: return "未知";
            }
        }

        window.onload = function () {
            initWeComJsSdk();
        };
    </script>
</head>
<body>

<h1>企业微信接口测试</h1>

<!-- 获取 Token -->
<button onclick="getToken()">获取 Token</button>
<p>Token:<code id="token">(点击上面按钮)</code></p>

<!-- 获取部门成员 -->
<hr>
<label>部门 ID:</label>
<input type="text" id="dept" value="1">
<button onclick="getUsers()">获取部门成员</button>

<!-- 显示返回数据 -->
<h3>接口返回数据:</h3>
<pre id="result">(点击按钮查看 JSON)</pre>

<!-- 成员列表表格 -->
<h3>成员列表表格:</h3>
<div class="scroll-box">
    <table>
        <thead>
        <tr>
            <th>姓名</th>
            <th>用户ID</th>
            <th>部门</th>
            <th>是否领导</th>
            <th>状态</th>
            <th>座机</th>
        </tr>
        </thead>
        <tbody id="userTableBody"></tbody>
    </table>
</div>

<!-- 企业微信 JS API 按钮 -->
<h3>企业微信功能测试:</h3>
<button id="scanBtn">扫一扫</button>
<button id="locBtn">获取当前位置</button>

</body>
</html>

Main.java ✅

java 复制代码
package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * ==================================================
 * This class ${NAME} is responsible for [功能描述].
 *
 * @author Darker
 * @version 1.0
 * ==================================================
 */

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

WeComService.java ✅

java 复制代码
package org.example;

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.http.ResponseEntity;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import java.time.Instant;

/**
 * ==================================================
 * This class WeComService is responsible for [功能描述].
 *
 * @author Darker
 * @version 1.0
 * ==================================================
 */

@Service
public class WeComService {
    private static final String CORP_ID = "你的企业微信ID";
    private static final String SECRET = "你的自建应用的Secret";
    private static final String TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken";

    private String accessToken;
    private long expireTime = 0;

    // jsapi_ticket(缓存 2 小时)
    private String jsapiTicket;
    private long ticketExpire = 0;

    public String getAccessToken() {
        long now = Instant.now().getEpochSecond();
        if (accessToken != null && now < expireTime) {
            return accessToken;
        }

        // 请求新的 token
        RestTemplate restTemplate = new RestTemplate();
        UriComponentsBuilder builder = UriComponentsBuilder
                .fromHttpUrl(TOKEN_URL)
                .queryParam("corpid", CORP_ID)
                .queryParam("corpsecret", SECRET);

        ResponseEntity<WeComTokenResponse> response = restTemplate.getForEntity(
                builder.toUriString(), WeComTokenResponse.class);

        WeComTokenResponse body = response.getBody();
        if (body != null && body.getAccess_token() != null) {
            this.accessToken = body.getAccess_token();
            this.expireTime = now + body.getExpires_in() - 60; // 提前60秒过期
            return accessToken;
        }

        throw new RuntimeException("无法获取 access_token");
    }

    public Map<String, Object> getJsSdkConfig(String url) {
        String jsapiTicket = getJsApiTicket(); // 用下面方法实现
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        long timestamp = System.currentTimeMillis() / 1000;

        String raw = String.format(
                "jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s",
                jsapiTicket, nonceStr, timestamp, url
        );

        String signature;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(raw.getBytes(StandardCharsets.UTF_8));
            signature = bytesToHex(md.digest());
        } catch (Exception e) {
            throw new RuntimeException("签名失败", e);
        }

        Map<String, Object> result = new HashMap<>();
        result.put("appId", CORP_ID);
        result.put("timestamp", timestamp);
        result.put("nonceStr", nonceStr);
        result.put("signature", signature);
        return result;
    }

    private String bytesToHex(byte[] bytes) {
        Formatter formatter = new Formatter();
        for (byte b : bytes) {
            formatter.format("%02x", b);
        }
        String result = formatter.toString();
        formatter.close();
        return result;
    }

    public String getJsApiTicket() {
        long now = System.currentTimeMillis() / 1000;
        if (jsapiTicket != null && now < ticketExpire) {
            return jsapiTicket;
        }
        String token = getAccessToken();
        String url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=" + token;
        RestTemplate restTemplate = new RestTemplate();
        Map<String, Object> res = restTemplate.getForObject(url, Map.class);
        if (res != null && res.get("ticket") != null) {
            jsapiTicket = (String) res.get("ticket");
            ticketExpire = now + ((Integer) res.get("expires_in")) - 60;
            return jsapiTicket;
        }
        throw new RuntimeException("获取 jsapi_ticket 失败");
    }

    // 内部类用于接收 JSON 响应
    public static class WeComTokenResponse {
        private String access_token;
        private int expires_in;

        public String getAccess_token() {
            return access_token;
        }

        public void setAccess_token(String access_token) {
            this.access_token = access_token;
        }

        public int getExpires_in() {
            return expires_in;
        }

        public void setExpires_in(int expires_in) {
            this.expires_in = expires_in;
        }
    }
}

WeComController.java ✅

java 复制代码
package org.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;

/**
 * ==================================================
 * This class WeComController is responsible for [功能描述].
 *
 * @author Darker
 * @version 1.0
 * ==================================================
 */
 
@RestController
@RequestMapping("/wecom")
public class WeComController {

    @Autowired
    private WeComService weComService;

    // GET 接口:/wecom/token
    @GetMapping("/token")
    public String getToken() {
        return weComService.getAccessToken();
    }

    @GetMapping("/department/users")
    public Object getDepartmentUsers(@RequestParam("id") String departmentId) {
        String token = weComService.getAccessToken();

        String url = "https://qyapi.weixin.qq.com/cgi-bin/user/list";
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
                .queryParam("access_token", token)
                .queryParam("department_id", departmentId);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<Object> response = restTemplate.getForEntity(
                builder.toUriString(), Object.class
        );

        return response.getBody();
    }

    // GET 接口:/wecom/js-sdk-config?url=xxx
    @GetMapping("/js-sdk-config")
    public Object getJsSdkConfig(@RequestParam("url") String url) {
        return weComService.getJsSdkConfig(url);
    }
}
相关推荐
啃火龙果的兔子2 小时前
判断手机屏幕上的横向滑动(左滑和右滑)
javascript·react.js·智能手机
yuanmenglxb20043 小时前
react基础技术栈
前端·javascript·react.js
coding随想4 小时前
从SPDY到HTTP/2:网络协议的革新与未来
javascript
无敌小肥0074 小时前
Springboot 整合 WebSocket 实现聊天室功能
spring boot·后端·websocket
一枚码农4045 小时前
使用pnpm、vite搭建Phaserjs的开发环境
javascript·游戏·vite·phaserjs
agenIT5 小时前
vue3 getcurrentinstance 用法
javascript·vue.js·ecmascript
代码老y5 小时前
基于springboot的校园商铺管理系统的设计与实现
java·vue.js·spring boot·后端·毕业设计·课程设计·个人开发
码农捻旧6 小时前
JavaScript 性能优化按层次逐步分析
开发语言·前端·javascript·性能优化
我是哈哈hh6 小时前
【Vue3】生命周期 & hook函数 & toRef
开发语言·前端·javascript·vue.js·前端框架·生命周期·proxy模式
傻虎贼头贼脑6 小时前
day28JS+Node-JS打包工具Webpack
开发语言·前端·javascript·webpack