开放接口签名(Signature)实现
开放接口签名(Signature)实现方案
既然是对外开放,那么调用者一定没有我们系统的Token,就需要对调用者进行签名验证,签名验证采用主流的验证方式,采用Signature 的方式。
字段 |
类型 |
必传 |
说明 |
appid |
String |
是 |
应用id |
timestamp |
String |
是 |
时间戳 |
nonce |
String |
是 |
随机数、不少于10位 |
signature |
String |
是 |
签名 |
signature: 生成方式
将参数appId=wx123456789&nonce=155121212121×tamp=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×tamp=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)实现相关推荐
- java接口签名(Signature)实现方案续
一.前言 由于之前写过的一片文章 (java接口签名(Signature)实现方案 )收获了很多好评,此次来说一下另一种简单粗暴的签名方案.相对于之前的签名方案,对body.paramenter.pa ...
- app开放接口签名设计与实现
只要接口暴露在外网,就避免不了安全问题.如果让接口裸奔,其他人只要知道接口地址和参数就可以调用,那简直就是灾难.试想有一个发送注册验证码的接口,如果仅仅知道接口地址和参数(手机号)就可以调用,那短信接 ...
- 开放平台设计之接口签名认证
目录 前言 签名认证 签名认证步骤: 下面以java代码举例: DEMO 前言 当前时代,数据是王 道!当我们自己的平台有了足够大的数据量,就有可能诞生一个开放平台供第三方分析.使用.那么我们怎么去实 ...
- 接口签名中的三位小伙伴signature,nonce,timestamp
在请求一些开放平台的接口时,我们一般会在请求头中塞入一些签名相关的信息以证明身份.以微信API-V3的为例,请求头中包含的认证信息主要包含: signature(签名值) nonce(随机串) tim ...
- 如何设计安全可靠的开放接口---之签名(sign)
文章目录 [如何设计安全可靠的开放接口]系列 前言 一.前置知识 二.签名的作用 1. 数据防篡改 2. 身份防冒充 三.流程说明 前置准备 交互流程 接口请求方 接口提供方 完整代码补充 总结 [如 ...
- App开放接口api安全:Token签名sign的设计与实现
点击上方蓝色"方志朋",选择"设为星标"回复"666"获取独家整理的学习资料! 来源:cnblogs.com/whcghost/p/5657 ...
- springboot接口签名统一效验_Python如何接入开放平台?签名验签、加密解密、授权认证测试实战...
当前大型top企业都有非常成熟的开放平台业务,比如微信开放平台.新浪微博开放平台.支付宝开放平台等.开放平台的发展为第三方个人或企业提供了巨大的机遇.开发者想要接入各大开放平台,必须要遵从开放平台的安 ...
- 如何设计安全可靠的开放接口---之Token
文章目录 [如何设计安全可靠的开放接口]系列 前言 一.Token机制 1. Token生成 2. Session存在的问题 3. JWT是如何解决Session存在的问题的 二.JWT中的数据结构 ...
- 如何设计安全可靠的开放接口---对请求参加密保护
文章目录 [如何设计安全可靠的开放接口]系列 前言 AES加解密 代码实现 [如何设计安全可靠的开放接口]系列 1. 如何设计安全可靠的开放接口-之Token 2. 如何设计安全可靠的开放接口-之Ap ...
最新文章
- java编写存钱_用Java编写一个简单的存款
- xml突然变成空白_“侏罗纪中期”出现了型增转变填补食肉性恐龙体型发展当中的空白...
- 多类线性分类器算法原理及代码实现 MATLAB
- iPhone 11终于没涨价但依然暴利 外媒:64GB起始容量就是个笑话
- SharePoint 2013 Reporting Service 部署配置图文教程
- android snackbar 底部,Android KitKat:Snackbar不在屏幕的底部
- 基于R语言、MATLAB、Python机器学习方法与案例分析
- 7.13 Python循环语句(2)、number、字符串
- nfs 跟rpcbind的关系
- 影子系统 重启蓝屏 开机蓝屏 安全模式蓝屏 进PE蓝屏 解决方案
- android httpclient 设置超时
- Expandable Button
- hdu3709——数位dp+枚举
- 华硕固件 mysql_刷华硕固件后的桥接中继教程
- 发现ULC(UltraLightClient)
- 湖北省最新测绘资质审批拟批准结果已公示,看看有没有你们公司
- mysql 建表 title create table_mysql中create命令建表sql语句
- latex IEEE单栏文章图片双栏目排列
- 听说要发年终奖了,来来来,我们父相伤害
- MCS-51单片机存储地址空间划分
热门文章
- Mac office word等办公软件如何关闭endntoe grammarly Acrobat等插件 以及解决word卡顿问题
- 批量拍照(证照摄像)软件ANDROID版
- 微软mcp证书有用吗_送给开学季的新生们,大学里必须考的六大证书,你都知道吗?...
- 基于bluez的树莓派低功耗蓝牙开发:与多个低功耗蓝牙模块连接
- html 加载c盘文件,如何手动整理C盘文件(清理整个C盘非系统文件)
- Node-RED教程(十四):定制Node-REDUI
- Java不行了?别开玩笑了,它明明一直很火
- 明日之后1.0(完整版)
- 零基础!!1小时学会跨时代的一门新语言 《建议收藏》
- STM32F4 读写 AT24C512问题