开放接口签名(Signature)实现方案

既然是对外开放,那么调用者一定没有我们系统的Token,就需要对调用者进行签名验证,签名验证采用主流的验证方式,采用Signature 的方式。

字段

类型

必传

说明

appid

String

应用id

timestamp

String

时间戳

nonce

String

随机数、不少于10位

signature

String

签名

signature: 生成方式

将参数appId=wx123456789&nonce=155121212121&timestamp=1684565287668&key=35AB7ECF665EF5EF44CF8640EC136300 进行拼接 key指的是appid对应的appSecret

将上述参数进行MD5加密

二、流程

1、通过应用设置模块添加应用分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret。

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

3、 加入随机字符串,则认为接口为重复调用,返回错误信息(防止重复提交)。

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

三、实现

简单来说,调用者调用接口业务参数在body中传递,header中额外增加四个参数signature、appid、timestamp,随机字符串。

我们在后台取到四个参数,其后三个参数加上调用者分配的appSecret,使用字典排序并使用MD5加密后与第一个参数signature进行比对,一致既表示调用者有权限调用。

接口异常:

403

签名不一致

403

appId或appSecret不正确

422

参数timestamp不能为空

408

请求时间超过规定范围时间10分钟

422

随机串nonce不能为空

422

随机串nonce长度最少为10位

407

不允许重复请求


代码实现:

自定义注解:

/*** 签名算法实现=>指定哪些接口或者哪些实体需要进行签名*/
@Target({TYPE, METHOD})
@Retention(RUNTIME)
@Documented
public @interface Signature {//允许重复请求boolean resubmit() default true;
}

签名工具类:

