1|0前言  

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

2|0签名流程

3|0签名规则

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

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

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

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

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

4|0签名的生成

  signature 字段生成规则如下。

4|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=ab=e=ec=c)

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

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

  上述拼接的值记作 Y。

4|2  请求头部分

  X=”appid=xxxnonce=xxxtimestamp=xxx”

4|3  生成签名

  最终拼接值=XY

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

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

5|0 签名算法实现

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

 

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;

}

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

 

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 "";

}

5|3  核心算法

 

/** * 生成所有注有 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;

}

6|0签名的校验

6|1  header中的参数如下

  

6|2  签名实体

 

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();

}

}

6|3  根据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)、是否允许重复请求

6|4  生成header信息参数拼接

 

String headersToSplice = SignatureUtils.toSplice(signatureHeaders);

6|5  生成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;

}

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

 

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

7|0依赖第三方工具包

 

<dependency>

<groupId>org.apache.commons</groupId>

<artifactId>commons-lang3</artifactId>

</dependency>

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-beans</artifactId>

</dependency>

8|0使用示例

8|1  生成签名

 

//初始化请求头信息

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);

8|2  输出结果

 

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

JAVA接口签名(Signature)实现方案相关推荐

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

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

  2. 开放接口签名(Signature)实现

    开放接口签名(Signature)实现方案 既然是对外开放,那么调用者一定没有我们系统的Token,就需要对调用者进行签名验证,签名验证采用主流的验证方式,采用Signature 的方式. 字段 类型 ...

  3. 微信开发之使用java获取签名signature

    一.前言 微信接口调用验证最终需要用到的三个参数noncestr.timestamp.signature: 接下来将会给出获取这三个参数的详细代码 本文使用到的技术HttpClient.Json字符串 ...

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

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

  5. JAVA接口签名sign生成工具类

    签名规则 1.线下分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret 2.加入timestamp(时间戳),10分钟内数据有效 3.加入流水号nonce(防止 ...

  6. JAVA 接口签名sign生成 工具类

    import org.springframework.util.StringUtils;import java.util.Map; import java.util.Random; import ja ...

  7. java 接口 签名机制_java – 当接口A在其方法签名中定义接口B时

    -如何限制A的实现在方法签名中使用B的某个实现? 用例 这是一个Unit接口和两个实现它的枚举: public interface Unit { ... } public enum ForceUnit ...

  8. java接口 密钥签名作用_api接口签名加密请求(二)

    在"api接口签名加密请求,从springmvc4项目搭建开始"篇文章,讲述了使用java springmvc搭建api接口请求例子.为了方便php能调用,接着写了php的demo ...

  9. java 手写签名,signature java html5+ 手写签名 源码 Develop 238万源代码下载- www.pudn.com...

    文件名称: signature下载 收藏√  [ 5  4  3  2  1 ] 开发工具: Java 文件大小: 491 KB 上传时间: 2013-08-03 下载次数: 17 提 供 者: 孙晨 ...

  10. 接口签名中的三位小伙伴signature,nonce,timestamp

    在请求一些开放平台的接口时,我们一般会在请求头中塞入一些签名相关的信息以证明身份.以微信API-V3的为例,请求头中包含的认证信息主要包含: signature(签名值) nonce(随机串) tim ...

最新文章

  1. 盘点 IDEA2020.3 那些炫酷的插件,超级好用的那种
  2. 今日头条字节跳动抖音小程序上线前后的搜索优化配置
  3. CSP认证201612-4 压缩编码[C++题解]:区间dp、huffman树、石子合并
  4. bootstrap datetimepicker的一些小总结
  5. 单链表删除所有值为x的元素_线性表之单链表
  6. python数据库模糊查询_Python操作mongodb数据库进行模糊查询操作示例
  7. 负载均衡—几种常见算法
  8. 黑马程序员-JAVA高级视频_IO输入与输出-20天-9(切割合并文件)
  9. python模板语言_你应该(或许)没使用过的 3 种 Python 模板语言 | Linux 中国
  10. 堆排序java语言描述
  11. odi12配置mysql_Oracle数据库之Oracle ODI 12c之多表联合查询以及定时任务设置
  12. 战略变了 IBM服务器由产品到解决方案的转变
  13. 2021年电工(初级)考试资料及电工(初级)考试总结
  14. 《中国人史纲》读书笔记:第八章、第九章 公元前第六、五世纪
  15. Spring Boot 接入钉钉扫码登录
  16. Java源码阅读之String(4)
  17. K8s安全管理:认证、授权、准入控制
  18. Yolov7实战,实现网页端的实时目标检测
  19. 人为回环SLAM-Human-in-the-Loop SLAM
  20. 交换机二三层转发原理简单总结

热门文章

  1. 出生年分数 15作者 陈越单位 浙江大学
  2. Web 通信 之 长连接、长轮询
  3. uniapp实现点击回到顶部
  4. 英语学习回炉之绕口令
  5. HTML CSS 网页设计作业「动漫小站」
  6. RuntimeError: mat1 dim 1 must match mat2 dim 0
  7. 华硕服务器 bios 内存 1333 显示 800,华硕服务器bios设置
  8. Java网络编程并实现一对一聊天室功能
  9. Linux基础操作命令
  10. Nginx配置带www域名转向无www域名