Java实现CSDN博客安全头x-ca-nonce与x-ca-signature的生成与测试
# 结论
**直接上结论,后面分析部分感兴趣的可以看一下,不感兴趣的忽略即可**
1. x-ca-key,x-ca-signature-headers:可以看做固定值
2. x-ca-nonce:随机串,只要格式和位数和csdn的一致即可,也可以用固定值
4. x-ca-signature:是对请求信息加密后的结果,用的是HmacSHA256加密算法,目前的加密appSecret = "9znpamsyl2c7cdrr9sas0le9vbc3r6ba",后续可能会变动
5. 需要加密的内容:请求方式,accept ,contentType ,date(默认空字符串),x-ca-key(固定值),x-ca-nonce(随机串),path(去掉域名部分),请求参数(只有get请求有,post请求,没有这部分)组成中间用\n分割,示例如下
```
"POST\n*/*\n\napplication/json\n\nx-ca-key:203803574\nx-ca-nonce:eff837eb-901e-403f-9ed3-d17d0227b16b\n/blog-console-api/v3/mdeditor/saveArticle"
```
7. post请求由于参数不参与加密,所以可以直接复用浏览器里面的x-ca-nonce和x-ca-signature
8. 怎么生成签名可以参考测试用例部分:将测试用例里面的请求方式信息和x-ca-nonce换成自己的即可,其中x-ca-nonce也可以不换,最好还是每次新生成一个
# Java代码实战
以下代码是基于springboot编写,基本框架已经搭建好,可以直接运行,获取签名的工具类已经准备到位,工具类的单元测试已经ok
## x-ca-nonce生成
直接调用就行,就是个随机串,格式一致就行,没啥特别好说的
```java
public class XCaNonceGenerator {
public static String generateXCaNonce() {
Random rand = new SecureRandom();
StringBuilder xCaNonce = new StringBuilder();
String charList = "abcdefghijklmnopqrstuvwxyz0123456789";
for (int i = 0; i < 36; i++) {
if (i == 8 || i == 13 || i == 18 || i == 23) {
xCaNonce.append("-");
} else if (i == 14) {
xCaNonce.append("4");
} else {
xCaNonce.append(charList.charAt(rand.nextInt(charList.length())));
}
}
return xCaNonce.toString();
}
public static void main(String[] args) {
System.out.println(generateXCaNonce());
}
}
```
## 自定义数据传输对象
只需要构建好此对象,直接传入HmacSHA256Util的getXCaSignature()方法即可获得x-ca-signature
```java
@Data
@Accessors(chain = true)
public class HmacSHA256Dto {
/**
* 请求方式(大写)
*/
public String method;
/**
* 请求头中的accept(直接从浏览器拷贝即可)
*/
public String accept;
/**
* 请求头中的content-type(直接从浏览器拷贝即可)
*/
public String contentType="";
/**
* 当前版本默认空字符串
*/
public String date = "";
/**
* 请求的url
*/
public String url;
/**
* 加密秘钥,暂时是这个固定值,后期可能会变,需要留意
*/
public String appSecret = "9znpamsyl2c7cdrr9sas0le9vbc3r6ba";
/**
* get请求的参数
*/
public Map<String,Object> params;
/**
* 当前默认值为203803574,后期可能会变
*/
public String xCaKey = "203803574";
/**
* 随机串
*/
public String xCaNonce;
}
```
## 获取签名的工具类
```java
public class HmacSHA256Util {
@SneakyThrows
private static String hmacSHA256(String secret, String message) {
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
hmacSha256.init(secret_key);
byte[] bytes = hmacSha256.doFinal(message.getBytes());
return Base64.getEncoder().encodeToString(bytes);
}
/**
* 构建需要加密的字符串
*/
@SneakyThrows
private static String getMessage(HmacSHA256Dto hmacSHA256Dto) {
URL url = new URL(hmacSHA256Dto.url);
// 处理get请求的查询参数,例如 id=136507441&model_type=
String query = url.getQuery();
if (StringUtils.hasText(query)) {
// 例如 id=136507441
String[] split = query.split("&");
for (String s : split) {
String[] kv = s.split("=");
if (kv.length == 2) {
hmacSHA256Dto.getParams().put(kv[0], kv[1]);
} else {
hmacSHA256Dto.getParams().put(kv[0], "");
}
}
}
String path = url.getPath();
StringBuilder sb = new StringBuilder();
sb.append(hmacSHA256Dto.method).append("\n")
.append(hmacSHA256Dto.accept).append("\n\n")
.append(hmacSHA256Dto.contentType).append("\n")
.append(hmacSHA256Dto.date).append("\n")
.append("x-ca-key:").append(hmacSHA256Dto.xCaKey).append("\n")
.append("x-ca-nonce:").append(hmacSHA256Dto.xCaNonce).append("\n")
.append(path);
Map<String, Object> params = hmacSHA256Dto.params;
if (!CollectionUtils.isEmpty(params)) {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
for (int i = 0; i < keys.size(); i++) {
String value = keys.get(i);
if (StringUtils.hasText(value)) {
if (i == 0) {
sb.append("?").append(value).append("=").append(params.get(value));
} else {
sb.append("&").append(value).append("=").append(params.get(value));
}
}
}
}
if (sb.lastIndexOf("=") == sb.length() - 1) {
sb.deleteCharAt(sb.lastIndexOf("="));
}
return sb.toString();
}
/**
* 获取签名
*/
public static String getXCaSignature(HmacSHA256Dto hmacSHA256Dto) {
String message = getMessage(hmacSHA256Dto);
return hmacSHA256(hmacSHA256Dto.getAppSecret(), message);
}
}
```
# 单元测试
单元测试所在包com.csdn.signature.utils.HmacSHA256UtilTest
## Post请求单元测试
```java
@Test
public void postTest() {
// 请求信息
// curl 'https://bizapi.csdn.net/blog-console-api/v3/mdeditor/saveArticle' \
// -H 'accept: */*' \
// -H 'accept-language: zh-CN,zh;q=0.9,en;q=0.8' \
// -H 'content-type: application/json' \
// -H 'cookie: ' \
// -H 'origin: https://editor.csdn.net' \
// -H 'referer: https://editor.csdn.net/' \
// -H 'sec-ch-ua: "Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"' \
// -H 'sec-ch-ua-mobile: ?0' \
// -H 'sec-ch-ua-platform: "Windows"' \
// -H 'sec-fetch-dest: empty' \
// -H 'sec-fetch-mode: cors' \
// -H 'sec-fetch-site: same-site' \
// -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' \
// -H 'x-ca-key: 203803574' \
// -H 'x-ca-nonce: 04db9c7c-b281-4117-944a-98c86d9f27a2' \
// -H 'x-ca-signature: qWbIIf/nSO/fNFuHO3cIl0kG+y2uZI2M+8xiLapq8B4=' \
// -H 'x-ca-signature-headers: x-ca-key,x-ca-nonce' \
HmacSHA256Dto hmacSHA256Dto = new HmacSHA256Dto();
hmacSHA256Dto.setMethod("POST").setAccept("*/*")
.setContentType("application/json")
.setXCaNonce("04db9c7c-b281-4117-944a-98c86d9f27a2")
.setUrl("https://bizapi.csdn.net/blog-console-api/v3/mdeditor/saveArticle");
String xCaSignature = HmacSHA256Util.getXCaSignature(hmacSHA256Dto);
System.out.println("x-ca-signature:" + xCaSignature);
}
```
运行结果
```java
x-ca-signature:qWbIIf/nSO/fNFuHO3cIl0kG+y2uZI2M+8xiLapq8B4=
```
可以看到只要随机串x-ca-nonce一样,我们生成的和页面的签名是一样的,此处也可以得出一个结论,相同请求(如果get请求参数也要相同),x-ca-nonce一样生成的x-ca-signature就是一样的,post请求的参数不参与x-ca-sig