微信 JSAPI 調用相機、相冊、圖片上傳下載需完成:JS-SDK 權限簽名驗證 → 前端選圖/拍照 → 上傳至微信服務器獲取 media_id → 後端下載素材落地自有存儲。本文提供前後端完整代碼實現,涵蓋簽名算法、錯誤處理、文件存儲規範。
前端頁面加載
│
├─→ 獲取當前頁面 URL(去除 #hash)
│
├─→ 後端 /getConfigInfo.json 返回簽名參數
│ ├─ access_token → jsapi_ticket
│ ├─ noncestr、timestamp、url
│ ├─ SHA1 簽名
│ └─ 返回 {appId, timestamp, nonceStr, signature, jsapi_ticket}
│
├─→ wx.config({debug: true, ...}) 驗證通過
│
├─→ 用戶點擊「調用相機」→ wx.chooseImage()
│
├─→ wx.uploadImage(localId) → 獲得 serverId (media_id)
│
├─→ 後端 /getPhoto.json?media_id=xxx 下載圖片
│ ├─ 微信媒體下載接口
│ ├─ 流式寫入本地/雲存儲
│ └─ 返回自有存儲 URL
│
└─→ 前端展示/業務入庫
@NoLogin
@RequestMapping(value = "/getConfigInfo.json", method = RequestMethod.POST)
@ResponseBody
public Object getConfigInfo(String url) throws NoSuchAlgorithmException {
// 1. 獲取 jsapi_ticket(需緩存,定時刷新)
JsapiTicket ticket = AccessTokenJsapiTicketThread.getTicket();
if (ticket == null) return JsonConvertor.convertFailResult(ErrorCodeEnum.SYSTEM_ERROR);
// 2. 簽名參數
long timestamp = System.currentTimeMillis() / 1000; // 秒級
String nonceStr = RandomStringUtils.randomAlphanumeric(16);
// 3. 簽名原文字符串(字典序拼接)
StringBuilder sb = new StringBuilder();
sb.append("jsapi_ticket=").append(ticket.getTicket())
.append("&noncestr=").append(nonceStr)
.append("×tamp=").append(timestamp)
.append("&url=").append(url); // 必須與前端傳入 URL 完全一致
// 4. SHA1 加密
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
String signature = bytesToHex(digest).toLowerCase();
// 5. 返回前端所需參數
Map map = new HashMap<>();
map.put("appId", WxPayConfPub.APPID);
map.put("timestamp", String.valueOf(timestamp));
map.put("nonceStr", nonceStr);
map.put("signature", signature);
map.put("jsapi_ticket", ticket.getTicket());
map.put("originalStr", sb.toString()); // 調試用
return JsonConvertor.convertSuccessResult(map);
}
// 工具方法:byte[] → 十六進制字符串
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
@NoLogin
@RequestMapping(value = "/getPhoto.json", method = RequestMethod.POST)
@ResponseBody
public Object getPhoto(String media_id) throws IOException {
AccessToken token = AccessTokenJsapiTicketThread.getAccessToken();
String url = "https://api.weixin.qq.com/cgi-bin/media/get?access_token="
+ token.getAccess_token() + "&media_id=" + media_id;
try (InputStream is = HttpUtil.initHttpsConnection(url, "GET").getInputStream()) {
// 1. 生成存儲路徑:/年月日/隨機文件名.jpg
String datePath = DateUtil.format(new Date(), "yyyyMMdd");
String fileName = DateUtil.format(new Date(), "yyyyMMddHHmmss")
+ RandomStringUtils.randomAlphanumeric(6) + ".jpg";
String relativePath = datePath + "/" + fileName;
File destFile = new File(imageRootPath, relativePath);
// 2. 確保目錄存在
File parent = destFile.getParentFile();
if (!parent.exists()) FileUtils.forceMkdir(parent);
// 3. 流式寫入(避免大文件 OOM)
try (FileOutputStream fos = new FileOutputStream(destFile)) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
}
// 4. 返回可訪問的相對路徑或完整 URL
return JsonConvertor.convertSuccessResult(relativePath);
} catch (IOException e) {
log.error("微信圖片下載失敗 media_id={}", media_id, e);
return JsonConvertor.convertFailResult(ErrorCodeEnum.SYSTEM_ERROR);
}
}
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script>
$(function() {
// 關鍵:獲取不含 #hash 的完整 URL
var url = window.location.href.split('#')[0];
$.post("/weixin/getConfigInfo.json", {url: url}, function(data) {
if (data.rs === 'f') return alert("系統錯誤");
var cfg = data.body;
wx.config({
debug: true, // 生產關閉
appId: cfg.appId,
timestamp: cfg.timestamp,
nonceStr: cfg.nonceStr,
signature: cfg.signature,
jsApiList: ['chooseImage', 'previewImage', 'uploadImage', 'downloadImage']
});
wx.ready(function() { console.log('wx.config success'); });
wx.error(function(res) { alert('wx.config failed: ' + res.errMsg); });
}, 'json');
});
var images = { localId: [], serverId: [] };
function takePhoto() {
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: function(res) {
images.localId = res.localIds;
uploadImages(0);
}
});
}
function uploadImages(index) {
if (index >= images.localId.length) return;
wx.uploadImage({
localId: images.localId[index],
success: function(res) {
images.serverId.push(res.serverId);
// 後端下載落地
$.post("/weixin/getPhoto.json", {media_id: res.serverId}, function(data) {
if (data.rs === 's') {
var serverUrl = data.body; // 如 20240115/abc123.jpg
previewImage(serverUrl); // 業務邏輯
}
}, 'json');
uploadImages(index + 1); // 遞歸上傳下一張
},
fail: function(res) { alert('上傳失敗: ' + JSON.stringify(res)); }
});
}
</script>
| 坑點 | 現象 | 解決方案 |
|---|---|---|
| URL 包含 #hash | 簽名驗證失敗 invalid signature | url.split('#')[0] 移除 hash 部分 |
| timestamp 單位不一致 | 簽名失效 | 統一用秒級(JS/JAVA 同) |
| nonceStr 重複 | 安全風險 | 每次請求生成隨機 16 位字符串 |
| 大文件下載 OOM | 堆內存溢出 | 流式讀寫,緩衝區 4KB |
| 目錄不存在 | FileNotFoundException | FileUtils.forceMkdir(parent) |
| access_token 過期 | 下載接口 40014/42001 | 統一緩存刷新機制,提前 3 分鐘續期 |
media_id 寫入 MQ,消費者異步下載,前端輪詢或 WebSocket 通知微信 JSAPI 圖片上傳下載鏈路涉及前端簽名驗證、媒體上傳、後端代理下載、文件落地四個關鍵環節。核心難點在於簽名參數一致性(URL、timestamp、nonceStr、jsapi_ticket)與大文件流式處理。掌握標準流程並建立完善的錯誤重試、監控體系,才能保障業務穩定運行。