138 lines
6.6 KiB
Java
138 lines
6.6 KiB
Java
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);
|
||
}
|
||
} |