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

前言

  在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题。其中我认为最终要的还是数据是否被篡改。在此分享一下我的关于接口签名的实践方案。如果这种方案不是很好理解,请参考另一篇更简单暴力的方案 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. U盘权限不足,只读文件系统

    https://blog.csdn.net/baocheng_521/article/details/77161791 用第一种方式成功

  2. EasyUI Layout 添加、删除、折叠、展开布局

    <!DOCTYPE html> <html> <head> <title>吹泡泡的魚-主页</title> <link rel=&qu ...

  3. follow

    public function follow(Request $request, FeedModel $model, FeedRepository $repository) { if (is_null ...

  4. java内部类和异常类的概念

    1.内部类的外嵌类的成员变量在内部类中任然有效,内部类中的方法也可以调用外嵌类中的 方法,内部类中不可以声明类的变量和方法,外嵌的类体可以用内部类声明对象,作为外嵌类的成员.内部类仅供他的外嵌类使用. ...

  5. ajax---获取XMLHttpReuquest 对象

    ajax的异步和同步(Asynchronus Javascript and Xml) 同步:一个时间段只能干一件事:即按部就班,一件事一件事的做. 异步:相同的时间段做多件事,同时进行.依靠 XMLH ...

  6. Jenkins删除或替换All view

    一.Jenkins删除All view “系统管理”→“系统设置”页面,更改“Default view”的下拉选项.(前提你已经新建了新的view) 二.My Views删除All view “用户” ...

  7. Android取得系统时间

    Time t = new Time();//实例化Time类 t.setToNow();//取得当前的系统时间 int month = t.month;//获取月 int year = t.year; ...

  8. RHEL7恢复root密码

    RHEL7恢复root密码 首先关闭SELINUX [root@panda ~]# getenforce Disabled 然后重启,按↑↓键,进入如下界面,选择第一项,按下e键进行编辑 在此界面找到 ...

  9. 集群部署时的分布式session如何实现

    tomcat + redis 这个其实还挺方便的,就是使用session的代码跟以前一样,还是基于tomcat原生的session支持即可,然后就是用一个叫做Tomcat RedisSessionMa ...

  10. Exception in thread "main" java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Ljava/lang/String;I)Z

    1.window操作系统的eclipse运行wordcount程序出现如下所示的错误: Exception in thread "main" java.lang.Unsatisfi ...