预祝大家国庆节快乐,赶快迎接美丽而快乐的假期吧!!!

前言

  在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题。其中我认为最终要的还是数据是否被篡改。在此分享一下我的关于接口签名的实践方案。如果这种方案不是很好理解,请参考另一篇更简单暴力的方案 java接口签名(Signature)实现方案续 

签名流程

签名规则

  1、线下分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret

  2、加入timestamp(时间戳),10分钟内数据有效

  3、加入流水号nonce(防止重复提交),至少为10位。针对查询接口,流水号只用于日志落地,便于后期日志核查。 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。

  4、加入signature,所有数据的签名信息。

  以上红色字段放在请求头中。

签名的生成

  signature 字段生成规则如下。

   数据部分

  Path:按照path中的顺序将所有value进行拼接

  Query:按照key字典序排序,将所有key=value进行拼接

  Form:按照key字典序排序,将所有key=value进行拼接

  Body

    Json: 按照key字典序排序,将所有key=value进行拼接(例如{"a":"a","c":"c","b":{"e":"e"}} => a=ab=e=ec=c)

    String: 整个字符串作为一个拼接

  如果存在多种数据形式,则按照path、query、form、body的顺序进行再拼接,得到所有数据的拼接值。

  上述拼接的值记作 Y。

  请求头部分

  X=”appid=xxxnonce=xxxtimestamp=xxx”

  生成签名

  最终拼接值=XY

  最后将最终拼接值按照如下方法进行加密得到签名。

  signature=org.apache.commons.codec.digest.HmacUtils.hmacSha256Hex(app secret, 拼接的值);

签名算法实现

  指定哪些接口或者哪些实体需要进行签名

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target; import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME; @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;
}

  指定哪些字段需要进行签名

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({FIELD})
@Retention(RUNTIME)
@Documented
public @interface SignatureField {
//签名顺序
int order() default 0; //字段name自定义值
String customName() default ""; //字段value自定义值
String customValue() default "";
}

  核心算法

/**
* 生成所有注有 SignatureField属性 key=value的 拼接
*/
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.getName()
, StringUtils.isNotEmpty(sgf.customValue()) ? sgf.customValue() : toString(field.get(object)));
}
}
} catch (Exception e) {
LOGGER.error("签名拼接(alphaSignature)异常", e);
}
}
} for (Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator(); iterator.hasNext(); ) {
Map.Entry<String, String> entry = iterator.next();
result.append(entry.getKey()).append("=").append(entry.getValue());
if (iterator.hasNext()) {
result.append(DELIMETER);
}
}
return result.toString();
} /**
* 针对array, collection, simple property, map做处理
*/
private static String toString(Object object) {
Class<?> type = object.getClass();
if (BeanUtils.isSimpleProperty(type)) {
return object.toString();
}
if (type.isArray()) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < Array.getLength(object); ++i) {
sb.append(toSplice(Array.get(object, i)));
}
return sb.toString();
}
if (ClassUtils.isAssignable(Collection.class, type)) {
StringBuilder sb = new StringBuilder();
for (Iterator<?> iterator = ((Collection<?>) object).iterator(); iterator.hasNext(); ) {
sb.append(toSplice(iterator.next()));
if (iterator.hasNext()) {
sb.append(DELIMETER);
}
}
return sb.toString();
}
if (ClassUtils.isAssignable(Map.class, type)) {
StringBuilder sb = new StringBuilder();
for (Iterator<? extends Map.Entry<String, ?>> iterator = ((Map<String, ?>) object).entrySet().iterator(); iterator.hasNext(); ) {
Map.Entry<String, ?> entry = iterator.next();
if (Objects.isNull(entry.getValue())) {
continue;
}
sb.append(entry.getKey()).append("=").append(toSplice(entry.getValue()));
if (iterator.hasNext()) {
sb.append(DELIMETER);
}
}
return sb.toString();
}
return NOT_FOUND;
}

签名的校验

  header中的参数如下

  

  签名实体

