当前位置: 首页 > news >正文

API接口签名验证

文章目录

      • 一、使用背景
      • 二、实现方案
      • 三、具体流程
      • 四、优化
      • 五、代码实现
      • 六、后续优化

一、使用背景

过去对于接口的验证我一般都是直接在登录时为用户发放token,用户在随后的操作中携带了token则允许请求。

但是这样的验证方式存在有一定的问题,如果token被泄露被他人获取,那么就会有非法请求的风险。其他人可以使用这个token自行调用接口进行请求,传入非法参数甚至进行注入攻击等,可能会造成严重的问题。

即存在以下安全问题:

  • 请求身份是否合法
  • 请求参数是否被篡改

为了防止这种情况的发生,我们验证接收到的数据与客户端发送的数据一致,且让接口只能被客户端请求。

二、实现方案

既然要实现接口只能被客户端请求,那么我们不难想到可以与客户端达成某些约定,让客户端按一定的规则发送请求。

只需要服务端与客户端约定一套密钥,客户端在发送请求时拼接上私钥后使用单向加密算法进行加密,服务端收到后使用相同的私钥和加密算法进行加密后验证是否与前端客户端传递的值相同。

这样的方案可以使得用户即使拿到了token没有私钥的话加密后的数据与服务端加密后的肯定不相同而无法通过验证。篡改参数同样会产生相同的问题,从而保证了接口的安全性。

