Spring Boot を使用した企業 WeChat JS-SDK 権限署名バックエンドサービスの迅速な構築
本記事では、Spring Boot を使用して企業 WeChat JS-SDK 権限検証のためのバックエンドインターフェースを迅速に構築する方法を紹介します。また、機能テスト用のシンプルな HTML ページも提供します。企業 WeChat のウェブページでスキャン、位置情報取得、録音などのインターフェースを使用する必要があるシナリオに適用されます。
プロジェクトの目標
以下の機能を含むサービスを実装します:
- 企業 WeChat
access_tokenを取得するインターフェースを提供 - トークンを含む部門メンバー情報を取得するインターフェースを提供
- JS-SDK フロントエンド初期化に必要な署名パラメータのインターフェースを提供(
wx.config()構成) - スキャン、位置情報取得、データテーブル表示などの機能をテストするためのフロントエンドページを提供
開発環境と依存関係
- JDK 17
- IDEA
- Spring Boot 3.2.5
- Maven 3.x
プロジェクト構造
WeComDemo ├── pom.xml // プロジェクト依存関係設定 ├── src │ └── main │ ├── java │ │ └── com.example │ │ ├── Application.java // プロジェクト起動クラス │ │ ├── WeComController.java // コントローラ:リクエスト処理 │ │ └── WeComService.java // サービスクラス:ロジック処理 │ └── resources │ └── static │ └── test.html // テストフロントエンドページ
説明: このプロジェクトでは application.yml を設定していません。Spring Boot のデフォルト設定で実行可能です。
完全な機能の実装
ステップ1:pom.xml を修正し、Spring Boot 構成を追加
pom.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+ を使用する必要があります。
ステップ2:依存関係を更新
IntelliJ の右側にある「Maven」ツールウィンドウの更新ボタン(🔄)をクリックするか、pom.xml を右クリック → Add as Maven Project を選択すると、IDE が自動的に Spring Boot 依存関係をダウンロードします。
ステップ3:Main.java を Spring Boot 起動クラスに修正
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
問題の確認: @SpringBootApplication を追加し忘れると、ApplicationContext の起動に失敗し、コンソールに Web コンテナクラス(例:Tomcat)が見つからない、または Controller Bean を作成できないというメッセージが表示される可能性があります。解決策:注釈が正しく追加されていることを確認してください。
ステップ4:サービスクラス WeComService.java を作成
access_token のキャッシュ取得、jsapi_ticket のキャッシュ、JS-SDK 署名生成ロジックを提供:
String raw = String.format(
"jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s",
jsapiTicket, nonceStr, timestamp, url);
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(raw.getBytes(StandardCharsets.UTF_8));
注意:
- 署名計算はパラメータの順序と形式を厳密に守る必要があります
access_tokenとjsapi_ticketはキャッシュすることをお勧めし、頻繁なリクエストを避けてください- 返却形式には
appId、timestamp、nonceStr、signatureを含める必要があります
JS-SDK パラメータ生成
- パラメータ構成:
jsapi_ticket、nonceStr、timestamp、url - アルゴリズム:
SHA-1(raw文字列)で署名を生成 - 返却構造:
appId、timestamp、nonceStr、signatureを含む
ステップ5:コントローラークラス WeComController.java
以下のインターフェースを提供:
| インターフェースアドレス | リクエストメソッド | 機能の説明 |
|---|---|---|
/wecom/token |
GET | access_token を取得 |
/wecom/department/users |
GET | 指定された部門のメンバーリストを取得 |
/wecom/js-sdk-config |
GET | JS-SDK 初期化構成情報を取得 |
よくある問題:
- 自動注入に失敗した場合は、
@Serviceと@RestControllerの注釈が追加されているか確認してください - 依存関係の注入に失敗した場合、コンソールに
UnsatisfiedDependencyExceptionが表示される可能性があります
ステップ6:フロントエンドテストページ test.html を作成
機能:
- トークンを取得して表示
- 部門メンバーを取得してテーブルで表示(スクロールバー付き)
- JS SDK を初期化し、スキャン、位置情報取得などのテストボタンをサポート
wx.config({
appId: config.appId,
timestamp: config.timestamp,
nonceStr: config.nonceStr,
signature: config.signature,
jsApiList: ["scanQRCode", "getLocation"]
});
wx.ready(function() {
alert("✅ 企業 WeChat JS SDK 初期化成功");
});
失敗処理:
wx.error(function (err) {
alert("❌ SDK 初期化失敗: " + JSON.stringify(err));
});
ページ構造は明確で、すべてのロジックは window.onload で初期化できます。
ステップ7:Spring Boot アプリケーションを実行
IntelliJ で Main.java を右クリック → Run 'Main'、または緑色の ▶ ボタンをクリックします。
以下のようなメッセージが表示されます:
Tomcat started on port(s): 8080 Started Main in x.xxx seconds
サービスが正常に起動したことを示します。
ステップ8:インターフェースの表示
http://localhost:8080/test.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 が自動的にマッピングするようにする |
エラーコアヒント:
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)の依存関係が欠けているため、Spring Boot が Web サービスの起動に失敗します。
最も一般的な原因:
pom.xmlにspring-boot-starter-web依存関係が欠けている、またはスペルミス- Maven が依存関係を正常にダウンロードできなかった(ネットワークまたはリポジトリの問題)
@SpringBootApplicationが追加されていない
今後の拡張方向
- 企業 WeChat 認証(OAuth2)を統合
- より多くの JS API(録音、音声認識、地図を開くなど)をサポート
- Redis を使用して token をキャッシュし、パフォーマンスと堅牢性を向上させる
- フロントエンドとバックエンドを分離し、Vue、React などのフレームワークを使用する
推奨
Maven Central(Maven 中央リポジトリ Web バージョン)
これは最も一般的で、ほとんどの Java 開発者が使用するウェブサイトです —— グラフィカルな Maven 中央リポジトリ検索プラットフォーム:
ウェブサイト:
🌐 https://mvnrepository.com
Spring Initializr 公式サイトを使用してプロジェクトを作成(グラフィカルウィンドウバージョン)
このウェブサイトは、実行可能な Spring Boot プロジェクトを自動的に生成し、zip ファイルにパッケージ化します。zip を解凍し、IDEA で開くことができます。
アドレス:
🌐 https://start.spring.io
付録:完全なファイル(コードを自分で補完できます)
pom.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>com.example</groupId>
<artifactId>WeComDemo</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>
test.html ✅
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>企業 WeChat インターフェーステスト</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>
<!-- 企業 WeChat JS SDK を導入 -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
<script>
// 企業 WeChat 環境を判断
function isWeComBrowser() {
return /wxwork/i.test(navigator.userAgent);
}
// デバイスタイプを判断
function isMobileDevice() {
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
}
// 企業 WeChat 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("企業 WeChat JS SDK が準備完了");
alert("✅ 企業 WeChat 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("❌ 企業 WeChat 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 () {
if (isWeComBrowser() && isMobileDevice()) {
initWeComJsSdk();
} else {
console.warn("非企業 WeChat 環境、機能制限あり");
}
};
</script>
</head>
<body>
<h1>企業 WeChat インターフェーステスト</h1>
<!-- トークンを取得 -->
<button onclick="getToken()">トークンを取得</button>
<p>トークン:(上のボタンをクリック)</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>
<!-- 機能テスト -->
<h3>機能テスト:</h3>
<button id="scanBtn">スキャン</button>
<button id="locBtn">現在位置を取得</button>
</body>
</html>
Application.java ✅
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
WeComService.java ✅
package com.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;
@Service
public class WeComService {
private static final String CORP_ID = "あなたの企業 WeChat 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;
}
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×tamp=%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 ✅
package com.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;
@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);
}
}