|
@@ -0,0 +1,150 @@
|
|
|
|
|
+package shop.alien.store.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
|
|
+import okhttp3.Request;
|
|
|
|
|
+import okhttp3.Response;
|
|
|
|
|
+import org.apache.commons.codec.digest.DigestUtils;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
|
+import shop.alien.store.config.BaseRedisService;
|
|
|
|
|
+import shop.alien.store.config.WxJsSdkProperties;
|
|
|
|
|
+import shop.alien.store.dto.WxJsSdkConfigVo;
|
|
|
|
|
+import shop.alien.store.service.WxJsSdkService;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.security.SecureRandom;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 微信公众号 JS-SDK 签名实现
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class WxJsSdkServiceImpl implements WxJsSdkService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String ACCESS_TOKEN_CACHE_KEY = "wx:jsapi:access_token";
|
|
|
|
|
+ private static final String JSAPI_TICKET_CACHE_KEY = "wx:jsapi:jsapi_ticket";
|
|
|
|
|
+ private static final long CACHE_SECONDS = 7000L;
|
|
|
|
|
+
|
|
|
|
|
+ private static final String ACCESS_TOKEN_URL =
|
|
|
|
|
+ "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
|
|
|
|
+ private static final String JSAPI_TICKET_URL =
|
|
|
|
|
+ "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi";
|
|
|
|
|
+
|
|
|
|
|
+ private static final SecureRandom RANDOM = new SecureRandom();
|
|
|
|
|
+ private static final char[] NONCE_CHARS =
|
|
|
|
|
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
|
|
|
|
|
+
|
|
|
|
|
+ private final WxJsSdkProperties wxJsSdkProperties;
|
|
|
|
|
+ private final BaseRedisService redisService;
|
|
|
|
|
+
|
|
|
|
|
+ private final OkHttpClient httpClient = new OkHttpClient.Builder()
|
|
|
|
|
+ .connectTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
+ .readTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
+ .writeTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public WxJsSdkConfigVo buildJsSdkConfig(String url) {
|
|
|
|
|
+ if (!StringUtils.hasText(url)) {
|
|
|
|
|
+ throw new IllegalArgumentException("url 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ String pageUrl = stripHash(url.trim());
|
|
|
|
|
+
|
|
|
|
|
+ String ticket = getJsapiTicket();
|
|
|
|
|
+ String nonceStr = randomNonceStr(15);
|
|
|
|
|
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
|
|
|
|
+
|
|
|
|
|
+ String signStr = "jsapi_ticket=" + ticket
|
|
|
|
|
+ + "&noncestr=" + nonceStr
|
|
|
|
|
+ + "×tamp=" + timestamp
|
|
|
|
|
+ + "&url=" + pageUrl;
|
|
|
|
|
+ String signature = DigestUtils.sha1Hex(signStr);
|
|
|
|
|
+
|
|
|
|
|
+ return new WxJsSdkConfigVo(
|
|
|
|
|
+ wxJsSdkProperties.getAppId(),
|
|
|
|
|
+ timestamp,
|
|
|
|
|
+ nonceStr,
|
|
|
|
|
+ signature
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getAccessToken() {
|
|
|
|
|
+ String cached = redisService.getString(ACCESS_TOKEN_CACHE_KEY);
|
|
|
|
|
+ if (StringUtils.hasText(cached)) {
|
|
|
|
|
+ return cached;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String appId = wxJsSdkProperties.getAppId();
|
|
|
|
|
+ String appSecret = wxJsSdkProperties.getAppSecret();
|
|
|
|
|
+ if (!StringUtils.hasText(appId) || !StringUtils.hasText(appSecret)) {
|
|
|
|
|
+ throw new IllegalStateException("未配置 wx.appId / wx.appSecret");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String requestUrl = String.format(ACCESS_TOKEN_URL, appId, appSecret);
|
|
|
|
|
+ JSONObject body = getWechatJson(requestUrl, "获取 access_token");
|
|
|
|
|
+ String accessToken = body.getString("access_token");
|
|
|
|
|
+ if (!StringUtils.hasText(accessToken)) {
|
|
|
|
|
+ throw new RuntimeException("获取 access_token 失败,返回数据异常: " + body);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ redisService.setString(ACCESS_TOKEN_CACHE_KEY, accessToken, CACHE_SECONDS);
|
|
|
|
|
+ return accessToken;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getJsapiTicket() {
|
|
|
|
|
+ String cached = redisService.getString(JSAPI_TICKET_CACHE_KEY);
|
|
|
|
|
+ if (StringUtils.hasText(cached)) {
|
|
|
|
|
+ return cached;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String accessToken = getAccessToken();
|
|
|
|
|
+ String requestUrl = String.format(JSAPI_TICKET_URL, accessToken);
|
|
|
|
|
+ JSONObject body = getWechatJson(requestUrl, "获取 jsapi_ticket");
|
|
|
|
|
+ String ticket = body.getString("ticket");
|
|
|
|
|
+ if (!StringUtils.hasText(ticket)) {
|
|
|
|
|
+ throw new RuntimeException("获取 jsapi_ticket 失败,返回数据异常: " + body);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ redisService.setString(JSAPI_TICKET_CACHE_KEY, ticket, CACHE_SECONDS);
|
|
|
|
|
+ return ticket;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private JSONObject getWechatJson(String url, String action) {
|
|
|
|
|
+ Request request = new Request.Builder().url(url).get().build();
|
|
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
|
|
+ throw new RuntimeException(action + "失败,HTTP状态码: " + response.code());
|
|
|
|
|
+ }
|
|
|
|
|
+ String responseBody = response.body() != null ? response.body().string() : "";
|
|
|
|
|
+ JSONObject jsonObject = JSON.parseObject(responseBody);
|
|
|
|
|
+ if (jsonObject.containsKey("errcode") && jsonObject.getIntValue("errcode") != 0) {
|
|
|
|
|
+ log.error("{}失败,errcode={}, errmsg={}, body={}",
|
|
|
|
|
+ action, jsonObject.getIntValue("errcode"), jsonObject.getString("errmsg"), responseBody);
|
|
|
|
|
+ throw new RuntimeException(action + "失败: " + jsonObject.getString("errmsg"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return jsonObject;
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ log.error("{}网络请求异常", action, e);
|
|
|
|
|
+ throw new RuntimeException(action + "网络请求失败: " + e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String stripHash(String url) {
|
|
|
|
|
+ int hashIndex = url.indexOf('#');
|
|
|
|
|
+ return hashIndex >= 0 ? url.substring(0, hashIndex) : url;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String randomNonceStr(int length) {
|
|
|
|
|
+ StringBuilder sb = new StringBuilder(length);
|
|
|
|
|
+ for (int i = 0; i < length; i++) {
|
|
|
|
|
+ sb.append(NONCE_CHARS[RANDOM.nextInt(NONCE_CHARS.length)]);
|
|
|
|
|
+ }
|
|
|
|
|
+ return sb.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|