首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

微信 JSAPI 權限配置與圖片上傳下載完整實現:從簽名驗證到媒體素材落地

     編輯:Java 綜合教學

前言

微信 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
    │
    └─→ 前端展示/業務入庫

二、後端核心實現

1. 簽名配置接口

@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();
}

2. 圖片下載落地接口

@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 signatureurl.split('#')[0] 移除 hash 部分
timestamp 單位不一致簽名失效統一用秒級(JS/JAVA 同)
nonceStr 重複安全風險每次請求生成隨機 16 位字符串
大文件下載 OOM堆內存溢出流式讀寫,緩衝區 4KB
目錄不存在FileNotFoundExceptionFileUtils.forceMkdir(parent)
access_token 過期下載接口 40014/42001統一緩存刷新機制,提前 3 分鐘續期

五、生產環境優化建議

  1. 圖片存儲雲化:接入 S3/OSS/COS,返回簽名 URL,減輕應用服務器 I/O
  2. 異步下載:media_id 寫入 MQ,消費者異步下載,前端輪詢或 WebSocket 通知
  3. 圖片處理管道:壓縮、水印、格式轉換(WebP)、縮略圖生成
  4. 安全防護:下載接口驗證 Referer/Token,防止媒體素材被盜鏈
  5. 監控告警:下載成功率、耗時 P99、磁盤佔用、微信接口錯誤碼分布

總結

微信 JSAPI 圖片上傳下載鏈路涉及前端簽名驗證、媒體上傳、後端代理下載、文件落地四個關鍵環節。核心難點在於簽名參數一致性(URL、timestamp、nonceStr、jsapi_ticket)與大文件流式處理。掌握標準流程並建立完善的錯誤重試、監控體系,才能保障業務穩定運行。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved