chrome 插件和代理配置实现跨域 cookie 携带

1,392 阅读3分钟

背景介绍

真实的项目通常都是需要携带动态的 cookie 的,复制粘贴到本地可不行,我这边提供两个解决方法。

  1. 第一个就是通过不安全浏览器打开 localhost 项目中直接调用线上地址,并且通过插件或者浏览器自己设置,使得强制携带跨站 cookie。浏览器看的话是 www.juejin.com/api/test 这个前面域名替换为真实要访问的域名,具体详细配置可以看我往篇跨域处理

  2. 代理模式,也就是通过 webpack 代理或者 vite 代理访问真实的地址,浏览器看的话接口为 localhost:3000/api/test,当启动了代理服务器后,我们需要将真实的 cookie 设置到 localhost下。

先获取对应 domain 下的 cookie。然后就设置到 localhost 就行,这里的话 chrome 119 以上的版本会获取不到带有独立分区(partitionKey)的 cookie,需要手动设置顶级域名 。也就是大象放冰箱需要几步

const cookies = await chrome.cookies.getAll({
    url: 'https://www.juejin.com',
    partitionKey: {
        topLevelSite: 'https://shopping.com',
        hasCrossSiteAncestor: false, // 表示此 cookie 不是通过跨站请求设置的
    }
});

// 留个作业,自己写循环 cookies 手动设置
chrome.cookies.set({ 
    url: 'http://localhost', domain: localhost, name, value, path: '/'
})

初始项目,磨刀不误砍柴工

先创建一个 chrome 插件项目

npm create vite@latest

可以参考我的目录

image.png

这里会存在一些热更新问题,因为 public 目录不会触发,但是为了方便,不想处理路径问题,可以先这样。所以需要自己手动 build 或者更改一下 src 目录的代码

我的做法是后者,然后搭配监听自动更新

image.png

继续来实现我们的项目,核心有两个,一个业务逻辑(也就是 curd cookie),一个是如何使用 chrome api

思路展示,谁都是产品经理

我的实现是获取当前用户输入的网址,然后循环获取这个网址的 list,然后扁平化数组,循环手动设置到 localhost (setCookies)

然后使用 storage 存储用户输入的网址,避免下次还需要输入

然后提供更新和删除逻辑,我实现都是全量删除,因为我想大部分人都是配置一次就不管了,用不到的功能咱们就不做,更新也就是重新调用 setCookies 函数

删除的话就是遍历 localhost 下面的 cookie 删掉,然后把存储的网站 list 更新

除此之外,我懒得用第三方 message,自己实现了下提示

还有注意下小细节,domain 是没有协议的,也就是 https/http,所以是 domain: localhost。url 是有协议,设置的时候我们要用 url: http://localhost 的。所以我默认就在输入框帮用户输入了 https:// ,除此之外输入框回车就可以更新

核心代码,我知道你很急,但是你先别急

// import './App.css';
import { useState, useEffect } from "react";

// 用线上就行
const urls = [];

