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

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

前言

  在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题。其中我认为最终要的还是数据是否被篡改。在此分享一下我的关于接口签名的实践方案。如果这种方案不是很好理解,请参考另一篇更简单暴力的方案 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)@SignatureFieldprivate String appid;/*** 线下分配的值* 客户端和服务端各自保存,与appId对应*/@SignatureFieldprivate String appsecret;/*** 时间戳,单位: ms*/@NotBlank(message = "Header中缺少" + HEADER_TIMESTAMP)@SignatureFieldprivate String timestamp;/*** 流水号【防止重复提交】; (备注:针对查询接口,流水号只用于日志落地,便于后期日志核查; 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求)*/@NotBlank(message = "Header中缺少" + HEADER_NONCE)@SignatureFieldprivate 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;}@Overridepublic 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 {//NOSONARMap<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对应的appsecretString 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);}}//设置appsecretsignatureHeaders.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, 便可查看。

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

转载于:https://www.cnblogs.com/hujunzheng/p/9725168.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/531220.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git rebase命令实战

一、前言 一句话&#xff0c;git rebase 可以帮助项目中的提交历史干净整洁&#xff01;&#xff01;&#xff01; 二、避免合并出现分叉现象 git merge操作 1、新建一个 develop 分支 2、在develop分支上新建两个文件 3、然后分别执行 add、commit、push 4、接着切换到master分…

windows系统nexus3安装和配置

一、前言 为什么要在本地开发机器上安装nexus&#xff1f;首先声明公司内部是有自己的nexus仓库&#xff0c;但是对上传jar包做了限制&#xff0c;不能畅快的上传自己测试包依赖。于是就自己在本地搭建了一个nexus私服&#xff0c;即可以使用公司nexus私服仓库中的依赖&#xf…

Springmvc借助SimpleUrlHandlerMapping实现接口开关功能

一、接口开关功能 1、可配置化&#xff0c;依赖配置中心 2、接口访问权限可控 3、springmvc不会扫描到&#xff0c;即不会直接的将接口暴露出去 二、接口开关使用场景 和业务没什么关系&#xff0c;主要方便查询系统中的一些状态信息。比如系统的配置信息&#xff0c;中间件的状…

log4j平稳升级到log4j2

一、前言 公司中的项目虽然已经用了很多的新技术了&#xff0c;但是日志的底层框架还是log4j&#xff0c;个人还是不喜欢用这个的。最近项目再生产环境上由于log4j引起了一场血案&#xff0c;于是决定升级到log4j2。 二、现象 虽然生产环境有多个结点分散高并发带来的压力&…

Springboot集成ES启动报错

报错内容 None of the configured nodes are available elasticsearch.yml配置 cluster.name: ftest node.name: node-72 node.master: true node.data: true network.host: 112.122.245.212 http.port: 39200 transport.tcp.port: 39300 discovery.zen.ping.unicast.hosts: [&…

kafka-manager配置和使用

kafka-manager配置 最主要配置就是用于kafka管理器状态的zookeeper主机。这可以在conf目录中的application.conf文件中找到。 kafka-manager.zkhosts"my.zookeeper.host.com:2181" 当然也可以声明为zookeeper集群。 kafka-manager.zkhosts"my.zookeeper.host.co…

kafka告警简单方案

一、前言 为什么要设计kafka告警方案&#xff1f;现成的监控项目百度一下一大堆&#xff0c;KafkaOffsetMonitor、KafkaManager、 Burrow等&#xff0c;具体参考&#xff1a;kafka的消息挤压监控。由于本小组的项目使用的kafka集群并没有被公司的kafka-manager管理&#xff0c;…

RedisCacheManager设置Value序列化器技巧

CacheManager基本配置 请参考博文&#xff1a;springboot2.0 redis EnableCaching的配置和使用 RedisCacheManager构造函数 /*** Construct a {link RedisCacheManager}.* * param redisOperations*/ SuppressWarnings("rawtypes") public RedisCacheManager(RedisOp…

HashMap 源码阅读

前言 之前读过一些类的源码&#xff0c;近来发现都忘了&#xff0c;再读一遍整理记录一下。这次读的是 JDK 11 的代码&#xff0c;贴上来的源码会去掉大部分的注释, 也会加上一些自己的理解。 Map 接口 这里提一下 Map 接口与1.8相比 Map接口又新增了几个方法&#xff1a;   …

SpringMvc接口中转设计(策略+模板方法)

一、前言 最近带着两个兄弟做支付宝小程序后端相关的开发&#xff0c;小程序首页涉及到很多查询的服务。小程序后端服务在我司属于互联网域&#xff0c;相关的查询服务已经在核心域存在了&#xff0c;查询这块所要做的工作就是做接口中转。参考了微信小程序的代码&#xff0c;发…

SpringSecurity整合JWT

一、前言 最近负责支付宝小程序后端项目设计&#xff0c;这里主要分享一下用户会话、接口鉴权的设计。参考过微信小程序后端的设计&#xff0c;会话需要依靠redis。相关的开发人员和我说依靠Redis并不是很靠谱&#xff0c;redis在业务高峰期不稳定&#xff0c;容易出现问题&…

Springboot定时任务原理及如何动态创建定时任务

一、前言 上周工作遇到了一个需求&#xff0c;同步多个省份销号数据&#xff0c;解绑微信粉丝。分省定时将销号数据放到SFTP服务器上&#xff0c;我需要开发定时任务去解析文件。因为是多省份&#xff0c;服务器、文件名规则、数据规则都不一定&#xff0c;所以要做成可配置是有…

转载:ThreadPoolExecutor 源码阅读

前言 之前研究了一下如何使用ScheduledThreadPoolExecutor动态创建定时任务(Springboot定时任务原理及如何动态创建定时任务)&#xff0c;简单了解了ScheduledThreadPoolExecutor相关源码。今天看了同学写的ThreadPoolExecutor 的源码解读&#xff0c;甚是NB&#xff0c;必须转…

使用pdfBox实现pdf转图片,解决中文方块乱码等问题

一、引入依赖 <dependency><groupId>org.apache.pdfbox</groupId><artifactId>fontbox</artifactId><version>2.0.13</version> </dependency> <dependency><groupId>org.apache.pdfbox</groupId><artif…

Spring异步调用原理及SpringAop拦截器链原理

一、Spring异步调用底层原理 开启异步调用只需一个注解EnableAsync Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Import(AsyncConfigurationSelector.class) public interface EnableAsync {/*** Indicate the async annotation type to be detec…

Spring MVC源码——Root WebApplicationContext

Spring MVC源码——Root WebApplicationContext 打算开始读一些框架的源码,先拿 Spring MVC 练练手,欢迎点击这里访问我的源码注释, SpringMVC官方文档一开始就给出了这样的两段示例: WebApplicationInitializer示例: public class MyWebApplicationInitializer implements Web…

Spring MVC源码——Servlet WebApplicationContext

上一篇笔记(Spring MVC源码——Root WebApplicationContext)中记录了下 Root WebApplicationContext 的初始化代码.这一篇来看 Servlet WebApplicationContext 的初始化代码 DispatcherServlet 是另一个需要在 web.xml 中配置的类, Servlet WebApplicationContext 就由它来创建…

Springboot源码——应用程序上下文分析

前两篇(Spring MVC源码——Root WebApplicationContext 和 Spring MVC源码——Servlet WebApplicationContext)讲述了springmvc项目创建上下文的过程&#xff0c;这一篇带大家了解一下springboot项目创建上下文的过程。 SpringApplication引导类 SpringApplication类用于启动或…

基于zookeeper实现分布式配置中心(一)

最近在学习zookeeper&#xff0c;发现zk真的是一个优秀的中间件。在分布式环境下&#xff0c;可以高效解决数据管理问题。在学习的过程中&#xff0c;要深入zk的工作原理&#xff0c;并根据其特性做一些简单的分布式环境下数据管理工具。本文首先对zk的工作原理和相关概念做一下…

基于zookeeper实现分布式配置中心(二)

上一篇&#xff08;基于zookeeper实现分布式配置中心&#xff08;一&#xff09;&#xff09;讲述了zookeeper相关概念和工作原理。接下来根据zookeeper的特性&#xff0c;简单实现一个分布式配置中心。 配置中心的优势 1、各环境配置集中管理。 2、配置更改&#xff0c;实时推…