三、具体流程

  • 按照请求参数名的字母升序排列非空请求参数(包含accesskey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串A;
  • 在字符串A最后拼接上secretkey得到字符串B;
  • 对B进行MD5运算,并将得到的字符串所有字符转换为大写,得到sign值;
  • 携带参数accesskey和sign进行请求

四、优化

以上方式虽然解决了非法用户请求和请求参数被篡改的问题,但是还存在着重复使用请求参数伪造二次请求的隐患。

为了解决这个问题我们不难想到请求时生成一个唯一的标识符,那么服务端在接收到请求之后只需要验证是否已经接受过改请求的标识即可。

不过以上方式还存在一个问题就是服务端要保存那么多的请求标识并不现实,所以我们可以设置一个过期时间,过期了就将标识删除。并让请求的时候携带一个时间戳,超过时间的请求直接打回。

五、代码实现

假设请求接口:test?param1=hello&param2=world

那么前端需要发送请求:test?accesskey=xxx&param1=hello&param2=world&nonce=随机唯一标识&timestamp=当前时间戳&sign=xxx

sign = MD5(“accesskey=xxx&nonce=随机唯一标识&param1=hello&param2=world&secretkey=xxx&timestamp=当前时间戳”).toUpperCase()

注意请求接口中的参数可以不按顺序,而用于生成sign的字符串中的各个参数必须按照一定的顺序(与后端约定)

后端验签:

@Component
public class ApiVerifyUtil {public static final String SECRET_KEY = "secretkey";public static final String ACCESS_KEY = "accesskey";public static final String TIMESTAMP_KEY = "timestamp";public static final String NONCE_KEY = "nonce";public static final String SIGN_KEY = "sign";private static final HashMap<String, String> KEY_PAIR;static {KEY_PAIR = new HashMap<>();KEY_PAIR.put("app1", "password1");  // 为客户端分配的密钥KEY_PAIR.put("app2", "password2");}@Autowiredprivate RedisTemplate<String, String> redisTemplateBean;private static RedisTemplate<String, String> redisTemplate;@PostConstructpublic void init() {redisTemplate = redisTemplateBean;}public static final Integer OK = 0;public static final Integer PARAMS_ERROR = 1;public static final Integer LACK_ACCESSKEY = 2;public static final Integer ACCESSKEY_INVALID = 3;public static final Integer LACK_NONCE = 4;public static final Integer LACK_TIMESTAMP = 5;public static final Integer LACK_SIGN = 6;public static final Integer REQUEST_TIMEOUT = 7;public static final Integer REQUEST_REPEATED = 8;public static final Integer REQUEST_INVALID = 9;// 超时时间(ms)public static final long TIMEOUT = 1000 * 60 * 15;public static final String AND = "&";public static final String EQUALS = "=";public static Integer verify(HashMap<String, String> params) {if (params == null || params.isEmpty()) {return PARAMS_ERROR;}// accessKey为空或不合法(即不存在对应的密钥)或请求参数不全则直接打回String accessKey, secretKey, timestamp, nonce, sign;if ((accessKey = params.get(ACCESS_KEY)) == null) {return LACK_ACCESSKEY;}if ((secretKey = KEY_PAIR.get(accessKey)) == null) {return ACCESSKEY_INVALID;}if ((timestamp = params.get(TIMESTAMP_KEY)) == null) {return LACK_TIMESTAMP;} else if (Long.parseLong(timestamp) - System.currentTimeMillis() > TIMEOUT) {// 超过15分钟return REQUEST_TIMEOUT;}if ((nonce = params.get(NONCE_KEY)) == null) {return LACK_NONCE;} else {Set<String> nonceSet = redisTemplate.opsForSet().members("nonce");if (nonceSet != null && nonceSet.contains(nonce)) {return REQUEST_REPEATED;}}if ((sign = params.get(SIGN_KEY)) == null) {return LACK_SIGN;}params.remove(SIGN_KEY);// 加密需要拼接上密钥params.put(SECRET_KEY, secretKey);ArrayList<String> keyList = new ArrayList<>(params.keySet());// 按顺序构造Collections.sort(keyList);StringBuilder sb = new StringBuilder();for (String key : keyList) {sb.append(key).append("=").append(params.get(key)).append("&");}String newSign = sb.toString();newSign = MD5.create().digestHex16(newSign.substring(0, newSign.length() - 1).toUpperCase());if (newSign.equals(sign)) {redisTemplate.opsForSet().add("nonce", nonce);return OK;}return REQUEST_INVALID;}public static HashMap<String, String> getParams(HttpServletRequest httpServletRequest) {String queryString = httpServletRequest.getQueryString();String[] split = queryString.split(AND);HashMap<String, String> params = new HashMap<>();for (String param : split) {params.put(param.split(EQUALS)[0], param.split(EQUALS)[1]);}return params;}public static HashMap<String, String> getParams(String queryString) {String[] split = queryString.split(AND);HashMap<String, String> params = new HashMap<>();for (String param : split) {params.put(param.split(EQUALS)[0], param.split(EQUALS)[1]);}return params;}}

六、后续优化

  1. 加入接口被调用的阈值限制,对接口访问频率设置一定阈值,对超过阈值的请求进行屏蔽及预警。
  2. 白名单机制:指定一些可以访问我们暴露接口的域名,不在白名单里面的域名发过来的请求,直接拒绝。
http://www.lryc.cn/news/24895.html

相关文章:

  • Keettle (pdi-ce) 整库多表迁移(避坑)
  • 搭建私人《我的世界》服务器,使用Cpolar内网穿透更简单
  • map和set的使用
  • 常用正则表达式大全
  • 注意,摸鱼程序员常用的9个小技巧,早点下班不秃头
  • 【Linux】文件时间-ACM
  • [架构之路-124]-《软考-系统架构设计师》-操作系统-3-操作系统原理 - IO设备、微内核、嵌入式系统
  • 【竞赛/TPU】算能TPU编程竞赛总结
  • Substrate 基础教程(Tutorials) -- 模拟网络 添加可信节点
  • SAP 设置无物料号的费用采购
  • k8s ConfigMap 中 subPath 字段和 items 字段
  • UML建模
  • JavaScript常见面试题(更新中)
  • TCP/IP协议
  • Python使用异步线程池实现异步TCP服务器交互
  • matplotlib常用操作
  • 二分算法题
  • Vue+ElementUI+SpringBoot项目配合分页插件快速实现分页(简单暴力)
  • 【回眸】牛客网刷刷刷!嵌入式软件中也会遇到的嵌入式硬件,通讯,通讯协议专题(一)
  • 使用Vue展示数据(动态查询)
  • 构建数据库测试数据——mysql
  • 你想要的Android性能优化系列:启动优化 !
  • python3的基础入门3:基本数据类型
  • 消息队列原理与实战-学习笔记
  • Linux权限相关知识(大量图文展示,及详细操作)
  • Ep_操作系统面试题-什么是协程
  • 在C#中使用互斥量解决多线程访问共享资源的冲突问题
  • JavaEE进阶第六课:SpringBoot配置文件
  • MySQL基础(一)SQL分类、导入、SELECT语句,运算符
  • 反激与正激的区别