您现在的位置: 首页 资讯 > > 正文
[云原生]接口安全方案提供和实践_焦点热门
发布时间:2023-03-31 07:44:12 来源:腾讯云

为什么要保证接口安全

对于互联网来说,只要你系统的接口暴露在外网,就避免不了接口安全问题。 如果你的接口在外网裸奔,只要让黑客知道接口的地址和参数就可以调用,那简直就是灾难。


【资料图】

举个例子:你的网站用户注册的时候,需要填写手机号,发送手机验证码,如果这个发送验证码的接口没有经过特殊安全处理,那这个短信接口早就被人盗刷不知道浪费多少钱了。

那如何保证接口安全呢?

一般来说,暴露在外网的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的值,如果不一致,就认定为被篡改,非法请求。

防重放

防重放也叫防复用。简单来说就是我获取到这个请求的信息之后什么也不改,,直接拿着接口的参数去 重复请求这个充值的接口。此时我的请求是合法的, 因为所有参数都是跟合法请求一模一样的。重放攻击会造成两种后果:

针对插入数据库接口:重放攻击,会出现大量重复数据,甚至垃圾数据会把数据库撑爆。针对查询的接口:黑客一般是重点攻击慢查询接口,例如一个慢查询接口1s,只要黑客发起重放攻击,就必然造成系统被拖垮,数据库查询被阻塞死。

对于重放攻击一般有两种做法:

基于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 resultData = ResultData.fail(returnCode.getCode(), returnCode.getMessage());        WebUtils.writeJson(httpResponse,resultData);    }​    @Override    public void init(FilterConfig filterConfig) throws ServletException {        String signTime = filterConfig.getInitParameter("signMaxTime");        signMaxTime = Long.parseLong(signTime);    }}

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;        }    }​}

标签:

我国的花中之王是什么花?花中之神是什么花?

我国的花中之王是什么花?花中之王指的是牡丹,牡丹花花色鲜艳,花姿典雅,花形端庄,是中国传统名花中最...

粤 水 电: 第七届董事会第三十二次会议决议公告_世界今日报

证券代码:002060   证券简称:粤水电   公告编号:临 2022-177        广东水电二局股...

扬杰科技:发行GDR申请事宜获中国证监会受理|每日热点

(原标题:扬杰科技:发行GDR申请事宜获中国证监会受理)证券时报e公司讯,扬杰科技(300373)12月6日晚间...

沪深两市合计成交量继续小幅萎缩 大盘反弹中个股涨多跌少

沪深两市7月7日探底回升,合计成交量继续小幅萎缩,大盘反弹中个股涨多跌少。龙虎榜中,虽然大盘出现反...

多家基金公司发布溢价风险提示 LOF基金二级市场表现异常

近日,多只场内规模不大、流动性欠缺的LOF产品的二级市场交易坐上过山车,价格在多个交易日内暴涨暴跌。...

业绩预增股走出强势独立行情 吸引了机构抢筹

近期市场震荡盘整,业绩预增股却走出强势独立行情,而部分机构已提前埋伏其中,部分业绩大幅预增股则吸...

重庆:到2025年25个重点领域企业能效全部达到基准水平

3月18日,重庆日报记者从市发展改革委获悉,日前,市发展改革委、市经济信息委、市生态环境局、市市场监...

重磅!2021“发现重庆之美”获奖名单揭晓

3月19日,2021发现重庆之美颁奖典礼在线上举行,最美城市管理人、最美坡坎崖、最美街头绿地、垃圾分类时...

去年重庆回收废弃农膜1.4万吨 农膜回收率达89.31%

3月16日,市五届人大常委会第六十九次主任会议听取了市政府关于《重庆市人大常委会对市人民政府农业面源...

申报分两批!今年国家级博士后科研工作站新设站工作启动

3月19日,重庆日报记者从市人力社保局获悉,为推动产学研深度融合,加强博士后工作平台建设,我市将开展...

浙江鄞州:“水、电、气、数”通办专窗实现城乡公共服务均等化

近日,在宁波市鄞州区邱隘镇公共事务服务中心,66岁的邱隘镇沈家新村居民邱秀月在一个窗口相继办理了不...

打开“浙里办” 浙江1000家农贸市场农产品可线上比价

今天哪个菜场的五花肉最便宜?食品安全抽检结果怎么样?这些问题,浙江居民只需打开浙里办APP上的浙里市场...

浙江鉴湖国家湿地公园规划发布 打造乡村数字旅游

19日上午,鉴湖国家湿地公园规划发布暨东鉴湖农旅观光体验启动仪式在绍兴市越城区陶堰街道举行。当天,...

总投资超10亿元!6个石化装备运维项目在岱山签约

日前,总投资超10亿元的6个石化装备运维项目在岱山经济开发区集中签约。此次签约的项目占地106亩,规划...

如何避免成为“买而不做”的“装备党”祝 杰

自恋是人的天性,人们总是希望自己是更好的,那么自己拥有的事物,也就相应地被自我赋予了更高的价值,...

山西临汾:率先在全省建起农村集体经济开发区

3月17日,临汾市农村集体经济发展(集团)有限公司在临汾经济开发区揭牌。以此为标志,临汾率先在全省建起...