import com.google.common.base.MoreObjects;
import com.google.common.collect.Sets;
import org.hibernate.validator.constraints.NotBlank; import java.util.Set; @ConfigurationProperties(prefix = "wmhopenapi.validate", exceptionIfInvalid = false)
@Signature
public class SignatureHeaders {
public static final String SIGNATURE_HEADERS_PREFIX = "wmhopenapi-validate"; public static final Set<String> HEADER_NAME_SET = Sets.newHashSet(); private static final String HEADER_APPID = SIGNATURE_HEADERS_PREFIX + "-appid";
private static final String HEADER_TIMESTAMP = SIGNATURE_HEADERS_PREFIX + "-timestamp";
private static final String HEADER_NONCE = SIGNATURE_HEADERS_PREFIX + "-nonce";
private static final String HEADER_SIGNATURE = SIGNATURE_HEADERS_PREFIX + "-signature"; static {
HEADER_NAME_SET.add(HEADER_APPID);
HEADER_NAME_SET.add(HEADER_TIMESTAMP);
HEADER_NAME_SET.add(HEADER_NONCE);
HEADER_NAME_SET.add(HEADER_SIGNATURE);
} /**
* 线下分配的值
* 客户端和服务端各自保存appId对应的appSecret
*/
@NotBlank(message = "Header中缺少" + HEADER_APPID)
@SignatureField
private String appid;
/**
* 线下分配的值
* 客户端和服务端各自保存,与appId对应
*/
@SignatureField
private String appsecret;
/**
* 时间戳,单位: ms
*/
@NotBlank(message = "Header中缺少" + HEADER_TIMESTAMP)
@SignatureField
private String timestamp;
/**
* 流水号【防止重复提交】; (备注:针对查询接口,流水号只用于日志落地,便于后期日志核查; 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求)
*/
@NotBlank(message = "Header中缺少" + HEADER_NONCE)
@SignatureField
private String nonce;
/**
* 签名
*/
@NotBlank(message = "Header中缺少" + HEADER_SIGNATURE)
private String signature; public String getAppid() {
return appid;
} public void setAppid(String appid) {
this.appid = appid;
} public String getAppsecret() {
return appsecret;
} public void setAppsecret(String appsecret) {
this.appsecret = appsecret;
} public String getTimestamp() {
return timestamp;
} public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
} public String getNonce() {
return nonce;
} public void setNonce(String nonce) {
this.nonce = nonce;
} public String getSignature() {
return signature;
} public void setSignature(String signature) {
this.signature = signature;
} @Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("appid", appid)
.add("appsecret", appsecret)
.add("timestamp", timestamp)
.add("nonce", nonce)
.add("signature", signature)
.toString();
}
}

  根据request 中 header值生成SignatureHeaders实体

private SignatureHeaders generateSignatureHeaders(Signature signature, HttpServletRequest request) throws Exception {//NOSONAR
Map<String, Object> headerMap = Collections.list(request.getHeaderNames())
.stream()
.filter(headerName -> SignatureHeaders.HEADER_NAME_SET.contains(headerName))
.collect(Collectors.toMap(headerName -> headerName.replaceAll("-", "."), headerName -> request.getHeader(headerName)));
PropertySource propertySource = new MapPropertySource("signatureHeaders", headerMap);
SignatureHeaders signatureHeaders = RelaxedConfigurationBinder.with(SignatureHeaders.class)
.setPropertySources(propertySource)
.doBind();
Optional<String> result = ValidatorUtils.validateResultProcess(signatureHeaders);
if (result.isPresent()) {
throw new ServiceException("WMH5000", result.get());
}
//从配置中拿到appid对应的appsecret
String appSecret = limitConstants.getSignatureLimit().get(signatureHeaders.getAppid());
if (StringUtils.isBlank(appSecret)) {
LOGGER.error("未找到appId对应的appSecret, appId=" + signatureHeaders.getAppid());
throw new ServiceException("WMH5002");
} //其他合法性校验
Long now = System.currentTimeMillis();
Long requestTimestamp = Long.parseLong(signatureHeaders.getTimestamp());
if ((now - requestTimestamp) > EXPIRE_TIME) {
String errMsg = "请求时间超过规定范围时间10分钟, signature=" + signatureHeaders.getSignature();
LOGGER.error(errMsg);
throw new ServiceException("WMH5000", errMsg);
}
String nonce = signatureHeaders.getNonce();
if (nonce.length() < 10) {
String errMsg = "随机串nonce长度最少为10位, nonce=" + nonce;
LOGGER.error(errMsg);
throw new ServiceException("WMH5000", errMsg);
}
if (!signature.resubmit()) {
String existNonce = redisCacheService.getString(nonce);
if (StringUtils.isBlank(existNonce)) {
redisCacheService.setString(nonce, nonce);
redisCacheService.expire(nonce, (int) TimeUnit.MILLISECONDS.toSeconds(RESUBMIT_DURATION));
} else {
String errMsg = "不允许重复请求, nonce=" + nonce;
LOGGER.error(errMsg);
throw new ServiceException("WMH5000", errMsg);
}
}
   //设置appsecret
signatureHeaders.setAppsecret(appSecret);
return signatureHeaders;
}

  生成签名前需要几个步骤,如下。

    (1)、appid是否合法

    (2)、根据appid从配置中心中拿到appsecret

    (3)、请求是否已经过时,默认10分钟

    (4)、随机串是否合法

    (5)、是否允许重复请求

  生成header信息参数拼接

