-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathoauth
More file actions
326 lines (283 loc) · 12.7 KB
/
Copy pathoauth
File metadata and controls
326 lines (283 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.Base64;
public class DComApiClient {
// 固定配置
private static final String CONSUMER_KEY = "2343242424";
private static final String CONSUMER_SECRET = "2323dvfsdfsdfsfsfsdf";
private static final String FULL_API_URL = "https://api.abc.com/dd/api/generateDoc?includedata=true&strict=false";
// 基础URL(去掉query,用于签名)
private static final String BASE_SIGN_URL = "https://api.abc.com/dd/api/generateDoc";
// url上的查询参数,参与签名
private static final Map<String, String> QUERY_PARAMS;
static {
QUERY_PARAMS = new HashMap<>();
QUERY_PARAMS.put("includedata", "true");
QUERY_PARAMS.put("strict", "false");
}
/**
* RFC3986 标准URL编码,OAuth1专用
*/
private static String urlEncode(String val) throws Exception {
if (val == null) return "";
String encode = URLEncoder.encode(val, StandardCharsets.UTF_8.name());
return encode.replace("+", "%20")
.replace("*", "%2A")
.replace("%7E", "~");
}
/**
* 生成oauth_signature HMAC-SHA256
*/
private static String buildOauthSignature(String httpMethod,
String baseUrl,
Map<String, String> queryParams,
Map<String, String> oauthParams,
String consumerSecret,
String tokenSecret) throws Exception {
// 合并oauth参数 + url查询参数,TreeMap自动字典升序
Map<String, String> allSignParams = new TreeMap<>();
allSignParams.putAll(oauthParams);
allSignParams.putAll(queryParams);
// 拼接 parameterString
List<String> paramParts = new ArrayList<>();
for (Map.Entry<String, String> entry : allSignParams.entrySet()) {
String k = urlEncode(entry.getKey());
String v = urlEncode(entry.getValue());
paramParts.add(k + "=" + v);
}
String parameterString = String.join("&", paramParts);
// baseString = METHOD & ENCODE(baseUrl) & ENCODE(parameterString)
String part1 = urlEncode(httpMethod);
String part2 = urlEncode(baseUrl);
String part3 = urlEncode(parameterString);
String baseString = part1 + "&" + part2 + "&" + part3;
// 签名密钥:encode(consumerSecret)&encode(tokenSecret)
String signingKey = urlEncode(consumerSecret) + "&" + urlEncode(tokenSecret);
// HMAC-SHA256 计算
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
byte[] digest = mac.doFinal(baseString.getBytes(StandardCharsets.UTF_8));
// base64输出签名
return Base64.getEncoder().encodeToString(digest);
}
/**
* 组装Authorization OAuth1请求头
*/
private static String buildAuthHeader(Map<String, String> oauthParams, String signature) throws Exception {
List<String> headerParts = new ArrayList<>();
for (Map.Entry<String, String> entry : oauthParams.entrySet()) {
String k = urlEncode(entry.getKey());
String v = urlEncode(entry.getValue());
headerParts.add(k + "=\"" + v + "\"");
}
headerParts.add("oauth_signature=\"" + urlEncode(signature) + "\"");
return "OAuth " + String.join(", ", headerParts);
}
/**
* 创建忽略SSL证书校验的HttpClient(仅开发调试用,解决PKIX报错)
* 生产环境删除此方法,直接用 HttpClients.createDefault()
*/
private static CloseableHttpClient getUnsafeHttpClient() throws NoSuchAlgorithmException, KeyManagementException {
TrustStrategy trustAll = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
.loadTrustMaterial(null, trustAll)
.build();
SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
return HttpClients.custom()
.setSSLSocketFactory(sslFactory)
.build();
}
/**
* 对外调用主方法
* @param xmlBody 接口请求XML报文
* @return 接口返回XML响应
*/
public String callGenerateDocApi(String xmlBody) {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
String respResult = null;
try {
// 调试使用:忽略SSL证书;上线替换为 HttpClients.createDefault()
httpClient = getUnsafeHttpClient();
// 1. 构造OAuth1基础参数
Map<String, String> oauthParams = new HashMap<>();
long timestamp = System.currentTimeMillis() / 1000;
String nonce = UUID.randomUUID().toString().replace("-", "");
oauthParams.put("oauth_consumer_key", CONSUMER_KEY);
oauthParams.put("oauth_signature_method", "HMAC-SHA256");
oauthParams.put("oauth_timestamp", String.valueOf(timestamp));
oauthParams.put("oauth_nonce", nonce);
oauthParams.put("oauth_version", "1.0");
// 无用户token,不传 oauth_token
// 2. 生成签名,tokenSecret为空字符串
String signature = buildOauthSignature(
"POST",
BASE_SIGN_URL,
QUERY_PARAMS,
oauthParams,
CONSUMER_SECRET,
""
);
// 3. 组装Authorization头
String authHeader = buildAuthHeader(oauthParams, signature);
// 4. 构建POST请求
HttpPost httpPost = new HttpPost(FULL_API_URL);
// OAuth认证头
httpPost.setHeader("Authorization", authHeader);
// XML报文头
httpPost.setHeader("Content-Type", "application/xml;charset=UTF-8");
// 设置XML请求体
StringEntity xmlEntity = new StringEntity(xmlBody, StandardCharsets.UTF_8);
httpPost.setEntity(xmlEntity);
// 5. 执行请求
response = httpClient.execute(httpPost);
respResult = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
System.out.println("响应状态码:" + response.getStatusLine().getStatusCode());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("调用generateDoc接口异常", e);
} finally {
// 关闭资源
try {
if (response != null) response.close();
if (httpClient != null) httpClient.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return respResult;
}
// 测试入口
public static void main(String[] args) {
ddComApiClient client = new ddComApiClient();
// 替换为你的真实XML报文
String requestXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<Root>\n" +
" <Data>测试数据</Data>\n" +
"</Root>";
String responseXml = client.callGenerateDocApi(requestXml);
System.out.println("接口返回XML:\n" + responseXml);
}
}
关键说明(对应你的业务场景)
1. URL 拆分规则
完整地址:https://au4.smartcom.cluld/smart/api/generateDoc?includedata=true&strict=false
签名用基准 URL:https://au4.smartcom.cluld/smart/api/generateDoc
query 参数 includedata=true&strict=false 存入 QUERY_PARAMS Map,参与签名
2. POST XML 签名规则
Content-Type=application/xml,XML 请求体不参与签名,仅 OAuth 参数 + URL query 参数参与。
3. OAuth1 参数说明
无 oauth_token、oauth_token_secret,加密密钥拼接为:encode(consumerSecret)&
时间戳是秒级,nonce 使用 UUID 保证每次唯一防重放
签名算法固定 HMAC-SHA256
4. SSL 证书报错处理
代码中 getUnsafeHttpClient() 临时关闭证书校验,解决你最开始的 PKIX path building failed:
开发 / 本地调试可用
生产环境必须删除该方法,替换为 HttpClients.createDefault(),并将 au4.smartcom.cluld 证书导入 JDK cacerts
5. 常见签名错误排查点
urlEncode 没有替换 + → 已封装标准编码方法
query 参数没有参与签名 → 代码已单独提取放入 TreeMap
baseUrl 带上问号参数 → 代码分离了签名基准地址和查询参数
时间戳用毫秒 → 代码除以 1000 转为秒
HTTP 方法小写 → 固定传入大写 POST
结论:必须保留 &,tokenSecret 传空字符串即可
RFC5849 标准明确规定签名密钥格式:
plaintext
signing-key = encode(consumer_secret) "&" encode(token_secret)
分两种场景:
有 token + tokenSecret
plaintext
encode(consumerSecret) + "&" + encode(tokenSecret)
无 token、无 tokenSecret(你当前场景)
tokenSecret = "",编码后还是空串,最终密钥为:
plaintext
encode(consumerSecret) + "&" + ""
等价于 encode(consumerSecret) + "&"
错误写法(会直接签名校验失败)
删掉 &,只留 consumerSecret:
java
运行
// 错!
String signingKey = urlEncode(consumerSecret);
正确写法(代码无需改动)
你现有代码逻辑完全兼容无 tokenSecret 的情况,调用时传入空字符串 "":
java
运行
String signature = buildOauthSignature(
"POST",
BASE_SIGN_URL,
QUERY_PARAMS,
oauthParams,
CONSUMER_SECRET,
"" // 无tokenSecret传空串,不能传null
);
内部拼接逻辑不变:
java
运行
// tokenSecret为空,结果:xxxEncodedSecret&
String signingKey = urlEncode(consumerSecret) + "&" + urlEncode(tokenSecret);
补充注意点
tokenSecret 不能传 null,否则 urlEncode(null) 返回空字符串,效果一样,但规范传 "";
服务端同样会按 secret& 计算密钥,少一个 & 摘要完全不同,直接返回签名不匹配;
只有 OAuth1.0a 授权流程拿到 AccessToken 后,才会填充真实 tokenSecret,单纯客户端调用接口无用户授权一律填空。
==================================================
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
import org.json.JSONObject;
import org.json.XML;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
public class XmlBase64ToPdfUtil {
/**
* 处理XML <a><name></name><data>pdfBase64</data></a>
* @param xmlStr 完整xml字符串
* @param fileName 生成的pdf文件名,如 "result.pdf"
* @throws Exception 解析/解码/文件写入异常
*/
public static void convertXmlToPdf(String xmlStr, String fileName) throws Exception {
// 1. xml转顶层json
JSONObject rootJson = XML.toJSONObject(xmlStr);
// 2. 获取a节点下的json对象
JSONObject aJson = rootJson.getJSONObject("a");
// 3. 取出data中的base64,清除换行空格
String base64Text = aJson.optString("data", "").replaceAll("\\s", "");
if (base64Text.isBlank()) {
throw new RuntimeException("xml中data节点无base64数据");
}
// 4. base64解码二进制pdf
byte[] pdfBytes = Base64.getDecoder().decode(base64Text);
// 5. 拼接项目根目录文件路径
String projectDir = System.getProperty("user.dir");
Path pdfPath = Paths.get(projectDir, fileName);
// 6. NIO Files一次性写入文件,自动关流
Files.write(pdfPath, pdfBytes);
System.out.println("PDF生成成功,路径:" + pdfPath.toAbsolutePath());
}
public static void main(String[] args) throws Exception {
// 测试xml
String xml = "<a><name>测试文件</name><data>这里替换成真实PDF的base64编码</data></a>";
// 生成文件到项目根目录 test.pdf
convertXmlToPdf(xml, "test.pdf");
}
}