记一次由gzip引起的nginx转发事故

故事背景

书接前几篇文章,仍然是交付甲方遇到的一个特殊诉求,从而引发了本期的事故。甲方的诉求是前端的请求过来,需要加密,但是要经过waf,必须要求是请求明文,那就要在waf和nginx之间做一个解密前置应用处理。大致架构图如下:
在这里插入图片描述
本次事故的起因是因为,经过waf的请求响应头信息增加了一个Content-Encoding:gzip导致的数据无法返回前端。

技术栈

nginx:1.16.1
springboot:2.5.14
hutool:5.8.15

NGINX下载:NGINX下载链接

情景再现

我们一点一点还原下,当时遇到的问题。我们这里需要两个java应用和一个nginx三个工程。注意下述不是完整代码,都是核心代码片段。只为说明问题产生的过程,所以不会大面积贴出所有代码!!!

前置服务

http调用目标服务核心代码

注意:此处代码重点在于使用hutool HttpRequest.execute()调用目标服务,其余方法理性观看!

public static HttpResponse executeAndGetResponse(HttpServletRequest request, String forwardAddr, String decryptData, String wafHost) {
        if (decryptData == null) {
            return executeAndGetResponse(request, forwardAddr, wafHost);
        }
        HttpRequest httpRequest = getHttpRequest(request, forwardAddr, wafHost);
        Map<String, Object> copyForm = null;
        if (httpRequest.form() != null) {
            copyForm = new HashMap<>(httpRequest.form());
        }
        
        httpRequest.body(decryptData, request.getContentType());
        //重新设置form,设置body时会将form设为null,所以需重新设置form
        //TODO body和form二者只能存在一个,当form存在时,则body会被置为null,除了Get请求其他参数都要放到body体中
        if (Objects.equals(RequestMethodEnum.GET.getMethod(), request.getMethod())
                && httpRequest.form() == null && CollectionUtil.isNotEmpty(copyForm)) {
            httpRequest.form(copyForm);
        }
        HttpResponse response = httpRequest.execute();
        return response;
    }

响应流返回给ng核心代码