String headersToSplice = SignatureUtils.toSplice(signatureHeaders);

  生成header中的参数,mehtod中的参数的拼接

private List<String> generateAllSplice(Method method, Object[] args, String headersToSplice) {
List<String> pathVariables = Lists.newArrayList(), requestParams = Lists.newArrayList();
String beanParams = StringUtils.EMPTY;
for (int i = 0; i < method.getParameterCount(); ++i) {
MethodParameter mp = new MethodParameter(method, i);
boolean findSignature = false;
for (Annotation anno : mp.getParameterAnnotations()) {
if (anno instanceof PathVariable) {
if (!Objects.isNull(args[i])) {
pathVariables.add(args[i].toString());
}
findSignature = true;
} else if (anno instanceof RequestParam) {
RequestParam rp = (RequestParam) anno;
String name = mp.getParameterName();
if (StringUtils.isNotBlank(rp.name())) {
name = rp.name();
}
if (!Objects.isNull(args[i])) {
List<String> values = Lists.newArrayList();
if (args[i].getClass().isArray()) {
//数组
for (int j = 0; j < Array.getLength(args[i]); ++j) {
values.add(Array.get(args[i], j).toString());
}
} else if (ClassUtils.isAssignable(Collection.class, args[i].getClass())) {
//集合
for (Object o : (Collection<?>) args[i]) {
values.add(o.toString());
}
} else {
//单个值
values.add(args[i].toString());
}
values.sort(Comparator.naturalOrder());
requestParams.add(name + "=" + StringUtils.join(values));
}
findSignature = true;
} else if (anno instanceof RequestBody || anno instanceof ModelAttribute) {
beanParams = SignatureUtils.toSplice(args[i]);
findSignature = true;
} if (findSignature) {
break;
}
}
if (!findSignature) {
LOGGER.info(String.format("签名未识别的注解, method=%s, parameter=%s, annotations=%s", method.getName(), mp.getParameterName(), StringUtils.join(mp.getMethodAnnotations())));
}
}
List<String> toSplices = Lists.newArrayList();
toSplices.add(headersToSplice);
toSplices.addAll(pathVariables);
requestParams.sort(Comparator.naturalOrder());
toSplices.addAll(requestParams);
toSplices.add(beanParams);
return toSplices;
}

  对最终的拼接结果重新生成签名信息

SignatureUtils.signature(allSplice.toArray(new String[]{}), signatureHeaders.getAppsecret());

依赖第三方工具包

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>

使用示例

  生成签名

//初始化请求头信息
SignatureHeaders signatureHeaders = new SignatureHeaders();
signatureHeaders.setAppid("111");
signatureHeaders.setAppsecret("222");
signatureHeaders.setNonce(SignatureUtils.generateNonce());
signatureHeaders.setTimestamp(String.valueOf(System.currentTimeMillis()));
List<String> pathParams = new ArrayList<>();
//初始化path中的数据
pathParams.add(SignatureUtils.encode("18237172801", signatureHeaders.getAppsecret()));
//调用签名工具生成签名
signatureHeaders.setSignature(SignatureUtils.signature(signatureHeaders, pathParams, null, null));
System.out.println("签名数据: " + signatureHeaders);
System.out.println("请求数据: " + pathParams);

  输出结果

拼接结果: appid=111^_^appsecret=222^_^nonce=c9e778ba668c8f6fedf35634eb493af6304d54392d990262d9e7c1960b475b67^_^timestamp=1538207443910^_^w8rAwcXDxcDKwsM=^_^
签名数据: SignatureHeaders{appid=111, appsecret=222, timestamp=1538207443910, nonce=c9e778ba668c8f6fedf35634eb493af6304d54392d990262d9e7c1960b475b67, signature=0a7d0b5e802eb5e52ac0cfcd6311b0faba6e2503a9a8d1e2364b38617877574d}
请求数据: [w8rAwcXDxcDKwsM=]

需要源码

  请关注订阅号,回复:signature, 便可查看。

 就先分享这么多了,更多分享请关注我们的技术公众号!!!