/*** 开放接口签名工具类* @author ShawnWang* @datetime 2023-05-19* @desc 接口校验工具类*  生成有序map,签名,验签*  通过appId、timestamp、appSecret做签名* @menu*/
@Slf4j
public class SignUtil {/*** 生成签名sign* 加密前:appid=wx123456789&nonce=155121212121&timestamp=1684565287668&key=35AB7ECF665EF5EF44CF8640EC136300* 加密后:4CD98E261F46AA75E8935695C864A26D*/public static String createSign(SortedMap<String, String> params, String key){StringBuilder sb = new StringBuilder();Set<Map.Entry<String, String>> es =  params.entrySet();Iterator<Map.Entry<String,String>> it =  es.iterator();//生成while (it.hasNext()){Map.Entry<String,String> entry = it.next();String k = entry.getKey();String v = entry.getValue();if(null != v && !"".equals(v) && !"signature".equals(k) && !"key".equals(k)){sb.append(k+"="+v+"&");}}sb.append("key=").append(key);System.err.println("生成密钥前 " + sb.toString());String sign = MD5(sb.toString()).toUpperCase();return sign;}/*** 校验签名*/public static Boolean isCorrectSign(SortedMap<String, String> params, String key){String sign = createSign(params,key);//这是前端带过来的String requestSign = params.get("signature").toUpperCase();log.info("通过用户发送数据获取新签名:{}", sign);return requestSign.equals(sign);}/*** md5常用工具类*/public static String MD5(String data){try {MessageDigest md5 = MessageDigest.getInstance("MD5");byte [] array = md5.digest(data.getBytes("UTF-8"));StringBuilder sb = new StringBuilder();for (byte item : array) {sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));}return sb.toString().toUpperCase();}catch (Exception e){e.printStackTrace();}return null;}/*** 生成uuid*/public static String generateUUID(){String uuid = UUID.randomUUID().toString().replaceAll("-","").substring(0,32);return uuid;}public static void main(String[] args) {/*** 模拟如下*///第一步:用户端发起请求,生成签名后发送请求//appId 和 appSecret 由生成者提供String appSecret = "35AB7ECF665EF5EF44CF8640EC136300";String appId = "wx123456789";String timestamp = new Date().getTime() + "";String Id = ObjectId.next();//生成签名 注意map顺序SortedMap<String, String> sortedMap = new TreeMap<>();sortedMap.put("appid", appId);sortedMap.put("timestamp", timestamp);sortedMap.put("nonce", Id);//通过sortedMap的参数 以及 appSecret 生成签名String sign = SignUtil.createSign(sortedMap, appSecret);System.out.println(appId + "生成签名:"+ sign);/*** 模拟服务端接受参数   并处理校验签名*///服务端接受到的参数String appid = sortedMap.get("appid");String timestamp1 = sortedMap.get("timestamp");String nonce = sortedMap.get("nonce");String websign = sign;//2.组装参数,SortedMap<String, String> sortedMap12 = new TreeMap<>();sortedMap12.put("appid", appid);sortedMap12.put("timestamp", timestamp1);sortedMap12.put("nonce", nonce);sortedMap12.put("signature", websign);//3.校验签名//sortedMap12模拟客户请求 ,appSecret表示数据库中存储的密钥Boolean flag = SignUtil.isCorrectSign(sortedMap12, appSecret);if(flag){System.out.println("签名验证通过");}else {System.out.println("签名验证未通过");}}
}

切面AOP:

@Order(2)
@Aspect
@Component
@Slf4j
/*** 通过Aop的方式实现接口签名*/
public class OpenApiValidatorAspect {//同一个请求多长时间内有效 10分钟private static final Long EXPIRE_TIME = 60 * 1000 * 10L;//同一个nonce 请求多长时间内不允许重复请求 2秒private static final Long RESUBMIT_DURATION = 2000L;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate ApplySettingMapper applySettingMapper;private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> map= new ConcurrentHashMap<>();/*** 执行OpenApiController 包下所有带注解@annotation的** @param pjp* @return* @throws Throwable*/@Around("execution(" +"* com.xx.xx.controller.api.OpenApiController.*(..)) " +"&& @annotation(xx.xx.xx.xx.openApiUtils.Signature) " +")")public Object doAround(ProceedingJoinPoint pjp) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();//如果是对外开放的URL, 进行签名校验//获取当前方法的组件MethodSignature methodSignature = (MethodSignature) pjp.getSignature();Method method = methodSignature.getMethod();Signature signature = AnnotationUtils.findAnnotation(method, Signature.class);//验证并获取header中的相关参数/*(1)、appid是否合法(2)、根据appid从配置中心中拿到appsecret(3)、请求是否已经过时,默认10分钟(4)、随机串是否合法(5)、是否允许重复请求*/Map<String, String> signatureHeaders = generateSignatureHeaders(signature, request);//2.组装参数,SortedMap<String, String> sortedMap = new TreeMap<>();sortedMap.put("appid", signatureHeaders.get("appid"));sortedMap.put("timestamp", signatureHeaders.get("timestamp"));sortedMap.put("nonce", signatureHeaders.get("nonce"));sortedMap.put("signature", signatureHeaders.get("signature"));//3.校验签名//sortedMap模拟客户请求 ,appSecret表示数据库中存储的密钥Boolean flag = SignUtil.isCorrectSign(sortedMap, signatureHeaders.get("appSecret"));//比较客户端与服务端签名if (!flag) {String message = "签名不一致";log.error(message);throw new ServiceException("403", message);}// 获取Map value对象, 如果没有则返回默认值//getOrDefault获取参数,获取不到则给默认值ExpiringMap<String, Integer> em= map.getOrDefault(signatureHeaders.get("appid"), ExpiringMap.builder().variableExpiration().build());Integer count = em.getOrDefault(signatureHeaders.get("appid"), 0);if (count >= 10 ) { // 超过次数,不执行目标方法throw new ServiceException("422", signatureHeaders.get("appid") + " 接口请求超过次数");} else if (count == 0){ // 第一次请求时,设置有效时间em.put(signatureHeaders.get("appid"), count + 1, ExpirationPolicy.CREATED,1 , TimeUnit.HOURS);} else { // 未超过次数, 记录加一em.put(signatureHeaders.get("appid"), count + 1);}map.put(signatureHeaders.get("appid"), em);log.info("签名验证通过, 相关信息: " + signatureHeaders);try {return pjp.proceed();} catch (Throwable e) {throw e;}}/*** 根据request 中 header值生成SignatureHeaders实体* <p>* 1.处理header name,通过工具类将header信息绑定到签名实体SignatureHeaders对象上。* 2.验证appid是否合法。* 3.根据appid拿到appsecret。* 4.请求是否已经超时,默认10分钟。* 5.随机串是否合法。* 6.是否允许重复请求。*/private Map<String, String> generateSignatureHeaders(Signature signature, HttpServletRequest request) throws Exception {/*** 需要用到的请求参数*/List<String> params = new ArrayList(4);params.add("appid");params.add("timestamp");params.add("nonce");params.add("signature");/*** 获取参数和value 生成Map*/Map<String, String> headerMap = Collections.list(request.getHeaderNames()).stream().filter(headerName -> params.contains(headerName)).collect(Collectors.toMap(headerName -> headerName, headerName -> request.getHeader(headerName)));//根据appId查询数据库LambdaQueryWrapper<ApplySetting> applySettingLambdaQueryWrapper = new LambdaQueryWrapper<>();LambdaQueryWrapper<ApplySetting> appid = applySettingLambdaQueryWrapper.eq(ApplySetting::getDisabled, EnableStatus.ENABLE).eq(ApplySetting::getStatus, EnableStatus.ENABLE).eq(ApplySetting::getAppId, headerMap.get("appid"));ApplySetting applySetting = applySettingMapper.selectOne(appid);if (applySetting == null) {String errMsg = "未找到appId对应的appSecret, appId=" + headerMap.get("appid");log.error(errMsg);throw new ServiceException("403", "appId或appSecret不正确");} else {headerMap.put("appSecret", applySetting.getAppSecret());}//其他合法性校验String timestamp = headerMap.get("timestamp");if (StringUtils.isEmpty(timestamp)) {throw new ServiceException("422", "参数timestamp不能为空");}Long now = System.currentTimeMillis();Long requestTimestamp = Long.parseLong(headerMap.get("timestamp"));if ((now - requestTimestamp) > EXPIRE_TIME) {String errMsg = "请求时间超过规定范围时间10分钟, timestamp =" + headerMap.get("timestamp");log.error(errMsg);throw new ServiceException("408", errMsg);}/*** 随机数位数*/String nonce = headerMap.get("nonce");if (StringUtils.isEmpty(nonce)) {throw new ServiceException("422", "随机串nonce不能为空");}if (nonce.length() < 10) {String errMsg = "随机串nonce长度最少为10位, nonce=" + nonce;log.error(errMsg);throw new ServiceException("422", errMsg);}/*** 表单重复提交问题*/if (!signature.resubmit()) {String existNonce = (String) redisTemplate.opsForValue().get(nonce);if (Objects.isNull(existNonce)) {redisTemplate.opsForValue().set(nonce, nonce, RESUBMIT_DURATION, TimeUnit.MILLISECONDS);} else {String errMsg = "不允许重复请求, nonce=" + nonce;log.error(errMsg);throw new ServiceException("407", errMsg);}}return headerMap;}

AOP实现中,同时也实现了对接口进行限流

private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> map= new ConcurrentHashMap<>();
// 获取Map value对象, 如果没有则返回默认值
//getOrDefault获取参数,获取不到则给默认值
ExpiringMap<String, Integer> em= map.getOrDefault(signatureHeaders.get("appid"), ExpiringMap.builder().variableExpiration().build());
Integer count = em.getOrDefault(signatureHeaders.get("appid"), 0);
if (count >= 10 ) { // 超过次数,不执行目标方法throw new ServiceException("422", signatureHeaders.get("appid") + " 接口请求超过次数");
} else if (count == 0){ // 第一次请求时,设置有效时间em.put(signatureHeaders.get("appid"), count + 1, ExpirationPolicy.CREATED,1 , TimeUnit.HOURS);
} else { // 未超过次数, 记录加一em.put(signatureHeaders.get("appid"), count + 1);
}
map.put(signatureHeaders.get("appid"), em);

开放接口签名(Signature)实现相关推荐

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

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

