From f37508674b9cefbfcdb6c9c05e9e9a06f36eec24 Mon Sep 17 00:00:00 2001 From: sh Date: Mon, 24 Nov 2025 18:01:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=98=BF=E9=87=8C=E4=BA=91?= =?UTF-8?q?=E5=86=85=E7=BD=91=E8=8E=B7=E5=8F=96=E7=BB=9F=E4=B8=80token?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/cms/handler/AliyunNlsTokenUtil.java | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/handler/AliyunNlsTokenUtil.java diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/handler/AliyunNlsTokenUtil.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/handler/AliyunNlsTokenUtil.java new file mode 100644 index 0000000..7b59713 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/handler/AliyunNlsTokenUtil.java @@ -0,0 +1,138 @@ +package com.ruoyi.cms.handler; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; + +public class AliyunNlsTokenUtil { + private static final Logger logger = LoggerFactory.getLogger(AliyunNlsTokenUtil.class); + // Nginx 代理配置(与 Nginx 一致) + private static final String PROXY_HOST = "192.168.2.102"; + private static final int PROXY_PORT = 10044; + // Token 接口代理地址(通过 Nginx 转发) + private static final String TOKEN_PROXY_URL = "http://" + PROXY_HOST + ":" + PROXY_PORT + "/"; + + // 单例 OkHttp 客户端(带代理,全局复用) + private static OkHttpClient proxyOkHttpClient; + + static { + // 初始化带代理的 OkHttp 客户端(仅初始化一次) + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_HOST, PROXY_PORT)); + proxyOkHttpClient = new OkHttpClient.Builder() + .proxy(proxy) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); + } + + /** + * 手动生成 Token(绕开 SDK 无代理逻辑) + * @param accessKeyId 阿里云 AccessKeyId + * @param accessKeySecret 阿里云 AccessKeySecret + * @param accessToken SDK 的 AccessToken 对象(用于设置 token 和 expireTime) + */ + public static void generateToken(String accessKeyId, String accessKeySecret, com.alibaba.nls.client.AccessToken accessToken) throws Exception { + // 1. 生成阿里云 API 签名(HMAC-SHA1) + String timestamp = getIso8601UtcTimestamp(); + String nonce = String.valueOf(System.currentTimeMillis()); + String signature = generateSignature(accessKeyId, accessKeySecret, timestamp, nonce); + + // 2. 构造请求 URL(带签名参数) + String requestUrl = TOKEN_PROXY_URL + + "?Action=CreateToken" + + "&Format=JSON" + + "&Version=2019-02-28" + + "&AccessKeyId=" + URLEncoder.encode(accessKeyId, "UTF-8") + + "&Timestamp=" + URLEncoder.encode(timestamp, "UTF-8") + + "&SignatureNonce=" + URLEncoder.encode(nonce, "UTF-8") + + "&Signature=" + URLEncoder.encode(signature, "UTF-8") + + "&SignatureMethod=HMAC-SHA1" + + "&SignatureVersion=1.0"; + + // 打印完整请求 URL(用于排查参数问题) + logger.info("Token 生成请求 URL:{}", requestUrl); + + // 3. 发送 GET 请求(走 Nginx 代理) + Request request = new Request.Builder().url(requestUrl).build(); + try (Response response = proxyOkHttpClient.newCall(request).execute()) { + String responseBody = response.body() != null ? response.body().string() : "无响应内容"; + logger.info("Token 接口响应 - 状态码:{},响应体:{}", response.code(), responseBody); + if (!response.isSuccessful()) { + throw new RuntimeException("Token 生成失败,状态码:" + response.code() + ",错误信息:" + responseBody); + } + com.alibaba.fastjson.JSONObject json = com.alibaba.fastjson.JSON.parseObject(responseBody); + + // 4. 提取 Token 和过期时间 + String token = json.getJSONObject("Token").getString("Id"); + long expireTime = json.getJSONObject("Token").getLong("ExpireTime"); + + // 5. 反射设置到 SDK 的 AccessToken 对象 + setAccessTokenField(accessToken, "token", token); + setAccessTokenField(accessToken, "expireTime", expireTime); + logger.info("Token 生成成功,过期时间:{}", expireTime); + } + } + + /** + * 生成阿里云 API 签名(遵循阿里云规范) + */ + private static String generateSignature(String accessKeyId, String accessKeySecret, String timestamp, String nonce) throws Exception { + // 1. 按字典序拼接参数(必须严格排序) + StringBuilder paramBuilder = new StringBuilder(); + paramBuilder.append("AccessKeyId=").append(URLEncoder.encode(accessKeyId, "UTF-8")) + .append("&Action=").append(URLEncoder.encode("CreateToken", "UTF-8")) + .append("&Format=").append(URLEncoder.encode("JSON", "UTF-8")) + .append("&SignatureMethod=").append(URLEncoder.encode("HMAC-SHA1", "UTF-8")) + .append("&SignatureNonce=").append(URLEncoder.encode(nonce, "UTF-8")) + .append("&SignatureVersion=").append(URLEncoder.encode("1.0", "UTF-8")) + .append("&Timestamp=").append(URLEncoder.encode(timestamp, "UTF-8")) + .append("&Version=").append(URLEncoder.encode("2019-02-28", "UTF-8")); + + // 2. 构造签名原文(Method + & + 编码后的URL + & + 编码后的参数) + String method = "GET"; + String encodedUrl = URLEncoder.encode("/", "UTF-8"); // 根路径编码 + String encodedParams = URLEncoder.encode(paramBuilder.toString(), "UTF-8"); + String signatureText = method + "&" + encodedUrl + "&" + encodedParams; + + // 调试日志:对比服务器端预期的签名原文 + logger.info("本地计算的签名原文:{}", signatureText); + + // 3. HMAC-SHA1 加密 + Base64 编码 + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec((accessKeySecret + "&").getBytes("UTF-8"), "HmacSHA1")); + byte[] signatureBytes = mac.doFinal(signatureText.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(signatureBytes); + } + + /** + * 生成 ISO8601 格式的 UTC 时间戳(如:2025-11-24T10:00:00Z) + */ + private static String getIso8601UtcTimestamp() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // 强制使用 UTC 时区 + return sdf.format(new Date()); + } + + /** + * 反射设置 AccessToken 的私有字段 + */ + private static void setAccessTokenField(com.alibaba.nls.client.AccessToken accessToken, String fieldName, Object value) throws Exception { + java.lang.reflect.Field field = com.alibaba.nls.client.AccessToken.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(accessToken, value); + } +} \ No newline at end of file