public static void returnStream(HttpResponse forwardResponse, HttpServletResponse response) {
        //拷贝转发响应头携带的信息
        Map<String, List<String>> headers = forwardResponse.headers();
        if (CollectionUtil.isNotEmpty(headers)) {
            log.info("----------------------------------------------");
            for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
                if (CollectionUtil.isNotEmpty(entry.getValue())) {
                    response.addHeader(entry.getKey(), entry.getValue().get(0));
                    log.info("响应头信息:[{}:{}]", entry.getKey(), entry.getValue().get(0));
                } else {
                    response.addHeader(entry.getKey(), "");
                }
            }
        }
        //输出响应日志
        String contentType = forwardResponse.header("Content-Type");
        if (StringUtils.isNotBlank(contentType) && contentType.toLowerCase().startsWith(MediaType.APPLICATION_JSON_VALUE)) {
            String body = forwardResponse.body();
            if (body.length() > 1000) {
                log.info("请求响应长度大于1000,只打印前1000个字符,详情可查看转发服务:{}", StrUtil.cleanBlank(body).substring(0, 1000));
            } else {
                log.info("请求响应:{}", StrUtil.cleanBlank(body));
            }
        }
        OutputStream outputStream = null;
        GZIPOutputStream gzipOut = null;
        try {
            response.setCharacterEncoding("UTF-8");

            outputStream = response.getOutputStream();
            IoUtil.copy(forwardResponse.bodyStream(), outputStream);
            outputStream.flush();
        } catch (IOException e) {
            log.error("流读取IO异常", e);
            throw new RuntimeException(e);
        } finally {
            if (gzipOut != null) {
                try {
                    gzipOut.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

启动配置文件

spring.application.name=router
server.port=7070

waf.host=127.0.0.1
forward.url=http://${waf.host}:9090

目标服务

接口实现

import cn.hutool.core.io.IoUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPOutputStream;

@RestController
@RequestMapping("/hello")
@Slf4j
public class HelloController {

    @GetMapping("/test")
    public Map getTest() {
        Map<String, String> ng = new HashMap<>();
        ng.put("2", "333333333333333333333");
        return ng;
    }

    @GetMapping("/test3")
    public void getTest3(String a, String b, HttpServletResponse response) {
        Map<String, String> ng = new HashMap<>();
        ng.put("2", "Hello,received:" + a + b);

        response.addHeader("Content-Encoding", "gzip");

        OutputStream outputStream = null;
        GZIPOutputStream gzipOut = null;
        try {
            response.setCharacterEncoding("UTF-8");

            outputStream = response.getOutputStream();
            log.info("--响应开始压缩--");
            gzipOut = new GZIPOutputStream(outputStream);

            IoUtil.write(gzipOut, false, JSON.toJSONBytes(ng));
            gzipOut.finish();
            log.info("--响应压缩完成--");

            outputStream.flush();
        } catch (IOException e) {
            log.error("流读取IO异常", e);
            throw new RuntimeException(e);
        } finally {
            if (gzipOut != null) {
                try {
                    gzipOut.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }


    @GetMapping("/test2")
    public Map getTest2() {
        Map<String, String> ng = new HashMap<>();
        ng.put("2", "333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333");
        ng.put("3", "333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333");
        return ng;
    }

    @GetMapping("/gzip-test")
    public void gzipTest(HttpServletRequest request, HttpServletResponse response) {
        response.addHeader("Content-Encoding", "gzip");

        OutputStream outputStream = null;
        GZIPOutputStream gzipOut = null;
        try {
            response.setCharacterEncoding("UTF-8");

            outputStream = response.getOutputStream();
            log.info("--响应开始压缩--");
            gzipOut = new GZIPOutputStream(outputStream);

            Map<String, String> data = new HashMap<>();
            data.put("name", "Kevin");
            IoUtil.write(gzipOut, false, JSON.toJSONBytes(data));
            gzipOut.finish();
            log.info("--响应压缩完成--");

            outputStream.flush();
        } catch (IOException e) {
            log.error("流读取IO异常", e);
            throw new RuntimeException(e);
        } finally {
            if (gzipOut != null) {
                try {
                    gzipOut.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    @GetMapping("/gzip-test2")
    public void gzipTest2(HttpServletRequest request, HttpServletResponse response) {
        response.addHeader("Content-Encoding", "gzip");

        OutputStream outputStream = null;
        GZIPOutputStream gzipOut = null;
        try {
            response.setCharacterEncoding("UTF-8");

            outputStream = response.getOutputStream();
            log.info("--响应开始压缩--");
            gzipOut = new GZIPOutputStream(outputStream);

            Map<String, String> data = new HashMap<>();
            data.put("name", "Mary");
            IoUtil.write(gzipOut, false, JSON.toJSONBytes(data));
            gzipOut.finish();
            log.info("--响应压缩完成--");

            outputStream.flush();
        } catch (IOException e) {
            log.error("流读取IO异常", e);
            throw new RuntimeException(e);
        } finally {
            if (gzipOut != null) {
                try {
                    gzipOut.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

}

启动配置文件

spring.application.name=demo2
server.port=9090

nginx配置


#user  nobody;
worker_processes  1;

error_log  logs/error.log;
#error_log  logs/error.log  notice;
error_log  logs/error.log  info;

pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    upstream backend {
        server 127.0.0.1:7070;
        keepalive 50;
    }

    #gzip  on;

    server {
        listen       6060;
        server_name  127.0.0.1;

        access_log  logs/host.access.log  main;
        client_max_body_size 20m;
        client_header_buffer_size 32k;

        location / {
            proxy_pass http://backend;
            #proxy_http_version 1.1;
            #proxy_set_header Connection "";
        }
    }

}

问题集锦

错误1:NS_ERROR_NET_RESET

在这里插入图片描述

解决方案

增加配置
proxy_http_version 1.1;
proxy_set_header Connection “”;


#user  nobody;
worker_processes  1;

error_log  logs/error.log;
#error_log  logs/error.log  notice;
error_log  logs/error.log  info;

pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    upstream backend {
        server 127.0.0.1:7070;
        keepalive 50;
    }

    #gzip  on;

    server {
        listen       6060;
        server_name  127.0.0.1;

        access_log  logs/host.access.log  main;
        client_max_body_size 20m;
        client_header_buffer_size 32k;

        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
    }

}

错误2:net::ERR_CONTENT_DECODING_FAILED 200 (OK)

在这里插入图片描述

解决方案

修改前置应用返回ng的响应流处理方法,检测到响应头中含有Content-Encoding:gzip对报文内容做压缩处理,再返给ng

public static void returnStream(HttpResponse forwardResponse, HttpServletResponse response) {
        //拷贝转发响应头携带的信息
        Map<String, List<String>> headers = forwardResponse.headers();
        if (CollectionUtil.isNotEmpty(headers)) {
            log.info("----------------------------------------------");
            for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
                if (CollectionUtil.isNotEmpty(entry.getValue())) {
                    response.addHeader(entry.getKey(), entry.getValue().get(0));
                    log.info("响应头信息:[{}:{}]", entry.getKey(), entry.getValue().get(0));
                } else {
                    response.addHeader(entry.getKey(), "");
                }
            }
        }
        //输出响应日志
        String contentType = forwardResponse.header("Content-Type");
        if (StringUtils.isNotBlank(contentType) && contentType.toLowerCase().startsWith(MediaType.APPLICATION_JSON_VALUE)) {
            String body = forwardResponse.body();
            if (body.length() > 1000) {
                log.info("请求响应长度大于1000,只打印前1000个字符,详情可查看转发服务:{}", StrUtil.cleanBlank(body).substring(0, 1000));
            } else {
                log.info("请求响应:{}", StrUtil.cleanBlank(body));
            }
        }
        OutputStream outputStream = null;
        GZIPOutputStream gzipOut = null;
        try {
            response.setCharacterEncoding("UTF-8");

            outputStream = response.getOutputStream();

            String contentEncoding = forwardResponse.contentEncoding();
            if(StringUtils.isNotBlank(contentEncoding) && contentEncoding.equalsIgnoreCase("gzip")) {
                log.info("--响应开始压缩--");
                gzipOut = new GZIPOutputStream(outputStream);
                IoUtil.write(gzipOut, false, forwardResponse.bodyBytes());
                gzipOut.flush();
                gzipOut.finish();
                log.info("--响应压缩完成--");
            } else {
                IoUtil.write(outputStream, false, forwardResponse.bodyBytes());
                outputStream.flush();
            }
        } catch (IOException e) {
            log.error("流读取IO异常", e);
            throw new RuntimeException(e);
        } finally {
            if (gzipOut != null) {
                try {
                    gzipOut.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

高版本Nginx

最开始我本地复现并没有关注到ng的版本,所以开始使用的是1.25.3的版本,还衍生出了新的问题也贴出来供大家参考下。


#user  nobody;
worker_processes  1;

error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;

pid        logs/nginx.pid;


events {
    worker_connections  10000;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    sendfile        on;
    tcp_nopush     on;

    keepalive_timeout  65;

    gzip  on;

    upstream backend {
        server 127.0.0.1:7070;
        keepalive 50;
    }

    server {
        listen       6060;
        server_name  127.0.0.1;

        access_log  logs/host.access.log  main;
        client_max_body_size 20m;
        client_header_buffer_size 32k;

        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

}

错误1:502 Bad Gateway

在这里插入图片描述

解决方案

通过配置ng的错误日志可以查到如下错误:

2024/03/28 01:16:07 [error] 8352#5480: *43 upstream sent duplicate header line: "Transfer-Encoding: chunked", previous value: "Transfer-Encoding: chunked" while reading response header from upstream, client: 127.0.0.1, server: 127.0.0.1, request: "GET /noauth/captcha/slide-image HTTP/1.1", upstream: "http://127.0.0.1:7070/noauth/captcha/slide-image", host: "localhost:6060"

意思是请求头中行Transfer-Encoding: chunked重复导致的;
这个只需要在后端服务中返回ng之前移除请求头就可以了。
注意:这是因为proxy_http_version 1.1;
proxy_set_header Connection “”; 这两个配置导致的,高版本nginx中会自动加入Transfer-Encoding: chunked,所以从后端传过来的response中也存在就会重复,nginx1.16.1版本就不会出现这个问题。
在这里插入图片描述

总结

一个gzip引发的案件,原因是因为过waf的时候,waf会自动引入gzip压缩处理,导致前置应用没有处理,解决此问题的方案有2。

方案一

前置应用获取到目标服务的响应结果后,已经是解压后的数据,这是因为hutool是一个http客户端,如果服务端返回的response中带有gzip的标志,hutool获取到的结果已经是解压过后的数据,可以继续移除hutool获取到的响应头中的Content-Encoding:gzip往外继续抛即可,这样抛到ng的时候也是不带gzip头信息的,数据也刚好搭对。但是注意这就失去了压缩的意义了,会损失一些传输损耗,达不到压缩的积极意义。

方案二

那就是检测到响应头中带有gzip标识,返回响应流的时候,做压缩处理,同样是响应头和响应体搭对即可。

未解之谜

情景复现中难以模拟我们真实的一种场景是,我们做了压缩处理,但是在请求中只有一个接口是成功的,其余接口均无返回,我们也不得其解,如果能找到原因再来公布。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/494726.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网络链路层之(2)PPP协议

网络链路层之(2)PPP协议 Author: Once Day Date: 2024年3月27日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏: 通信网络技术_Once-Day的博客-CSDN…

pt-archiver的实践分享,及为何要用 ob-archiver 归档数据的探讨

作者简介&#xff1a;肖杨&#xff0c;软件开发工程师 在数据密集型业务场景中&#xff0c;数据管理策略是否有效至关重要&#xff0c;它直接关系到系统性能与存储效率的提升。数据归档作为该策略的关键环节&#xff0c;不仅有助于优化数据库性能&#xff0c;还能有效降低存储成…

(一)基于IDEA的JAVA基础9

循环结构及特点 while循环 do while循环 for 循环 特点:有循环条件 循环起始值 循环自增量(每次自增的量) 循环操作 while循环 语法: 初始部分//定义变量的地方&#xff0c;声明变量 while(循环条件){ 循环操作&#xff1b; 迭代部分&#xff1b; } 流程图: 练习:打…

Rust编程(三)生命周期与异常处理

生命周期 生命周期&#xff0c;简而言之就是引用的有效作用域。在大多数时候&#xff0c;我们无需手动的声明生命周期&#xff0c;因为编译器可以自动进行推导。生命周期的主要作用是避免悬垂引用&#xff0c;它会导致程序引用了本不该引用的数据&#xff1a; {let r;{let x …

【办公类-21-11】 20240327三级育婴师 多个二级文件夹的docx合并成docx有页码,转PDF

背景展示&#xff1a;有页码的操作题 背景需求&#xff1a; 实操课终于全部结束了&#xff0c;把考试内容&#xff08;docx&#xff09;都写好了 【办公类-21-10】三级育婴师 视频转文字docx&#xff08;等线小五单倍行距&#xff09;&#xff0c;批量改成“宋体小四、1.5倍行…

2024 MCM数学建模美赛2024年A题复盘,思路与经验分享:资源可用性与性别比例 | 性别比例变化是否对生态系统中的其他生物如寄生虫提供优势(五)

审题 第四问让我们探究性别比例变化是否对生态系统中的其他生物如寄生虫提供优势。这里我们可以把问题简化一下&#xff0c;只探究性别比例会不会对寄生虫提供优势。因为考虑太多生物&#xff0c;会使模型更复杂&#xff0c;我这个水平处理不了这么复杂的问题&#xff0c;是我…

整数在内存里面的存储

整数在内存里面的存储 整数在计算机里面的存储是按照二进制的方式进行存储 显示的时候是按照16进制的方法进行显示 1. 整数在内存中的存储在讲解操作符的时候&#xff0c;我们就讲过了下⾯的内容&#xff1a;整数的2进制表⽰⽅法有三种&#xff0c;即原码、反码和补码 三种…

案例研究|DataEase实现物业数据可视化管理与决策支持

河北隆泰物业服务有限责任公司&#xff08;以下简称为“隆泰物业”&#xff09;创建于2002年&#xff0c;总部设在河北省高碑店市&#xff0c;具有国家一级物业管理企业资质&#xff0c;通过了质量体系、环境管理体系、职业健康安全管理体系等认证。自2016年至今&#xff0c;隆…

FIM配置

FIM&#xff08;功能抑制管理器&#xff09; FIM模块根据DTC状态来确定对应功能是否要禁止 FiM_GetFunctionPermission通过RTE提供给SWC FiMFIDs FiMInhibitionConfigurations FiMInhFunctionIdRef&#xff1a;关联FIMID FiMInhInhibitionMask: FIM_LAST_FAILED Inh Event…

【氮化镓】p-GaN栅极退化的温度和结构相关性

论文总结&#xff1a; 本文献深入研究了带有p-GaN栅极的正常关断型(normally-off)高电子迁移率晶体管(GaN-HEMTs)在恒定电压应力下的时序退化行为。通过直流特性分析和温度依赖性分析&#xff0c;研究了故障时间(TTF)与应力温度和器件几何结构的依赖性。结果显示&#xff0c;p…

算法打卡day19

今日任务&#xff1a; 1&#xff09;235. 二叉搜索树的最近公共祖先 2&#xff09;701.二叉搜索树中的插入操作 3&#xff09;450.删除二叉搜索树中的节点 235. 二叉搜索树的最近公共祖先 题目链接&#xff1a;235. 二叉搜索树的最近公共祖先 - 力扣&#xff08;LeetCode&…

Android 自定义EditText

文章目录 Android 自定义EditText概述源码可清空内容的EditText可显示密码的EditText 使用源码下载 Android 自定义EditText 概述 定义一款可清空内容的 ClearEditText 和可显示密码的 PasswordEditText&#xff0c;支持修改提示图标和大小、背景图片等。 源码 基类&#xf…

大语言模型(LLM)token解读

1. 什么是token&#xff1f; 人们经常在谈论大模型时候&#xff0c;经常会谈到模型很大&#xff0c;我们也常常会看到一种说法&#xff1a; 参数会让我们了解神经网络的结构有多复杂&#xff0c;而token的大小会让我们知道有多少数据用于训练参数。 什么是token&#xff1f;比…

【C语言】Infiniband驱动init_dev_assign函数

一、注释 一个内核模块的初始化函数&#xff0c;用于分配和初始化某些资源。以下是对代码块的逐行中文注释&#xff1a; // 定义一个初始化设备分配的函数 static void init_dev_assign(void) {int i 1;spin_lock_init(&dev_num_str_lock); // 初始化自旋锁if (mlx4_fil…

量化交易入门(二十三)什么是MTM指标,原理是什么

MTM指标全称是Momentum指标,翻译为动量指标。它用来衡量市场价格在一定时间内上涨或下跌的幅度,属于趋势型指标。其计算公式是: MTM(N) 当前收盘价 - N日前的收盘价 其中N表示统计的周期数,常用参数有6日、12日和24日。 MTM指标的应用要点如下: 判断趋势强弱:MTM数值越大,表…

泛型的进阶

1 通配符 &#xff1f; 我们想调用fun函数帮我们打印&#xff0c;但由于不知道Message具体是什么类型&#xff0c;所以我们可以使用 &#xff1a; &#xff1f;即通配符 当我们将fun函数中改为Message<?>此时就不会报错 2 通配符的上界&#xff1a; <? extends 上…

如何使用 ArcGIS Pro 自动矢量化水系

对于某些要素颜色统一的地图&#xff0c;比如电子地图&#xff0c;可以通过图像识别技术将其自动矢量化&#xff0c;这里为大家介绍一下 ArcGIS Pro 自动矢量化水系的方法&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的电子地图数据&#…

二分练习题——123

123 二分等差数列求和前缀和数组 题目分析 连续一段的和我们想到了前缀和&#xff0c;但是这里的l和r的范围为1e12&#xff0c;明显不能用O(n)的时间复杂度去求前缀和。那么我们开始观察序列的特点&#xff0c;可以按照等差数列对序列进行分块。如上图&#xff0c;在求前10个…

虚拟机Linux(centos)安装python3.8(超详细)

一、Python下载 下载地址&#xff1a;https://www.python.org/downloads/source/ 输入下面网址即可直接下载&#xff1a; python3.8&#xff1a;https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgz python3.6&#xff1a;https://www.python.org/ftp/python/3.6.5/…

Chrome 插件 tabs API 解析

Chrome.tabs API 解析 使用 chrome.tabs API 与浏览器的标签页系统进行交互&#xff0c;可以使用此 API 在浏览器中创建、修改和重新排列标签页 Tabs API 不仅提供操作和管理标签页的功能&#xff0c;还可以检测标签页的语言、截取屏幕截图&#xff0c;以及与标签页的内容脚本…