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