安全优雅的RESTful API签名实现方案
1、接口签名的必要性
在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题。其中我认为最终要的还是数据是否被篡改。在此分享一下我的关于接口签名的实践方案。
2、项目中签名方案痛点
- 每个接口有各自的签名方案,不统一,维护成本较高。
- 没有对消息实体进行签名,无法避免数据被篡改。
- 无法避免数据重复提交。
3、签名及验证流程
4、签名规则
- 线下分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret。
- 加入timestamp(时间戳),10分钟内数据有效。
- 加入流水号nonce(防止重复提交),至少为10位。针对查询接口,流水号只用于日志落地,便于后期日志核查。 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
- 加入signature,所有数据的签名信息。
其中appid、timestamp、nonce、signature这四个字段放入请求头中。
5、签名生成
5.1、数据部分
- Path:按照path中的顺序将所有value进行拼接
- Query:按照key字典序排序,将所有key=value进行拼接
- Form:按照key字典序排序,将所有key=value进行拼接
- Body:
Json: 按照key字典序排序,将所有key=value进行拼接(例如{"a":"a","c":"c","b":{"e":"e"}} => a=a^_^b=e=e^_^c=c)
String: 整个字符串作为一个拼接
如果存在多种数据形式,则按照path、query、form、body的顺序进行再拼接,得到所有数据的拼接值。
上述拼接的值记作 Y。
5.2、请求头部分
X=“appid=xxxnonce=xxxtimestamp=xxx”
5.3、生成签名
最终拼接值=XY
最后将最终拼接值按照如下方法进行加密得到签名(signature)。
signature=org.apache.commons.codec.digest.HmacUtils.hmacSha256Hex(app secret, 拼接的值);
6、签名算法实现
6.1、指定哪些接口或者哪些实体需要签名
@Target({
TYPE, METHOD})
@Retention(RUNTIME)
@Documented
public @interface Signature {
String ORDER_SORT = "ORDER_SORT";//按照order值排序
String ALPHA_SORT = "ALPHA_SORT";//字典序排序
boolean resubmit() default true;//允许重复请求
String sort() default Signature.ALPHA_SORT;
}
6.2、指定哪些字段需要签名
@Target({
FIELD})
@Retention(RUNTIME)
@Documented
public @interface SignatureField {
//签名顺序
int order() default 0;
//字段name自定义值
String customName() default "";
//字段value自定义值
String customValue() default "";
}
6.3、签名核心算法(SignatureUtils)
public static String toSplice(Object object) {
if (Objects.isNull(object)) {
return StringUtils.EMPTY;
}
if (isAnnotated(object.getClass(), Signature.class)) {
Signature sg = findAnnotation(object.getClass(), Signature.class);
switch (sg.sort()) {
case Signature.ALPHA_SORT:
return alphaSignature(object);
case Signature.ORDER_SORT:
return orderSignature(object);
default:
return alphaSignature(object);
}
}
return toString(object);
}
private static String alphaSignature(Object object) {
StringBuilder result = new StringBuilder();
Map<String, String> map = new TreeMap<>();
for (Field field : getAllFields(object.getClass())) {
if (field.isAnnotationPresent(SignatureField.class)) {
field.setAccessible(true);
try {
if (isAnnotated(field.getType(), Signature.class)) {
if (!Objects.isNull(field.get(object))) {
map.put(field.getName(), toSplice(field.get(object)));
}
} else {
SignatureField sgf = field.getAnnotation(SignatureField.class);
if (StringUtils.isNotEmpty(sgf.customValue()) || !Objects.isNull(field.get(object))) {
map.put(StringUtils.isNotBlank(sgf.customName()) ? sgf.customName() : field.