问题概述
在开发浏览器插件时,遇到了一个典型的 Mixed Content(混合内容) 问题。本文详细解释这个问题的原因、影响以及解决方案。
Mixed Content 详细解释
什么是 Mixed Content?
Mixed Content 是指在通过 HTTPS 加载的安全网页中,包含了通过 HTTP 加载的不安全资源。现代浏览器出于安全考虑,会阻止这种混合内容的加载。
浏览器的安全策略
同源策略 (Same-Origin Policy)
浏览器规定,网页只能访问与该网页同源(相同协议、域名、端口)的资源。
HTTPS 安全要求
- HTTPS 页面:加密的安全连接
- HTTP 资源:未加密的不安全连接
- 安全风险:中间人攻击、数据篡改、信息泄露
Mixed Content 的类型
Active Mixed Content(主动混合内容)
- 脚本、样式、iframe、插件等
- 可以修改页面内容,高风险
- 浏览器会严格阻止
Passive Mixed Content(被动混合内容)
- 图片、音频、视频等
- 只能显示内容,不能修改页面
- 浏览器可能会警告但允许加载
浏览器的阻止行为
当检测到 Mixed Content 时,浏览器会:
控制台错误:
css
Mixed Content: The page at 'https://xxxxxxx/...' was loaded over HTTPS,
but requested an insecure resource 'http://xxxxxx/...'.
This request has been blocked; the content must be served over HTTPS.
网络请求失败:
typescript
// 请求会被阻止,返回失败状态
fetch('http://xxxxxxx/api/gitlab/job/123/resources')
.then(response => {
// 永远不会执行到这里
})
.catch(error => {
// 会捕获到网络错误
console.error('Request failed:', error);
});
实际场景分析
GitLab CI Session Tools 的场景
环境配置
- GitLab 页面 :
https://git.xxxxxx.com/*
(HTTPS) - Session 服务 :
http://session.ci.xxxxxx.cn/*
(HTTP) - 插件位置:运行在 GitLab HTTPS 页面中
问题发生的具体位置
javascript
// 在 content-script.js 中直接请求会失败
// ❌ Mixed Content 问题
async function getJobResources(jobID) {
const apiUrl = `http://session.ci.xxxxxx.cn/api/gitlab/job/${jobID}/resources`;
// 这个请求会被浏览器阻止
const response = await fetch(apiUrl); // 失败!
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
错误信息分析
css
Mixed Content: The page at 'https://git.xxxxxx.com/group/project/-/jobs/123'
was loaded over HTTPS, but requested an insecure resource
'http://session.ci.xxxxxx.cn/api/gitlab/job/123/resources'.
This request has been blocked; the content must be served over HTTPS.
为什么会出现这个问题?
历史原因
- 内网服务传统:很多内网服务传统上使用 HTTP 协议
- 证书管理复杂:HTTPS 需要证书管理和续期
- 迁移成本:将现有 HTTP 服务升级到 HTTPS 需要工作量
技术限制
- GitLab 强制 HTTPS:现代 GitLab 部署都强制使用 HTTPS
- Session 服务内网部署:Session 服务通常在内网,可能没有配置 HTTPS
- 跨域请求:即使协议相同,也可能存在跨域问题
解决方案对比
方案一:直接请求(失败)
javascript
// ❌ 在 content script 中直接请求
async function getJobResources(jobID) {
const apiUrl = `http://session.ci.xxxxxx.cn/api/gitlab/job/${jobID}/resources`;
// 会被浏览器阻止
const response = await fetch(apiUrl);
return response.json();
}
优点:
- 代码简单直接
- 没有额外的通信开销
缺点:
- ❌ 完全无法工作:浏览器会阻止请求
- ❌ 用户体验差:功能完全不可用
- ❌ 调试困难:错误信息不够明确
方案二:升级 Session 服务到 HTTPS(理想但不现实)
javascript
// ✅ 如果 Session 服务支持 HTTPS
async function getJobResources(jobID) {
const apiUrl = `https://session.ci.xxxxxx.cn/api/gitlab/job/${jobID}/resources`;
// 可以正常工作
const response = await fetch(apiUrl);
return response.json();
}
优点:
- ✅ 最安全:全程 HTTPS 加密
- ✅ 代码简单:不需要额外的代理逻辑
- ✅ 性能最佳:没有额外的通信开销
缺点:
- ❌ 实施成本高:需要配置 HTTPS 证书
- ❌ 依赖外部团队:可能需要协调其他团队
- ❌ 内网环境复杂:内网环境的证书管理可能很复杂
- ❌ 时间周期长:不是短期内能解决的
方案三:Chrome 扩展代理方案(实际采用)
css
// ✅ 通过 background script 代理
// Content Script → Background Script → API
优点:
- ✅ 立即解决:不需要修改服务端
- ✅ 完全可行:利用 Chrome 扩展的权限机制
- ✅ 安全性好:在扩展的沙盒环境中运行
- ✅ 用户体验佳:功能完全正常
缺点:
- ⚠️ 代码复杂度增加:需要消息传递机制
- ⚠️ 轻微性能开销:额外的进程间通信
- ⚠️ 调试复杂度:需要调试多个脚本
方案对比总结
方案 | 可行性 | 安全性 | 实施成本 | 代码复杂度 |
---|---|---|---|---|
直接请求 | ❌ 不可行 | ❌ 不安全 | ❌ 低 | ❌ 低 |
升级 HTTPS | ✅ 可行 | ✅ 高安全 | ❌ 高 | ✅ 低 |
扩展代理 | ✅ 可行 | ✅ 安全 | ✅ 低 | ⚠️ 中 |
Chrome 扩展代理方案
核心原理
利用 Chrome 扩展的权限隔离机制:
- Content Scripts:运行在网页上下文中,继承网页的安全策略
- Background Scripts:运行在扩展的独立上下文中,有自己的权限系统
架构设计
scss
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ GitLab Page │ │ Content Script │ │ │
│ (HTTPS) │◄──►│ │◄──►│ User Actions │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Chrome Extension │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Content Script │ │ Background Script│ │
│ │ │ │ │ │
│ │ • 页面交互 │ │ • HTTP 代理 │ │
│ │ • UI 渲染 │ │ • 权限管理 │ │
│ │ • 继承网页策略 │ │ • 独立权限 │ │
│ │ • 无法请求 HTTP │ │ • 可请求 HTTP │ │
│ └─────────────────┘ └─────────────────┘ │
│ │ │ │
│ │ │ │
│ └───────────────┐ ┌───────────────────┘ │
│ │ │
│ chrome.runtime.sendMessage │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Message Bus │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Session Service (HTTP) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ API Server │ │ Job Resources │ │ Log/Session │ │
│ │ │ │ │ │ │ │
│ │ • HTTP 协议 │ │ • Kubernetes │ │ • Docker │ │
│ │ • 内网部署 │ │ • Pods │ │ • Containers │ │
│ │ • 无 HTTPS │ │ • Logs │ │ • Terminals │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
权限配置
在 manifest.json
中配置必要的权限:
json
{
"manifest_version": 3,
"name": "GitLab CI Session Tools",
"version": "1.0.1",
"permissions": [
"activeTab", // 当前标签页权限
"storage" // 存储权限
],
"host_permissions": [
"http://session.xxxxxxx.cn/*", // HTTP 服务权限
"https://git.xxxxxx.com/*" // HTTPS 服务权限
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://git.xxxxxx.com/*"],
"js": ["content-script.js"],
"css": ["styles-fixed.css"],
"run_at": "document_end",
"all_frames": false
}
]
}
代码实现详解
Background Script 实现
文件:background.js
typescript
// Background script for handling HTTP requests to avoid Mixed Content issues
// 监听来自其他脚本的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'fetchJobResources') {
const { jobId } = request;
const apiUrl = `http://session.ci.xxxxxxx.cn/api/gitlab/job/${jobId}/resources`;
console.log('Background: Fetching job resources for', jobId);
// Background script 可以请求 HTTP 资源
fetch(apiUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Background: API response received', data);
// 发送成功响应
sendResponse({ success: true, data: data });
})
.catch(error => {
console.error('Background: API request failed', error);
// 发送错误响应
sendResponse({ success: false, error: error.message });
});
// 返回 true 表示异步响应
return true;
}
});
console.log('GitLab CI Session Tools: Background script loaded');
Content Script 实现
文件:content-script.js
(关键部分)
javascript
// 获取Job资源信息 - 通过background script避免Mixed Content问题
async function getJobResources(jobID) {
try {
console.log(`Fetching job resources for Job ID: ${jobID}`);
// 使用chrome.runtime.sendMessage通过background script发送请求
const response = await new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
{ action: 'fetchJobResources', jobId: jobID },
(response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(response);
}
}
);
});
if (!response.success) {
throw new Error(response.error || 'Unknown error');
}
console.log('API Response:', response.data);
return response.data.success ? response.data.data : null;
} catch (error) {
console.error('Failed to get job resources:', error);
return null;
}
}
完整的调用流程
1. 初始化阶段
scss
// content-script.js
async function init() {
if (!isJobPage()) {
console.log('Not a job page, skipping...');
return;
}
const jobID = extractJobID();
if (!jobID) {
console.log('Could not extract job ID from URL');
return;
}
console.log('GitLab Session Extension: Found job ID', jobID);
// 显示加载状态
showLoadingPanel(jobID);
// 获取资源信息
const jobData = await getJobResources(jobID);
if (!jobData) {
showErrorPanel(jobID, '无法连接到Session服务或未找到相关资源');
return;
}
// 创建快捷访问面板
createQuickAccessPanel(jobData);
}
2. 消息发送阶段
javascript
// content-script.js
async function getJobResources(jobID) {
const response = await new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
{ action: 'fetchJobResources', jobId: jobID },
(response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(response);
}
}
);
});
if (!response.success) {
throw new Error(response.error || 'Unknown error');
}
return response.data.success ? response.data.data : null;
}
3. 背景处理阶段
typescript
// background.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'fetchJobResources') {
const { jobId } = request;
const apiUrl = `http://session.ci.xxxxxxxx.cn/api/gitlab/job/${jobId}/resources`;
fetch(apiUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
sendResponse({ success: true, data: data });
})
.catch(error => {
sendResponse({ success: false, error: error.message });
});
return true;
}
});
4. 响应处理阶段
scss
// content-script.js
const jobData = await getJobResources(jobID);
if (jobData) {
// 成功获取数据,创建面板
createQuickAccessPanel(jobData);
} else {
// 获取数据失败,显示错误
showErrorPanel(jobID, '无法连接到Session服务或未找到相关资源');
}
总结
Mixed Content 问题是现代 Web 开发中的常见挑战,特别是在企业内网环境中。通过 Chrome 扩展的代理方案,成功地解决了这个问题,同时保持了代码的可维护性和安全性。
关键要点
- 问题根源:HTTPS 页面无法直接请求 HTTP 资源
- 解决方案:利用 Chrome 扩展的权限隔离机制
- 实现方式:Background Script 作为 HTTP 代理
- 优势:无需修改服务端,立即生效,安全性好
适用场景
这种解决方案适用于以下场景:
- 企业内网应用中的混合协议问题
- 需要在 HTTPS 页面中访问 HTTP API 的浏览器扩展
- 无法立即升级服务端 HTTPS 配置的情况
- 需要保持用户体验的同时解决安全限制