1:使用开源的jar包
API-Signed: 一个轻松实现API签名校验的库。 (gitee.com)
本地下载源码:E:\JavaCode\java-API签名校验
2:该jar 包操作说明
本仓库包含以下内容:
- 签名校验的源码
- 基于Spring boot的web示例
- 由于要开放接口供第三方调用, 采用签名校验的方式以保证安全, 于是有了这个项目。
该项目使用面向切面的方式对签名进行校验, 接口本身只需要关心业务逻辑的处理。
同时防止了重放攻击, 也支持对加密规则, 参数字段的自定义。
3:操作流程 集成jar包
1. 添加 maven 依赖
<dependency>
<groupId>cn.oever</groupId>
<artifactId>api-signed</artifactId>
<version>0.0.1</version>
</dependency>
2. 添加配置信息
YAML格式:
oever:
signature:
time-diff-max: 300
algorithm: HmacSHA1
redis:
host: 127.0.0.1
port: 6379
如果使用properties格式配置文件, 那它应该看起来是这样:
oever.signature.time-diff-max=300
oever.signature.algorithm=HmacSHA1
redis.host=127.0.0.1
redis.port=6379
参数 | 说明 |
---|---|
time-diff-max | 调用方与服务器时间戳允许的最大差值 |
algorithm | MAC算法的标准名称, 可以参阅 Java Cryptography Architecture Reference Guide 中的附录A |
另外, 本项目使用Redis作为缓存的实现。
3. 添加扫描注解
@SpringBootApplication
@SignedScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
将@SignedScan
注解添加到启动类, 以引入相关实现。
到此为止, 已经完成了全部的配置, 可以尝试实现一个进行签名校验的API了。
4. 签名校验接口
@RestController
@RequestMapping("api")
@SignedMapping
public class TestController {
@RequestMapping("test")
public String test(@RequestBody SignedParam signedParam) {
// the request data is signedParam.getData() in JSON
// then do something in service
return "SUCCESS";
}
}
如上, 只需要在接口上使用@SignedMapping
注解, 且请求参数为SignedParam
类, 即可实现对签名的校验。@SignedMapping
注解既可以作用于类, 也可以作用于单独的方法。
当然, 对于部分需求, 或许默认的SignedParam
类中的参数并不能满足, 甚至有可能需要自己实现一套加密规则, 可以参见自定义。
Usage
名称 | 取值 |
---|---|
appId | APP_ID_TEST |
appSecret | APP_SECRET_TEST |
1. 请求说明
1.1 请求方式
POST
1.2 请求参数
名称 | 类型 | 说明 |
---|---|---|
data | String | 取值与具体请求接口相关, 格式为JSON体的字符串形式 |
appId | String | 分配给调用方的ID |
timestamp | Long | 10位时间戳, 若调用方与服务端时间相差过大, 将拒绝本次请求 |
nonce | Integer | 随机整数, 与timestamp联合使用以防止重放攻击 |
signature | String | 用于验证此次请求合法性的签名 |
2. 签名方法
2.1. 参数排序
将请求参数依据参数名称(首字母小写)的ASCII序进行升序排列, 参与排序的参数包括除signature以外的所有请求参数。
业务请求参数的数据为JSON对象时, 需要将其转化为字符串再参与排序和签名计算。
例如, 请求参数为:
{
"userId": "test"
}
则, 完整的请求参数列表: 名称|取值|说明 ---|---|--- data|"{\"userId\":\"test\"}"
|请求参数 appId|APP_ID_TEST|测试ID nonce|-2028703096|随机整数 timestamp|1597415679|发起请求时的时间戳 signature|待计算|签名值
则排序后的结果为:
{
"appId": "APP_ID_TEST",
"data": "{\"userId\":\"test\"}",
"nonce": -2028703096,
"timestamp": 1597415679
}
2.2. 参数拼接
将排序后的请求参数依照参数名=参数值
的形式格式化, 然后将各个参数依序用&
符号拼接在一起, 得到待签名字符串plainText
appId=APP_ID_TEST&data={"userId":"test"}&nonce=-2028703096×tamp=1597415679
2.3. 生成签名
以HMAC-SHA1算法为例对plainText进行加密, 再使用Base64对加密后的字节流进行编码, 即得到了最终签名signature
signature=base64_encode(hash_hmac('sha1', $plainText, $appSecret, true));
2.4. cURL示例
curl -v -X POST "127.0.0.1:8080/example/base" -H "Accept: application/json" -H "Content-Type: application/json; charset=utf-8" -d '{"data":"{\"userId\":\"test\"}","signature":"tFACzWpdduGputwIzxffmkJwij8=","appId":"APP_ID_TEST","nonce":-2028703096,"timestamp":1597415679}'
Custom
1. 自定义请求参数
以我们默认实现的请求参数类为例:
@SignedEntity
public class SignedParam {
@SignedAppId
private String appId;
private String data;
@SignedTimestamp
private long timestamp;
@SignedNonce
private int nonce;
@Signature
private String signature;
// getter and setter...
}
注解 | 说明 |
---|---|
@SignedEntity | 标注了该类为一个需要进行签名计算的请求参数类 |
@SignedAppId | 标注了该字段为调用方的AppId, 我们将使用此字段的值获取对应的AppSecret进行签名的计算 |
@SignedTimestamp | 标注了该字段为请求的时间戳, 以检查调用方与服务器的时间差 |
@SignedNonce | 标注了该字段为一个随机数, 和时间戳联合使用以防止重放攻击 |
@SignedIgnore | 标注了该字段不参与签名的计算 |
@Signature | 标注了该字段为调用方计算出来的签名, 该字段并不会参与签名的计算, 而是其他字段计算得出签名后与该字段进行比较 |
其中, 未使用任何注解的data字段为JSON格式的字符串, 用于业务逻辑处理, 该字段仍参与签名计算。
2. 自定义校验规则
2.1. 实现
继承BaseSignedService
并重写需要修改的方法
方法 | 参数列表 | 参数说明 | 返回值 | 方法说明 |
---|---|---|---|---|
getAppSecret | String appId | appId | String appSecret | 通过appId获取对应的appSecret, 默认实现使用Redis, 如果使用其他缓存, 可以重写此方法 |
isTimeDiffLarge | long timestamp | 时间戳 | void | 判断调用方与服务器的时间差值是否超过设定的最大值, 若超出, 则抛出异常 |
isReplayAttack | String appId, long timestamp, int nonce, String signature | appId, 时间戳, 随机数, 签名 | void | 通过appId + 时间戳 + 随机数 作为键, 签名 作为值, 每次请求进行判重并缓存, 如存在, 则抛出异常 |
getSignature | String appId, Map map | appId, 参与计算签名的参数 | String signature | 通过appId和需要计算签名的参数列表, 计算签名, 如果需要自定义加密规则, 可以重写该方法 |
entry | Object obj | 请求参数的实体类 | void | 入口方法, 该方法依次调用了参数的非空判断, 时间差, 是否重放攻击, 与签名的计算。如果需要增加或修改额外的验证步骤, 可以重写此方法。如果只需要以上步骤中的一步或几步, 更好的做法是重写不需要的方法并留空 |
2.2. 使用
然后, 在使用@SignedMapping
注解的地方, 添加自定义的类作为参数, 如:
@RequestMapping("test")
-@SignedMapping
+@SignedMapping(CustomizeSignedService.class)
public String test(@RequestBody SignedParam signedParam) {
return "SUCCESS";
}
此时, 签名校验将被自定义类中重写的方法所接管。
在example中, 可以看到默认实现, 自定义参数, 自定义实现三种例子。
并且每一种例子都提供了签名生成与签名校验两个接口, 可以自行尝试。