2023-03-30 22:25:27 来源:腾讯云
为什么要保证接口安全
对于互联网来说,只要你系统的接口暴露在外网,就避免不了接口安全问题。 如果你的接口在外网裸奔,只要让黑客知道接口的地址和参数就可以调用,那简直就是灾难。
举个例子:你的网站用户注册的时候,需要填写手机号,发送手机验证码,如果这个发送验证码的接口没有经过特殊安全处理,那这个短信接口早就被人盗刷不知道浪费多少钱了。
(资料图)
那如何保证接口安全呢?
一般来说,暴露在外网的api接口需要做到防篡改和防重放才能称之为安全的接口。
防篡改
我们知道http 是一种无状态的协议,服务端并不知道客户端发送的请求是否合法,也并不知道请求中的参数是否正确。
举个例子, 现在有个充值的接口,调用后可以给用户增加对应的余额。
http://localhost/api/user/recharge?user_id=1001&amount=10
如果非法用户通过抓包获取到接口参数后,修改user_id 或 amount的值就可以实现给任意账户添加余额的目的。
1,如何解决
采用https协议可以将传输的明文进行加密,但是黑客仍然可以截获传输的数据包,进一步伪造请求进行重放攻击。如果黑客使用特殊手段让请求方设备使用了伪造的证书进行通信,那么https加密的内容也会被解密。
一般的做法有2种:
采用https方式把接口的数据进行加密传输,即便是被黑客破解,黑客也花费大量的时间和精力去破解。接口后台对接口的请求参数进行验证,防止被黑客篡改;步骤1:客户端使用约定好的秘钥对传输的参数进行加密,得到签名值sign1,并且将签名值也放入请求的参数中,发送请求给服务端步骤2:服务端接收到客户端的请求,然后使用约定好的秘钥对请求的参数再次进行签名,得到签名值sign2。步骤3:服务端比对sign1和sign2的值,如果不一致,就认定为被篡改,非法请求。防重放
防重放也叫防复用。简单来说就是我获取到这个请求的信息之后什么也不改,,直接拿着接口的参数去 重复请求这个充值的接口
。此时我的请求是合法的, 因为所有参数都是跟合法请求一模一样的。重放攻击会造成两种后果:
对于重放攻击一般有两种做法:
基于timestamp的方案
每次HTTP请求,都需要加上timestamp参数,然后把timestamp和其他参数一起进行数字签名。因为一次正常的HTTP请求,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间比较,是否超过了60s,如果超过了则认为是非法请求。
一般情况下,黑客从抓包重放请求耗时远远超过了60s,所以此时请求中的timestamp参数已经失效了。 如果黑客修改timestamp参数为当前的时间戳,则sign1参数对应的数字签名就会失效,因为黑客不知道签名秘钥,没有办法生成新的数字签名。
但是这种方式的漏洞也是显而易见,如果在60s之内进行重放攻击,那就没办法了,所以这种方式不能保证请求仅一次有效。
老鸟们一般会采取下面这种方案,既可以解决接口重放问题,又可以解决一次请求有效的问题。
基于nonce + timestamp 的方案
nonce的意思是仅一次有效的随机字符串,要求每次请求时该参数要保证不同。实际使用用户信息+时间戳+随机数等信息做个哈希之后,作为nonce参数。
此时服务端的处理流程如下:
去 redis 中查找是否有 key 为nonce:{nonce}
的 string如果没有,则创建这个 key,把这个 key 失效的时间和验证 timestamp 失效的时间一致,比如是 60s。如果有,说明这个 key 在 60s 内已经被使用了,那么这个请求就可以判断为重放请求。这种方案nonce和timestamp参数都作为签名的一部分传到后端,基于timestamp方案可以让黑客只能在60s内进行重放攻击,加上nonce随机数以后可以保证接口只能被调用一次,可以很好的解决重放攻击问题。
代码实现
接下来以SpringBoot项目为例看看如何实现接口的防篡改和防重放功能。
1、构建请求头对象
@Data@Builderpublic class RequestHeader { private String sign ; private Long timestamp ; private String nonce;}
2、工具类从HttpServletRequest获取请求参数
@Slf4j@UtilityClasspublic class HttpDataUtil { /** * post请求处理:获取 Body 参数,转换为SortedMap * * @param request */ public SortedMap getBodyParams(final HttpServletRequest request) throws IOException { byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream()); String body = new String(requestBody); return JsonUtil.json2Object(body, SortedMap.class); } /** * get请求处理:将URL请求参数转换成SortedMap */ public static SortedMap getUrlParams(HttpServletRequest request) { String param = ""; SortedMap result = new TreeMap<>(); if (StringUtils.isEmpty(request.getQueryString())) { return result; } try { param = URLDecoder.decode(request.getQueryString(), "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String[] params = param.split("&"); for (String s : params) { String[] array=s.split("="); result.put(array[0], array[1]); } return result; }}
这里的参数放入SortedMap中对其进行字典排序,前端构建签名时同样需要对参数进行字典排序。
3、签名验证工具类
@Slf4j@UtilityClasspublic class SignUtil { /** * 验证签名 * 验证算法:把timestamp + JsonUtil.object2Json(SortedMap)合成字符串,然后MD5 */ @SneakyThrows public boolean verifySign(SortedMap map, RequestHeader requestHeader) { String params = requestHeader.getNonce() + requestHeader.getTimestamp() + JsonUtil.object2Json(map); return verifySign(params, requestHeader); } /** * 验证签名 */ public boolean verifySign(String params, RequestHeader requestHeader) { log.debug("客户端签名: {}", requestHeader.getSign()); if (StringUtils.isEmpty(params)) { return false; } log.info("客户端上传内容: {}", params); String paramsSign = DigestUtils.md5DigestAsHex(params.getBytes()).toUpperCase(); log.info("客户端上传内容加密后的签名结果: {}", paramsSign); return requestHeader.getSign().equals(paramsSign); }}
4、HttpServletRequest包装类
public class SignRequestWrapper extends HttpServletRequestWrapper { //用于将流保存下来 private byte[] requestBody = null; public SignRequestWrapper(HttpServletRequest request) throws IOException { super(request); requestBody = StreamUtils.copyToByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return bais.read(); } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); }}
防篡改和防重放我们会通过SpringBoot Filter来实现,而编写的filter过滤器需要读取request数据流,但是request数据流只能读取一次,需要自己实现HttpServletRequestWrapper对数据流包装,目的是将request流保存下来。
5、创建过滤器实现安全校验
@Configurationpublic class SignFilterConfiguration { @Value("${sign.maxTime}") private String signMaxTime; //filter中的初始化参数 private Map initParametersMap = new HashMap<>(); @Bean public FilterRegistrationBean contextFilterRegistrationBean() { initParametersMap.put("signMaxTime",signMaxTime); FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(signFilter()); registration.setInitParameters(initParametersMap); registration.addUrlPatterns("/sign/*"); registration.setName("SignFilter"); // 设置过滤器被调用的顺序 registration.setOrder(1); return registration; } @Bean public Filter signFilter() { return new SignFilter(); }}
@Slf4jpublic class SignFilter implements Filter { @Resource private RedisUtil redisUtil; //从fitler配置中获取sign过期时间 private Long signMaxTime; private static final String NONCE_KEY = "x-nonce-"; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; log.info("过滤URL:{}", httpRequest.getRequestURI()); HttpServletRequestWrapper requestWrapper = new SignRequestWrapper(httpRequest); //构建请求头 RequestHeader requestHeader = RequestHeader.builder() .nonce(httpRequest.getHeader("x-Nonce")) .timestamp(Long.parseLong(httpRequest.getHeader("X-Time"))) .sign(httpRequest.getHeader("X-Sign")) .build(); //验证请求头是否存在 if(StringUtils.isEmpty(requestHeader.getSign()) || ObjectUtils.isEmpty(requestHeader.getTimestamp()) || StringUtils.isEmpty(requestHeader.getNonce())){ responseFail(httpResponse, ReturnCode.ILLEGAL_HEADER); return; } /* * 1.重放验证 * 判断timestamp时间戳与当前时间是否操过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。 */ long now = System.currentTimeMillis() / 1000; if (now - requestHeader.getTimestamp() > signMaxTime) { responseFail(httpResponse,ReturnCode.REPLAY_ERROR); return; } //2. 判断nonce boolean nonceExists = redisUtil.hasKey(NONCE_KEY + requestHeader.getNonce()); if(nonceExists){ //请求重复 responseFail(httpResponse,ReturnCode.REPLAY_ERROR); return; }else { redisUtil.set(NONCE_KEY+requestHeader.getNonce(), requestHeader.getNonce(), signMaxTime); } boolean accept; SortedMap paramMap; switch (httpRequest.getMethod()){ case "GET": paramMap = HttpDataUtil.getUrlParams(requestWrapper); accept = SignUtil.verifySign(paramMap, requestHeader); break; case "POST": paramMap = HttpDataUtil.getBodyParams(requestWrapper); accept = SignUtil.verifySign(paramMap, requestHeader); break; default: accept = true; break; } if (accept) { filterChain.doFilter(requestWrapper, servletResponse); } else { responseFail(httpResponse,ReturnCode.ARGUMENT_ERROR); return; } } private void responseFail(HttpServletResponse httpResponse, ReturnCode returnCode) { ResultData
6、Redis工具类
@Componentpublic class RedisUtil { @Resource private RedisTemplate redisTemplate; /** * 判断key是否存在 * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入 * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } }}
标签:
- 加快虚拟仿真实训基地建设 启动职业学校信息化建设试点很必要
- “双减”后如何在满足学生多样需求方面做“加法”?
- 处于生理活跃期且心理发展不成熟 高校开设公共卫生必修课很必要
- 打造高水平人才队伍 爱与希望的种子正在西部地区发芽
- 河南封丘“学生餐后呕吐腹泻”调查:配餐点“未批先餐” 家长盼说法
知识
- 他把银行卡卖给骗子,“黑吃黑”“截胡”十万元
- “老司机”4S店试驾豪车 结果油门当刹车撞了
- 新开工改造城镇老旧小区5.34万个
- 发动巡河志愿者2万余名 “用心护好每一条河”
- 假客服的套路:伪装成大平台客服,层层布局引人上钩
人物
- 天天热消息:[云原生]接口安全方案提供和实践
- 王玮晨盛赞GALA:这个人一到季后赛就续挂 TT则清算失败
- 天天简讯:漳州长泰:萌娃行“开笔礼” 感受传统文化魅力
- 环球今头条!唐朝魏征的这句话怎么读_唐朝魏征的这句话
- 澄迈推行“数字政务大厅” 提升基层群众办事效率_世界焦点
- 国米和巴斯托尼续约合同已搁置 两大豪门将对其展开竞争_天天热头条
- 环球动态:德国切莫误解“独立的中国能力”
- 今日看点:虹桥镇社区事务受理服务中心获评“5A”
- 缅怀革命先烈,弥牟小学学子传承红色精神
- 现货黄金交易策略:多空陷入“拉锯战”,关注初请数据和美联储官员讲话
- 千亿通信龙头 跳水!冲上热搜 苹果CEO库克 现身这家上海公司!_当前速递
- 微资讯!广东中考报名号查询_中考报名号查询
- 沪铜继续上冲驱动力不强 涨势有所收窄
- 下个月,南昌计划举办8场展会
- 明源云财报发布后股价反弹;前搜狗 CEO 王小川入局人工智能-热门
- 华洲数控榫槽机您值得拥有__数控榫槽机_华洲数控发布|每日聚焦
- 交流电源和直流电源的区别_怎么区别交流电源和直流电源 环球微头条
- 今日快讯:江苏无锡:浪漫夜樱醉游人
- subtotal函数中的9和109_subtotal 9代表什么
- 【环球新要闻】湖北省十六运会火种汇集暨火炬传递启动仪式在宜昌隆重举行
- 西安市新城区幸福林带板块推出1宗商住用地 起始价14.38亿元_世界今日报
- 榜额1.3亿元!常州市36项科技攻关重大技术需求发榜
- 成都“第三水源”工程三坝水库开工建设
- “一盔一带”保安全 房山交警在行动
- 世界速讯:浩柏国际(08431.HK)3月29日收盘跌0.4%
- 扁平疣治疗(扁平疣的最好治疗方法)|环球时快讯
- 贵妃芒是不是要等黄了才能吃 贵妃芒颜色变成什么样就可以吃了_天天动态
- 每日视点!追梦谈惜败森林狼后对球队影响:本场前18分钟 我们打得要死不活
- 辽宁机电职业技术学院优势专业有哪些 山西医科大学晋祠学院优势专业有哪些_环球看点
- 国内咖啡频现融资,新一轮混战开始?
- 环球动态:壁挂炉市场业绩触底,冷凝炉逆势增长
- 华映科技:3月28日融资买入169.38万元,融资融券余额8587.75万元 世界动态
- 中国飞鹤:2022年实现营业收入213.1亿元,研发投入同比增长15.9%
- 大美中国 候鸟北归丨苍鹭“衔”来的村庄|重点聚焦
- “厢”约那达慕 休闲周末游——内蒙古展览馆首场后备厢市集明日开张
- 桐昆股份投资新设互联科技公司 含5G通信技术服务 焦点观察
- 商用车板块3月28日涨2.78%,宇通客车领涨,北向资金增持5414.17万元-世界速看
- pte是什么意思呢_pte是什么意思
- 中国磷化工产业链节能与绿色低碳提升项目在贵阳启动-热门看点
- 滴水观音叶子越养越小是什么原因?
- 智慧旅游正当时,如何走好科技文旅融合发展路? 世界热资讯
- 当前讯息:杰伊汉港原油运输受阻,国际油价飙涨5%,内盘石油化工股涨停
- “到中东去”何以成为中企出海的新热潮?
- 3月28日,据美国国家公路交通安全管理局(NHTSA)披露,美国本田汽车公司正在召回部分2020-2021款Pilot和Ridgeline车型,以及2020-2022款Passport和Odyssey车型,共计330318辆,原因为两侧侧翼后视镜背后的加热垫可能粘接不当,镜面玻璃存在脱落风险
- 当前滚动:Steam客户端自明年起不再支持微软Win7/8/8.1 系统
- 以为太阳开始全勤了?其实雨水已候场_焦点简讯
- 中孚信息3月28日盘中涨幅达5%|头条焦点
- 环球快播:美国制度设计的初始逻辑背离民主真谛——揭穿美式民主真相之一
- 要闻速递:华泰证券:港股底部或已确认 关注Q1业绩有望上行及利率敏感度较高的板块
- 币圈惊雷!长达74页的起诉书,监管重锤砸向币安|每日热讯
精彩阅读
- 航天动力:3月27日获融资买入816.71万元 环球看点
- 龙龙高铁福建段进入铺轨阶段-新要闻
- 环球报道:李天腾与赵小宝全集完整版_李天腾与赵小宝全集
- 视焦点讯!科技部启动人工智能领域专项部署工作;沙特阿美246亿元参股荣盛石化丨明日主题前瞻
- 天天短讯!黄多多染了金发_黄磊夫妇发声
- 梦之墨首场"校园On-Site电子设计挑战赛"走进西北工业大学
- 全是焦点!LPL季后赛赛程确定:TES东征管泽元预言一穿四
- 快报:瑞声科技(2018.HK)2022Q4传感器及半导体营收3.56亿元,同比增长49.5%
- 富士通与大阪大学联合开发新的量子计算架构
- 有方科技(688159):实控人及其控制企业拟现金认购彰显信心 物联网劲旅布局云-世界资讯
- 志愿者走进九江美术馆
- 当前头条:2023年宁波建行数字人民币红包多久开始抽签
- 076期老刀福彩3D预测奖号:绝杀一码推荐
- 明湖晓月是什么意思千里寻他千百度(明湖晓月是什么意思)
- 公牛巨人休闲鞋官网_公牛巨人休闲鞋
- 消息!胰腺癌中晚期能活多久_中晚期的肺癌能活多久
- 2022上合昆明马拉松在海埂会堂鸣枪起跑 万名跑者用脚步丈量春城之美_环球播报
- 每日焦点!省委秘书长是什么级别
- 第八届全国十佳文博技术产品及服务推介活动终评结果揭晓
- 深度 | 网友吐槽:最差的一代车!广汽本田11代雅阁实车图曝光
- 精彩看点:公交祭扫专线,4月1日开通!
- 西南证券给予金徽酒买入评级,产品结构持续优化,省外拓展不断突破_世界观焦点
- kfc豆浆是龙王豆浆粉吗|信息
- 灰烬审判军声望戒指哪里领_灰烬审判军声望
- 天天快资讯:“唯小人与女子难养也”_唯小人与女子难养也什么意思
- 成都东部新区:卫生大扫除进行中,居民们纷纷点赞
- 环球要闻:东西问·镇馆之宝丨彭梅:因《狂飙》再火的《孙子兵法》为何被译为“战争的艺术”?
- 什么是理财如何理财
- 造梦西游3联盟怎么加_造梦西游3联盟如何加 今日快看
- 热点评!大杂烩的做法视频_大杂烩的做法
- 济南森林公园有什么好玩的
- 井贤栋:降低数字化门槛,加速中小企业数字化升级_焦点速读
- 湖北十堰泗河综合治理黑臭水体 昔日“龙须沟”蝶变重生
- 速讯:3万多公里的大众保养发动机,车主围观全过程,只为更懂车一点
- 双鸭山旅游景点大全(双鸭山旅游景点大全)
- 大明虾最简单的做法|今日精选
- 怎么样拆大电机铜线,什么拔铜机好?_环球热讯
- 电脑怎么在图片上面加字_电脑怎么在图片上加字_播资讯
- 即时看!内蒙古自治区乌兰察布市2023-03-24 18:04发布道路结冰黄色预警
- 天天百事通!中煤能源(601898)公司年报点评报告:大额减值拖累Q4业绩 资产质量进一步夯实
- 2023上半年安徽教师资格认定申请报名入口-认定登录入口(4月10日至21日
- 世界观焦点:苏f是哪里的车牌
- 当前速读:《原始征途》今日全平台公测 史玉柱曾提400多条修改意见
- 环球热消息:狂神无双最新玩法攻略
- 焦点精选!无糖食品专卖店加盟_无糖食品专卖店
- 协鑫集成拟募资60亿元加码TOPCon电池等产能
- 天天时讯:管涔山林场偶遇褐马鸡
- H&M 扩大规模并搬迁至米尔顿凯恩斯 全球快讯
- 宁静志远淡薄明志的含义是什么呀
- 世界级的名家名团来了 今年全年40余场国际演出琴台上演