  2. app开放接口签名设计与实现

    只要接口暴露在外网,就避免不了安全问题.如果让接口裸奔,其他人只要知道接口地址和参数就可以调用,那简直就是灾难.试想有一个发送注册验证码的接口,如果仅仅知道接口地址和参数(手机号)就可以调用,那短信接 ...

  3. 开放平台设计之接口签名认证

    目录 前言 签名认证 签名认证步骤: 下面以java代码举例: DEMO 前言 当前时代,数据是王 道!当我们自己的平台有了足够大的数据量,就有可能诞生一个开放平台供第三方分析.使用.那么我们怎么去实 ...

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

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

  5. 如何设计安全可靠的开放接口---之签名(sign)

    文章目录 [如何设计安全可靠的开放接口]系列 前言 一.前置知识 二.签名的作用 1. 数据防篡改 2. 身份防冒充 三.流程说明 前置准备 交互流程 接口请求方 接口提供方 完整代码补充 总结 [如 ...

  6. App开放接口api安全:Token签名sign的设计与实现

    点击上方蓝色"方志朋",选择"设为星标"回复"666"获取独家整理的学习资料! 来源:cnblogs.com/whcghost/p/5657 ...

  7. springboot接口签名统一效验_Python如何接入开放平台?签名验签、加密解密、授权认证测试实战...

    当前大型top企业都有非常成熟的开放平台业务,比如微信开放平台.新浪微博开放平台.支付宝开放平台等.开放平台的发展为第三方个人或企业提供了巨大的机遇.开发者想要接入各大开放平台,必须要遵从开放平台的安 ...

  8. 如何设计安全可靠的开放接口---之Token

    文章目录 [如何设计安全可靠的开放接口]系列 前言 一.Token机制 1. Token生成 2. Session存在的问题 3. JWT是如何解决Session存在的问题的 二.JWT中的数据结构 ...

  9. 如何设计安全可靠的开放接口---对请求参加密保护

    文章目录 [如何设计安全可靠的开放接口]系列 前言 AES加解密 代码实现 [如何设计安全可靠的开放接口]系列 1. 如何设计安全可靠的开放接口-之Token 2. 如何设计安全可靠的开放接口-之Ap ...

最新文章

  1. java编写存钱_用Java编写一个简单的存款
  2. xml突然变成空白_“侏罗纪中期”出现了型增转变填补食肉性恐龙体型发展当中的空白...
  3. 多类线性分类器算法原理及代码实现 MATLAB
  4. iPhone 11终于没涨价但依然暴利 外媒:64GB起始容量就是个笑话
  5. SharePoint 2013 Reporting Service 部署配置图文教程
  6. android snackbar 底部,Android KitKat:Snackbar不在屏幕的底部
  7. 基于R语言、MATLAB、Python机器学习方法与案例分析
  8. 7.13 Python循环语句(2)、number、字符串
  9. nfs 跟rpcbind的关系
  10. 影子系统 重启蓝屏 开机蓝屏 安全模式蓝屏 进PE蓝屏 解决方案
  11. android httpclient 设置超时
  12. Expandable Button
  13. hdu3709——数位dp+枚举
  14. 华硕固件 mysql_刷华硕固件后的桥接中继教程
  15. 发现ULC(UltraLightClient)
  16. 湖北省最新测绘资质审批拟批准结果已公示,看看有没有你们公司
  17. mysql 建表 title create table_mysql中create命令建表sql语句
  18. latex IEEE单栏文章图片双栏目排列
  19. 听说要发年终奖了,来来来,我们父相伤害
  20. MCS-51单片机存储地址空间划分

热门文章

  1. Mac office word等办公软件如何关闭endntoe grammarly Acrobat等插件 以及解决word卡顿问题
  2. 批量拍照(证照摄像)软件ANDROID版
  3. 微软mcp证书有用吗_送给开学季的新生们,大学里必须考的六大证书,你都知道吗?...
  4. 基于bluez的树莓派低功耗蓝牙开发:与多个低功耗蓝牙模块连接
  5. html 加载c盘文件,如何手动整理C盘文件(清理整个C盘非系统文件)
  6. Node-RED教程(十四):定制Node-REDUI
  7. Java不行了?别开玩笑了,它明明一直很火
  8. 明日之后1.0(完整版)
  9. 零基础!!1小时学会跨时代的一门新语言 《建议收藏》
  10. STM32F4 读写 AT24C512问题