java接口签名(Signature)实现方案的更多相关文章

  1. java接口签名(Signature)实现方案续

    一.前言 由于之前写过的一片文章 (java接口签名(Signature)实现方案 )收获了很多好评,此次来说一下另一种简单粗暴的签名方案.相对于之前的签名方案,对body.paramenter.pa ...

  2. 转:微信开发之使用java获取签名signature(贴源码,附工程)

    微信开发之使用java获取签名signature(贴源码,附工程) 标签: 微信signature获取签名 2015-12-29 22:15 6954人阅读 评论(3) 收藏 举报  分类: 微信开发 ...

  3. 微信开发之使用java获取签名signature(贴源码,附工程)

    一.前言 微信接口调用验证最终需要用到的三个参数noncestr.timestamp.signature: 接下来将会给出获取这三个参数的详细代码 本文的环境eclipse + maven 本文使用到 ...

  4. 『居善地』接口测试 — 11、接口签名sign原理

    目录 1.什么是加密以及解密? 2.加密方式的分类 (1)对称加密 (2)非对称加密 (3)总结: 3.接口签名sign原理 (1)什么是接口签名? (2)为什么需要做接口签名 (3)接口签名的实践方 ...

  5. 基于HTTP在互联网传输敏感数据的消息摘要、签名与加密方案

    基于HTTP在互联网传输敏感数据的消息摘要.签名与加密方案 博客分类: 信息安全 Java 签名加密AESMD5HTTPS  一.关键词 HTTP,HTTPS,AES,SHA-1,MD5,消息摘要,数 ...

  6. php--php调java接口验签

    <?php namespace Fmall_cloud\Model; use Think\Model; class DealJavaModel extends Model { /** * @ti ...

  7. Java 接口基础详解

    目录 Java接口示例 实现一个接口 接口实例 实现多个接口 方法签名重叠 接口变量 接口方法 接口默认方法 接口与继承 继承与默认方法 接口与多态性 在Java中,接口是一个抽象类型,有点类似于类, ...

  8. java 接口详解

    定义接口 接口继承和实现继承的规则不同,一个类只有一个直接父类,但可以实现多个接口.Java 接口本身没有任何实现,只描述 public 行为,因此 Java 接口比 Java 抽象类更抽象化.Jav ...

  9. java接口深入

    1.抽象类. java常规类中,有些方法并不是固定的,而是在不同的情况下有不同的实现的,比如一个人,在公司要工作,在学校要学习,在食堂要吃饭,但是这个人还有相同的地方,比如不论在哪里都要呼吸,看,挺, ...

随机推荐

  1. vue-列表嵌套

  2. poj3107树的重心

    /*树的重心求法:两次dfs,第一次dfs处理出每个结点的size,以此求每个结点大儿子的size,第二次dfs将每个结点大儿子的size和余下结点数进行比较,所有结点里两个值之间差值最小的那个点就是 ...

  3. easyui 布局之window和panel一起使用时,拉动window宽高时panel不跟随一起变化

    项目开发中布局是每一个组件都由最外层的window和内部的至少一个panel组成,其他的细小组件再依次放到panel中. 问题:当拉动外部的window时我们希望内部的panel的宽高也跟着变化,但是 ...

  4. 充分认识Mysql

    使用开源产品是一种潮流.在使用之前,我们首先需要对Mysql 有一定的了解,特别是Mysql 的缺点.只有了解其缺点后,我们才知道,能不能真正的应用到我们的业务场景中去. 2.1 Mysql 数据库简 ...

  5. spring coud Feign常用配置

    Ribbon配置 在Feign中配置Ribbon非常简单,直接在application.properties中配置即可,如: # 设置连接超时时间 ribbon.ConnectTimeout=500 ...

  6. Red Language

    官网地址:http://www.red-lang.org/ 源代码地址:https://github.com/red/red   通过github上的Readme,可以完成Hello World的学习 ...

  7. sqoop无法导出parquet文件到mysql

    1.问题描述 在CDH集群中我们需要将Hive表的数据导入到RDBMS数据库中,使用Sqoop工具可以方便的将Hive表数据抽取到RDBMS数据库中,在使用Sqoop抽取Hive Parquet表时作 ...

  8. es6 2017

    http://es6.ruanyifeng.com/ 18.async  20.Decorator 属于ES2017, nodejs 未实现 21.Module 中的 import export  n ...

  9. AtCoder Regular Contest 101

    C题是个傻逼题, 一定是先向右,然后停了或者向左走到某一个点(左边同理)模拟就可以了 D题想了一会才想出来 和tjoi那道排序挺像的 二分答案变0/1来做 刚开始写的时候还把自己作为另外一类搞出来 这 ...

  10. ASP.NET machineKey的作用和使用方法

    ASP.NET machineKey的作用 如果你的Asp.Net程序执行时碰到这种错误:“验证视图状态 MAC 失败.如果此应用程序由网络场或群集承载,请确保 <machineKey> ...