项目场景:
【微信小程序】H5页面调用JSSDK中chooseImage多选图片接口
本文章是利用 [微信公众平台的接口测试号] 介绍JSSDK接口如何配置及调用样例
准备工作
- 需要注册一个接口测试号(也可以在正式的公众号平台上操作)
https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login - 获取
appID
和appsecret
(注册完接口测试号就有) - 需要公网IP域名:用于配置
JS接口安全域名
原理解释
微信原生解释文档:
https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html
ps:这篇开发文档对我这种新手真的很不友好…
大概意思就是:
1.前端
携带前端H5页面的url参数向后端请求下述若干信息,完成config配置
url:需要在微信公众平台的接口测试号中配置到
JS接口安全域名
不需要写https://
直接就是example.com
好像要求是https,不能是http,但本人没有测试,看其他文章这样说就改了
至于域名啥的就自己去搞吧
2.后端:
通过后端代码,拿着appID
和appsecret
向微信请求到AccessToken
然后还是用后端代码,拿着刚获取的AccessToken
向微信请求到jsapi_ticket
然后还是在后端,就可以用一堆参数加密生成签名signature
(提供签名校验工具)
校验网址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
前端通过接口向后端请求到appID、appsecret、signature
等参数就可以了
(官方提供了后端示例代码)
示例代码链接:https://www.weixinsxy.com/jssdk/sample.zip
JS-SDK说明文档——签名生成规则如下:
1、参与签名的字段包括noncestr
(随机字符串), 有效的jsapi_ticket
,timestamp
(时间戳),url
(当前网页的URL,不包含#及其后面部分) 。
2、对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。
3、这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
另附:接入微信公众平台开发的指南说明
https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
具体代码:
前端我在Vs Code中用Live Server插件在本地运行
后端直接写个启动类就行了
然后用某壳做个内网穿透就可以在本地临时测试了
有些细节内容,我放在注释里了,比如:7200秒的缓存,Token的含义
前端代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>测试页面</title>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1>选择图片</h1>
<button id="chooseImage">选择图片</button>
<div id="result"></div>
<script>
$(document).ready(function() {
const pageUrl = window.location.href;
// 请求后端获取微信配置
$.ajax({
url: 'https://xxxxxxxxxx/getWXconfig',
method: 'POST',
data: {
url: pageUrl // 当前页面的 URL
},
success: function(data) {
// 初始化 JSSDK
wx.config({
debug: true, // 开启调试
appId: data.appId,
timestamp: data.timestamp,
nonceStr: data.noncestr,
signature: data.signature,
jsApiList: ['chooseImage'] // 需要使用的 JS API 列表
});
// 调用选择图片接口
wx.ready(function() {
$('#chooseImage').on('click', function() {
wx.chooseImage({
count: 5, // 默认9
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success: function(res) {
const localIds = res.localIds; // 返回选定照片的本地ID
$('#result').html('<p>选择的图片ID: ' + localIds.join(', ') + '</p>');
},
fail: function(err) {
console.error('选择图片失败', err);
}
});
});
});
wx.error(function(res) {
console.error('wx.config 失败', res);
});
},
error: function(xhr, status, error) {
console.error('请求失败', error);
}
});
});
</script>
</body>
</html>
后端(服务器端):
启动类
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
核心代码
package com.example.demo;
import org.json.JSONObject;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@RestController
public class wxGetShareData {
private static String appid = "wx0x0x0x0x0x0x0x";
private static String secret = "abcdefghijklmn1234567890";
private static String token = "token123"; // 自定义的Token,与页面token对应
// 缓存 Access Token 和 Ticket
// Access_Token (有效期7200秒,开发者必须在自己的服务全局缓存access_token)
private static Map<String, String> tokenCache = new ConcurrentHashMap<>();
private static Map<String, Long> tokenExpiry = new HashMap<>();
private static final long TOKEN_EXPIRY_TIME = 7000 * 1000;
// 获取微信accessToken的静态方法
public static String getAccessToken() {
String cachedToken = tokenCache.get("accessToken");
Long expiryTime = tokenExpiry.get("accessTokenExpiry");
// 如果缓存存在且未过期,直接返回缓存的accessToken
if (cachedToken != null && expiryTime != null && System.currentTimeMillis() < expiryTime) {
return cachedToken;
}
String requestUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid
+ "&secret=" + secret;
try {
// 创建 URL 对象
URL url = new URL(requestUrl);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("Accept", "application/json");
// 读取响应
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8"));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// 解析 JSON 响应
JSONObject data = new JSONObject(response.toString());
String accessToken = data.getString("access_token");
// 缓存 accessToken 和过期时间
tokenCache.put("accessToken", accessToken);
tokenExpiry.put("accessTokenExpiry", System.currentTimeMillis() + TOKEN_EXPIRY_TIME);
return accessToken;
} else {
System.out.println("GET request failed. Response Code: " + responseCode);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String getTicket() {
String accessToken = getAccessToken();
if (accessToken == null) {
System.out.println("Access token is null");
return null;
}
String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=" + accessToken;
try {
URL object = new URL(url);
HttpURLConnection con = (HttpURLConnection) object.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("Accept", "application/json");
StringBuilder sb = new StringBuilder();
int httpResult = con.getResponseCode();
if (httpResult == HttpURLConnection.HTTP_OK) {
BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8"));
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
br.close();
JSONObject data = new JSONObject(sb.toString());
return data.getString("ticket");
} else {
System.out.println("GET request failed: " + con.getResponseMessage());
}
return null;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 签名算法
public static String signString(String jsapi_ticket, String url, String nonce_str, String timestamp) {
Map<String, String> ret = sign(jsapi_ticket, url, nonce_str, timestamp);
return ret.get("signature");
}
// 签名算法中的string1的顺序
public static Map<String, String> sign(String jsapi_ticket, String url, String nonce_str, String timestamp) {
Map<String, String> ret = new HashMap<>();
String string1;
String signature = "";
// 注意这里参数名必须全部小写,且必须有序
string1 = "jsapi_ticket=" + jsapi_ticket +
"&noncestr=" + nonce_str +
"×tamp=" + timestamp +
"&url=" + url;
System.out.println("string1:"+string1);
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes("UTF-8"));
signature = byteToHex(crypt.digest());
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
}
ret.put("url", url);
ret.put("jsapi_ticket", jsapi_ticket);
ret.put("nonceStr", nonce_str);
ret.put("timestamp", timestamp);
ret.put("signature", signature);
return ret;
}
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
private boolean checkSignature(String signature, String timestamp, String nonce) {
String[] params = {token, timestamp, nonce};
Arrays.sort(params); // 字典序排序
StringBuilder sb = new StringBuilder();
for (String param : params) {
sb.append(param);
}
String temp = sha1(sb.toString());
return temp.equals(signature);
}
// 签名算法中的加密
private String sha1(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] hash = digest.digest(input.getBytes("UTF-8"));
return byteToHex(hash);
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
// 调用接口
@RequestMapping(value = "/getWXconfig", method = {RequestMethod.POST, RequestMethod.GET})
public void getWechatConfig(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 添加 CORS 头部
// 这段代码是我在运行的时候报错(CORS(跨源资源共享) 错误):Access to XMLHttpRequest at 'http://xxxxxxxxxxxx/getWXconfig' from origin 'http://8578g29f45.vicp.fun' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
response.setHeader("Access-Control-Allow-Origin", "*"); // 允许所有来源
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); // 允许的请求方法
response.setHeader("Access-Control-Allow-Headers", "Content-Type"); // 允许的请求头
// 验证请求的合法性
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
// 这段代码是Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)
// 测试时发现忽略掉也行
// if (request.getMethod().equalsIgnoreCase("GET")) {
// if (checkSignature(signature, timestamp, nonce)) {
// response.getWriter().write(echostr); // 返回echostr,接入成功
// } else {
// response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid signature");
// }
// return;
// }
// POST 请求处理
System.out.println("------------------------------");
System.out.println(LocalDateTime.now());
System.out.println("开始配置wxconfig");
Map<String, Object> map = new HashMap<>();
String jsapi_ticket = getTicket();
if (jsapi_ticket != null) System.out.println("已获取ticket");
System.out.println("jsapi_ticket:" + jsapi_ticket);
timestamp = String.valueOf(new Date().getTime() / 1000); // 获取当前时间戳(秒)
System.out.println("timestamp:" + timestamp);
String noncestr = UUID.randomUUID().toString(); // 生成随机字符串
System.out.println("noncestr:" + noncestr);
String url = request.getParameter("url"); // 获取请求参数中的url
System.out.println("url:" + url);
if (url == null || url.isEmpty()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "URL parameter is missing");
return;
}
// 生成签名
signature = signString(jsapi_ticket, url, noncestr, timestamp);
System.out.println("signature:" + signature);
// 设置返回的参数
map.put("jsapi_ticket", jsapi_ticket);
map.put("timestamp", timestamp);
map.put("noncestr", noncestr);
map.put("signature", signature);
map.put("appId", appid);
// 设置响应类型
response.setContentType("application/json");
response.getWriter().write(new JSONObject(map).toString()); // 返回JSON格式
}
}
参考文章
https://blog.csdn.net/qq_41108402/article/details/105971724
https://blog.csdn.net/qq_38366657/article/details/123295493
https://huaweicloud.csdn.net/64e5d7ca6ffa502025760442.html
https://blog.csdn.net/chmod_R_755/article/details/76091774 (这篇文章启发很大)
https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html