一线工作近22年的缉毒警:我知道坏的是毒品不是人性

  “影子”般的缉毒警:一线工作22年,我知道坏的是毒品不是人性  如果我不继续干,别人也要干,缉...

广东肇庆“毒驾连撞5车致1死”肇事司机被批捕

  1月5日14时30分许,广东肇庆市端州区一男子赵某毒驾连撞5车,致一人死亡。  1月10日,澎湃新闻(ww...

江西最大文物倒卖案宣判:倒卖国家二级文物 9人获刑

  中新网南昌1月10日电 (冷峥嵘 张一怡)江西省共青城市人民法院10日发布消息称,近日,该院依法审结...

青海保障门源地震后生活必需品应急物资

  中新网西宁1月10日电 (记者 孙睿)记者10日从青海省商务厅获悉,青海海北州门源县6 9级地震灾害发...

广西东兴口岸恢复通关 入境需网上预约

  中新社防城港1月10日电 (翟李强)自2022年1月10日零时起,广西东兴口岸和边民互市贸易区恢复人员、...

呼和浩特:寒假期间有条件的学校要开展校内托管服务

  中新网呼和浩特1月10日电 (记者 张林虎)10日,记者从呼和浩特市教育局获悉,在暑假校内托管试点的...

“中国最后一个原始部落”翁丁老寨火灾原因公布

  “中国最后一个原始部落”翁丁老寨火灾原因公布:小孩玩火引起  中新网昆明1月10日电 (罗婕)近日...

北京市十五届人大五次会议胜利闭幕

  北京市十五届人大五次会议胜利闭幕   蔡奇陈吉宁李伟魏小东张延昆出席   张延昆齐静当选市人...

天津市委市政府致全市父老乡亲的慰问信:我们一定能够打赢

  中新网天津1月10日电 (记者 张道正)中共天津市委、天津市人民政府10日发布了“致全市父老乡亲的慰...

天津米面油存量由20天提高至30天 超市菜市场进货量翻倍

兰州名师话“美育”:“尚乐立人”分层培优 以“美”润教

子夜直击,天津寒天战“疫”

重庆姐弟被生父扔下坠亡案上诉期结束 一审法院暂未收到两被告人上诉状

天津:划定封控区 全市开展全员核酸检测

江歌母亲江秋莲:尊重法院判决,法律认定在我意料之中

中国边疆“北方第一所”:9名民警守护“生命禁区”

辟谣!网传“封控区管控区相继解封”通知并非西安

河南安阳9日12时至24时新增11例本土确诊病例

老人5折环卫工8折生活困难免费 这家面馆背后有个暖心事

铁路公安以110幅优秀书画作品庆祝人民警察节

本周中东部冷空气频繁 东北等地有降雪

河南新增本土确诊病例60例

“打拐”民警眼里的百态人生:见证一份份不愿放弃的爱

迎腊八北京晴天上线 阵风6至7级体感冻人

多省份倡议春节“非必要不离开”,这地补贴1000元

伪造国家机关证件典型案例发布 有力打击制假贩假行为

15年照顾170多个新生儿 金牌月嫂“漂”到海外去看娃

江歌母亲江秋莲诉刘鑫案一审将于今日宣判

河南省安阳市两地划为高风险地区 一地划为中风险地区

员工迟到一次罚一千引争议 单位惩戒员工法律边界何在?

以体育人 秀出“青年范儿”

保安、厨师曾被竞业限制 企业滥用竞业限制让员工很苦恼

反诈老陈破圈:人民群众在哪 就把反诈宣传开展到哪

一所中职学校的育人实践

各地严惩恶意欠薪 保障农民工及时拿到工资

中学生成剧本杀行业潜在消费人群 多方助推行业“净化”

“这就是我最好的选择”

对餐饮浪费说“不”(百姓关注)

校园“直通车” 服务“零距离”

琉璃河遗址 两段铭文共证北京三千年建城史

千元修复个人征信报告?银行:“征信修复”都是骗局

琉璃河遗址 两段铭文共证北京三千年建城史

北京公交将开展无人驾驶道路测试

河南郑州调整五地为中风险区域 公路入郑需核酸检测阴性证明

“共享法庭”让金融消费者畅享“智慧司法”便利

《传奇2》网游著作权纠纷案峰回路转 最高法五份裁决四份改判一份发回重审

三代警察:从未放弃的28年

“胡叔叔”的寻亲工作室

天津津南本轮本土疫情第3—20例阳性感染者活动轨迹公布

“团圆”行动刑侦专家吕游 每一个案例都有单独的技术方案

河南“战疫”直面五重考验

开考古书店日均两三个顾客 流量时代她决心仍是只卖书

冬奥开幕在即 “双减”催热冰雪课堂

“不得以任何借口拒收患者”彰显生命至上

天津多站进京车票暂停发售

冷空气来袭广州气温骤降 广东多地发布寒冷预警

“电话发我”——“霸气回应”疫情求助背后的城市温度

天津津南区再增20例阳性感染者,详情公布

电影《农民院士》昆明首映 为观众呈现“把论文写在大地上”

x 广告
x 广告

Copyright ©  2015-2023 港澳自然网版权所有  备案号:京ICP备2023022245号-31   联系邮箱:435 226 40 @qq.com