function App() {
  const [baseUrl, setBaseUrl] = useState(urls);
  const [inputVal, setInputVal] = useState("https://");
  const [message, setMessage] = useState("");

  useEffect(() => {
    const fetchBaseUrl = async () => {
      const res = await chrome.storage.local.get("baseUrl");
      // 检查返回的结果
      if (res.baseUrl) {
        setBaseUrl(res.baseUrl);
      } else {
        setBaseUrl(urls); // 使用默认值
      }
    };

    fetchBaseUrl();
  }, []); // 依赖数组为空,意味着只在组件挂载时运行一次

  useEffect(() => {
    return () => {
      // 组件卸载时清除定时器
      clearTimeout();
    };
  });

  const showMessage = (msg) => {
    setMessage(msg);
    let timer = null;
    if (!timer) {
      timer = setTimeout(() => {
        setMessage("");
      }, 2000);
    }
  
    return () =>  clearTimeout(timer);
  }

  const deleteCookies = async () => {
    // 重置
    chrome.storage.local.set({ baseUrl: urls }, () => {
      setBaseUrl(urls);
      showMessage("重置成功");
    });

    chrome.cookies.getAll({ url: "http://localhost" }, (cookies) => {
      cookies.forEach((cookie) => {
        chrome.cookies.remove(
          { url: "http://localhost", name: cookie.name },
          (details) => {
            if (chrome.runtime.lastError) {
              console.error(
                `Error deleting cookie: ${cookie.name}`,
                chrome.runtime.lastError
              );
            } else {
              console.log(`Deleted cookie: ${cookie.name}`);
              // alert('删除成功', cookie.name);
            }
          }
        );
      });
    });
  };

  const setCookies = (cookies) => {
    for (const { name, value } of cookies) {
      chrome.cookies.set(
        {
          url: "http://localhost",
          domain: "localhost",
          name,
          value,
          path: "/",
          // path: '/'
          // 可以根据需要设置其它可选的字段
        },
        function (setCookie) {
          if (chrome.runtime.lastError) {
            console.error(
              `Error setting cookie for ${name}:`,
              chrome.runtime.lastError
            );
          } else {
            console.log("Cookie set:", setCookie, name, value);
          }
        }
      );
    }
  }

  const setAllCookies = async () => {
    const cookiePromises = urls.map((url) => chrome.cookies.getAll({ url }));
    const cookieArray = await Promise.all(cookiePromises);

    const cookies = cookieArray.flat();
    if (Array.isArray(cookies) && cookieArray.length > 0) {
      showMessage("获取 cookies 成功");

      setCookies(cookies)
    }
  };

  const onAddUrl = (e) => {
    if (e.key === "Enter") {
      // 阻止换行
      e.preventDefault();
  
      // 获取当前输入的 URL
      const inputUrl = e.target.value;
  
      // 获取所有 cookies
      chrome.cookies.getAll({ url: inputUrl }, (cookies) => {
        if (Array.isArray(cookies) && cookies.length > 0) {
          setCookies(cookies);
          showMessage(`获取 cookies 成功`);
  
          // 更新新的 URL 列表
          setBaseUrl((prevUrls) => {
            const uniqueUrls = [...new Set([...prevUrls, inputUrl])];
  
            // 存储新的 URL 列表
            chrome.storage.local.set({ baseUrl: uniqueUrls });
  
            return uniqueUrls; // 返回新的 URL 列表
          });
        } else {
          showMessage(`获取 cookies 失败,请登录或者检查域名完整`);
        }
      });
  
      // 重置输入值
      setInputVal("https://");
    }
  };
  

  return (
    <div
      style={{
        width: 260,
        // maxHeight: 400,
        overflow: "auto",
        padding: 10,
        position: "relative",
      }}
    >
      <div>目前获取的网站 cookie</div>
      {baseUrl?.map((v) => (
        <p key={v}>{v}</p>
      ))}
      <p>添加新的网站</p>
      <textarea
        value={inputVal}
        onKeyDown={onAddUrl}
        onChange={(e) => setInputVal(e.target.value)}
        style={{ width: "100%", height: 100, boxSizing: "border-box" }}
      ></textarea>

      <div>
        <button onClick={setAllCookies} style={{ marginRight: 10 }}>
          更新 cookies
        </button>
        <button
          onClick={() => {
            deleteCookies();
          }}
        >
          删除 cookies
        </button>
      </div>
      {message && (
        <div
          style={{
            color: "green",
            border: "1px solid green",
            padding: '2px 4px',
            zIndex: 100,
            top: 10,
            position: "absolute",
            left: "50%",
            transform: "translateX(-50%)",
            background: 'white',
            display: 'inline-block',
          }}
        >
          {message}
        </div>
      )}
    </div>
  );
}

export default App;

看不懂或者懒得看的同学可以复制过去,跑起来回去看思路就知道了

我这里没考虑到的情况是需要监听 cookie 变化,现在的插件当 cookie 更新后可能需要用户手动更新一下

如果想要实现监听变化,可以在 backgroud.js 文件实现


autoSyncCookiesEvent();

function autoSyncCookiesEvent() {
  console.log('start autoSyncCookiesEvent');
  chrome.cookies.onChanged.addListener(async (params = {}) => {
    const { cookie: { domain, name, value } = {} } = params;

    const localStorage = await chrome.storage.local.get();
    const { domainList = [] } = localStorage || {};

    domainList.forEach(async (item = {}) => {
      const { isAuto = false, source, target, cookieNameList = [] } = item;
      if (!isAuto) return;

      // 1. 过滤域名

      if (removeProtocol(source) === domain) {
        // 2. 过滤name
        const flag = cookieNameList.findIndex((cookieName) => cookieName === name) !== -1;

        if (!flag) return;

        // 3. 更新目标地址的cookie
        await chrome.cookies.set({
          url: addProtocol(target),
          domain: removeProtocol(target),
          name,
          path: '/',
          value
        });
      }
    });
  });
}

const addProtocol = (uri) => {
  return uri.startsWith('http') ? uri : `http://${uri}`;
};

// 移除协议头
const removeProtocol = (uri) => {
  return uri.startsWith('http') ? uri.replace('http://', '').replace('https://', '') : uri;
};