好久没做浏览器插件开发了,因为公司堡垒机,每次登录都要输入账号密码和验证码。太浪费时间了,就想着做一个右键菜单形式的扩展。
实现思路也很简单,在这里做下记录,方便下次开发参考。
manifest.json
也叫清单文件。
先简单看下配置:
//需要做C#兼职 或者浏览器插件兼职的请加我QQ:3388486286
{
"manifest_version": 2,
"name": "堡垒机自动化",
"version": "1.0",
"description": "右键菜单跳转到堡垒机页面,并自动登录",
"permissions":
[
"tabs",
"contextMenus",
"webRequest",
"webRequestBlocking",
"https://127.0.0.4/*",
"http://127.0.0.4/*"
],
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": [
"https://127.0.0.4/*",
"http://127.0.0.4/*"
],
"js": ["JS/log.js"]
}
],
"web_accessible_resources": [
"JS/config.json"
],
"icons": {
"16": "Images/icon.png",
"48": "Images/icon.png",
"128": "Images/icon.png"
}
}
上述配置基本包含了插件开发的常用配置。现在简单介绍下每个配置的作用:
1.manifest_version
:也就是版本的意思,你需要使用的插件版本,不过现在已经是第三版了,因为之前用过2开发,2和3的语法有差别,估本版本还是用到老版本的语法。新版本没时间去研究,反正都能用。
2.name
:插件名称(自定义)
3.version
: 当前插件的版本(自定义)
4.description
: 插件描述
5.permissions
:权限(你需要使用哪些API就需要对应的开通权限,也叫注册服务吧)
"tabs", (该权限可以打开新的页面)
"contextMenus",(该权限可以右键创建新的菜单)
"webRequest",(该权限可以监听网络请求)
"webRequestBlocking",(该权限可以监听网络请求并且修改请求)
"https://127.0.0.4/*",(表示插件需要访问以 HTTPS 协议开头、IP 地址为 127.0.0.4 的所有页面或资源。这意味着插件将被允许在浏览器中与这些特定的 IP 地址和相关页面进行通信,无论是通过 HTTP 还是 HTTPS 访问。)
"http://127.0.0.4/*"(表示插件需要访问以 HTTP 协议开头、IP 地址为 127.0.0.4 的所有页面或资源。)
添加对应的地址作用:这意味着插件将被允许在浏览器中与这些特定的 IP 地址和相关页面进行通信,无论是通过 HTTP 还是 HTTPS 访问。
"background":
{ "scripts": ["background.js"] }该脚本会一直运行监听。
具体含义如下:
"background":表示指定插件的后台脚本。
"scripts":表示指定后台脚本文件的路径。
"background.js":表示插件的后台页面脚本文件名为 "background.js"。
通过将脚本文件名添加到 "scripts" 数组中,你可以告诉浏览器插件在加载时要运行哪个脚本文件作为后台页面。这个后台页面脚本通常用于处理插件的核心功能、与浏览器 API 进行交互以及响应来自其他插件部分(如内容脚本或浏览器操作栏)的事件。
7.content_scripts
//content_scripts 为业务JS注入,就是你要操作前端页面元素的JS 其中matches表示你JS要注入到哪些页面地址。
"content_scripts": [
{
"matches": [
"https://127.0.0.4/*",
"http://127.0.0.4/*"
],
"js": ["JS/log.js"] //需要注入的Js文件
}
]
8.web_accessible_resources
:这里面往往放些静态全局的配置文件。
9.icons
:图标文件。
background.js
的思路。1.先注册右键菜单,这样你右键浏览器,就可以看到自己的扩展程序了。
chrome.contextMenus.create({
title:"堡垒机自动化",
onclick:function(){
console.log('准备跳转..')
//跳转指定页面
chrome.tabs.create({url:'https://127.0.0.4/index.php/login'},(tab)=>{
console.log('跳转成功..')
console.log(tab)
//执行业务代码 注入JS
chrome.tabs.executeScript(tab.id,{ file: 'JS/content_script.js'})
})
}
})
其中executeScript
也是注入JS的一种方式,这种方式比较灵活,这种注入方式也可以和页面元素进行交互。
2.跳转到指定页面后,我们需要输入账号和密码,这块业务逻辑就写在content_script.js
中
var credentials = {};
// 读取配置文件并存储到全局对象
async function getConfig() {
try {
const response = await fetch(chrome.runtime.getURL('JS/config.json'));
credentials = await response.json();
console.log(credentials); // 打印全局对象
// 调用填充函数
fillCredentials();
} catch (error) {
console.error('Error reading config file:', error);
}
}
// 在页面上填充账号和密码
function fillCredentials() {
document.querySelector("#pwd_username").value = credentials.username;
document.querySelector("#pwd_pwd").value = credentials.password;
//GetAccessToken();
}
这里我们调用下getConfig()
方法,进行配置文件读取,然后赋值给全局变量存储。,然后调用fillCredentials()
进行账号密码赋值。
3.验证码识别,并赋值登录。
我们验证码就是一个4位字母图片,这块逻辑我是直接调用百度API实现的,本来想着自己后端实现,但是用了下第三方库,要么响应时间太久了,要么就是识别不准确。百度API直接帮你解决了这2个麻烦。但是要收费,我只用了100次的免费体验。
现在说下具体实现思路吧,本来想着直接前端发送请求识别,但是浏览器有同源策略,跨域了…这个最终还是要用到后端服务,于是就用了后端API去做图片识别功能了。
3.1先获取验证码图片的url地址。
这里我们在background.js
中添加网络监听,因为图片url有固定格式,很好匹配。具体代码如下:
// 声明一个变量用于保存监听器
let requestListener;
// 启动网络请求监听器
requestListener = function(details) {
// 检查请求地址是否以指定的 URL 开头
if (details.url.startsWith('https://127.0.0.4/captcha/')) {
// 提取图片地址
const imageUrl = details.url;
// 输出图片地址
console.log('图片地址:', imageUrl);
// 在这里可以进行进一步的处理
sendImageURLToContentScript(imageUrl);
}
}
chrome.webRequest.onBeforeRequest.addListener(
requestListener,
{ urls: ['https://127.0.0.4/captcha/*'] }, // 监听以指定 URL 开头的请求
['blocking']
);
通过 chrome.webRequest.onBeforeRequest.addListener
开启网络监听,当匹配到指定url开头的信息后,就代表该url是图片url,然后把imageUrl
传给content_script.js
这里声明下:
background.js
是不能直接和操作的页面通信的,background.js
主要操作浏览器提供的API。实际操作Dom元素需要通过业务JS去操作。这里的业务JS就是content_script.js
那么background.js
和content_script.js
如何通信呢,看代码:
// 在background.js中获取到图片地址后,延迟1秒发送消息给业务 JS
function sendImageURLToContentScript(imageUrl) {
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
if (tabs.length > 0) {
const tabId = tabs[0].id;
setTimeout(function() {
chrome.tabs.sendMessage(tabId, { imageUrl: imageUrl }, function(response) {
// 在收到业务 JS 的响应后进行处理(可选)
console.log('收到业务 JS 的响应:', response);
});
}, 500); // 延迟1秒后发送消息
}
});
}
通过 chrome.tabs.sendMessage
发送消息。
// 业务 JS 接收消息,并进行处理
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
// 接收到来自 background.js 的消息
if (request.imageUrl) {
const imageUrl = request.imageUrl;
// 在这里进行业务逻辑处理,使用获取到的图片地址
console.log('收到来自 background.js 的图片地址:', imageUrl);
GetCode(imageUrl);
//GetBase64(imageUrl);
// 可以通过 sendResponse 向 background.js 发送响应消息(可选)
sendResponse({ message: '已收到图片地址' });
}
});
通过chrome.runtime.onMessage.addListener
接收消息
这样就实现了background.js
和content_script.js
的通信。
至于为什么要延迟500毫秒再发送,是因为我们点击右键后,content_script.js
才注入到页面,而再这时候,background.js
已经再运行了。所有延迟会再发送消息,这样可以避免content_script.js
那边接收不到消息。
3.2业务JS发送imageUrl给后端api进行图像验证码识别
function GetCode(imageUrl)
{
fetch(credentials.getcode_url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(imageUrl)
})
.then(response => response.text())
.then(result => {
let data = JSON.parse(result);
// 获取 words_result 字段的值
let wordsResult = data.words_result;
// 获取数组中第一个元素的 words 字段的值
let words = wordsResult[0].words;
console.log(words); // 输出: "code"
//赋值code
document.querySelector("#pwd_captcha").value = words;
//点击登录
document.querySelector("#sign-box > form.form-vertical.login-content.active > div.submit-row > button").click();
})
.catch(error => {
console.error("请求出错:", error);
});
}
好了这样就实现了验证码识别,然后赋值登录操作呢。
config.json:
{
"username": "username",
"password": "password",
"client_id":"FrktGAjFVjGv9SSA6S3",
"client_secret":"IFL6FbU6tuFrPoCjaYnvvRrCGd",
"token_url":"https://aip.baidubce.com/oauth/2.0/token",
"getcode_url":"http://localhost:5270/api/CodeIdentity"
}
background.js
// 声明一个变量用于保存监听器
let requestListener;
// 启动网络请求监听器
requestListener = function(details) {
// 检查请求地址是否以指定的 URL 开头
if (details.url.startsWith('https://127.0.0.4/captcha/')) {
// 提取图片地址
const imageUrl = details.url;
// 输出图片地址
console.log('图片地址:', imageUrl);
// 在这里可以进行进一步的处理
sendImageURLToContentScript(imageUrl);
}
}
chrome.webRequest.onBeforeRequest.addListener(
requestListener,
{ urls: ['https://127.0.0.4/captcha/*'] }, // 监听以指定 URL 开头的请求
['blocking']
);
// 在background.js中获取到图片地址后,延迟1秒发送消息给业务 JS
function sendImageURLToContentScript(imageUrl) {
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
if (tabs.length > 0) {
const tabId = tabs[0].id;
setTimeout(function() {
chrome.tabs.sendMessage(tabId, { imageUrl: imageUrl }, function(response) {
// 在收到业务 JS 的响应后进行处理(可选)
console.log('收到业务 JS 的响应:', response);
});
}, 500); // 延迟1秒后发送消息
}
});
}
chrome.contextMenus.create({
title:"堡垒机自动化",
onclick:function(){
console.log('准备跳转..')
//跳转指定页面
chrome.tabs.create({url:'https://127.0.0.4/index.php/login'},(tab)=>{
console.log('跳转成功..')
console.log(tab)
//执行业务代码 注入JS
chrome.tabs.executeScript(tab.id,{ file: 'JS/content_script.js'})
})
}
})
// // 启动网络请求监听器
// chrome.webRequest.onBeforeRequest.addListener(
// function(details) {
// // 检查请求地址是否以指定的 URL 开头
// if (details.url.startsWith('https://127.0.0.4/captcha/')) {
// // 提取图片地址
// const imageUrl = details.url;
// // 输出图片地址
// console.log('图片地址:', imageUrl);
// // 在这里可以进行进一步的处理
// sendImageURLToContentScript(imageUrl);
// }
// },
// { urls: ['https://127.0.0.4/captcha/*'] }, // 监听以指定 URL 开头的请求
// ['blocking']
// );
// 在background.js中获取到图片地址后,延迟1秒发送消息给业务 JS
// function sendImageURLToContentScript(imageUrl) {
// chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
// if (tabs.length > 0) {
// const tabId = tabs[0].id;
// setTimeout(function() {
// chrome.tabs.sendMessage(tabId, { imageUrl: imageUrl }, function(response) {
// // 在收到业务 JS 的响应后进行处理(可选)
// console.log('收到业务 JS 的响应:', response);
// // 移除监听器
// chrome.webRequest.onBeforeRequest.removeListener(requestListener);
// });
// }, 100); // 延迟1秒后发送消息
// }
// });
// }
// 开始监听网络请求
//startRequestListener();
content_script.js
// 业务 JS 接收消息,并进行处理
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
// 接收到来自 background.js 的消息
if (request.imageUrl) {
const imageUrl = request.imageUrl;
// 在这里进行业务逻辑处理,使用获取到的图片地址
console.log('收到来自 background.js 的图片地址:', imageUrl);
GetCode(imageUrl);
//GetBase64(imageUrl);
// 可以通过 sendResponse 向 background.js 发送响应消息(可选)
sendResponse({ message: '已收到图片地址' });
}
});
var credentials = {};
// 读取配置文件并存储到全局对象
async function getConfig() {
try {
const response = await fetch(chrome.runtime.getURL('JS/config.json'));
credentials = await response.json();
console.log(credentials); // 打印全局对象
// 调用填充函数
fillCredentials();
} catch (error) {
console.error('Error reading config file:', error);
}
}
// 在页面上填充账号和密码
function fillCredentials() {
document.querySelector("#pwd_username").value = credentials.username;
document.querySelector("#pwd_pwd").value = credentials.password;
//GetAccessToken();
}
/**
* 使用 AK,SK 生成鉴权签名(Access Token)
* @returns 鉴权签名信息(Access Token)
*/
// function GetAccessToken() {
// const url = credentials.token_url;
// const data = {
// grant_type: 'client_credentials',
// client_id: credentials.client_id,
// client_secret: credentials.client_secret
// };
// const requestOptions = {
// method: 'POST',
// mode: 'cors',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(data)
// };
// return fetch(url, requestOptions)
// .then(response => {
// if (!response.ok) {
// throw new Error('Network response was not ok');
// }
// return response.json();
// })
// .then(data => {
// console.log(data);
// console.log(data.access_token);
// return data.access_token;
// })
// .catch(error => {
// console.error(error);
// });
// }
function GetCode(imageUrl)
{
fetch(credentials.getcode_url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(imageUrl)
})
.then(response => response.text())
.then(result => {
let data = JSON.parse(result);
// 获取 words_result 字段的值
let wordsResult = data.words_result;
// 获取数组中第一个元素的 words 字段的值
let words = wordsResult[0].words;
console.log(words); // 输出: "code"
//赋值code
document.querySelector("#pwd_captcha").value = words;
//点击登录
// document.querySelector("#sign-box > form.form-vertical.login-content.active > div.submit-row > button").click();
})
.catch(error => {
console.error("请求出错:", error);
});
}
//图片转base64
// function getFileContentAsBase64(path) {
// return new Promise((resolve, reject) => {
// fetch(path)
// .then(response => response.arrayBuffer())
// .then(buffer => {
// const bytes = new Uint8Array(buffer);
// let binary = '';
// for (let i = 0; i < bytes.byteLength; i++) {
// binary += String.fromCharCode(bytes[i]);
// }
// const base64 = btoa(binary);
// resolve(base64);
// })
// .catch(error => reject(error));
// });
// }
// function GetBase64(captchaUrl)
// {
// // 使用fetch函数获取验证码图片的二进制数据
// fetch(captchaUrl)
// .then(response => response.blob())
// .then(blob => {
// // 创建一个FileReader对象来读取blob数据
// const reader = new FileReader();
// reader.onloadend = function() {
// // 读取完成后,将二进制数据转换为Base64编码
// const base64 = reader.result.split(',')[1];
// // 调用getFileContentAsBase64方法进行后续处理
// getFileContentAsBase64(base64)
// .then(result => {
// console.log("base64:",result);
// })
// .catch(error => {
// console.error(error);
// });
// };
// reader.readAsDataURL(blob);
// })
// .catch(error => {
// console.error(error);
// });
// }
//识别验证码
// 获取配置并存储到全局对象
getConfig();
后端代码:
using CodeIdentify.Servers;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IBaiDuCodeIdentity, BaiDuCodeIdentityRepostoty>();
builder.Services.AddCors(options => {
options.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
app.UseCors("AllowAll");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
namespace CodeIdentify.Servers
{
public interface IBaiDuCodeIdentity
{
Task<string> GetAccessTokenAsync();
Task<string> GetCodeAsync(string base64);
Task DownloadImageAsync(string url , string saveDirectory);
string GetFileContentAsBase64(string path);
void DeleteImage(string path);
}
}
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using RestSharp;
using System.Net;
namespace CodeIdentify.Servers
{
public class BaiDuCodeIdentityRepostoty : IBaiDuCodeIdentity
{
public void DeleteImage(string path)
{
try
{
// 获取指定文件夹中的所有图片文件
string[] imageFiles = Directory.GetFiles(path, "*.jpg");
foreach (string file in imageFiles)
{
// 删除文件
File.Delete(file);
Console.WriteLine($"已删除文件: {file}");
}
Console.WriteLine("所有图片文件已删除。");
}
catch (Exception ex)
{
throw new Exception(ex.Message);
Console.WriteLine($"删除图片文件时出错:{ex.Message}");
}
}
public async Task DownloadImageAsync(string imageUrl, string saveDirectory)
{
// 创建自定义的 HttpClientHandler,并禁用证书验证
var handler = new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
};
// 创建 HttpClient 实例,并使用自定义的 HttpClientHandler
using (HttpClient httpClient = new HttpClient(handler))
{
try
{
// 发送 GET 请求并获取响应消息
HttpResponseMessage response = await httpClient.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();
// 从响应消息中获取图片内容
byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
// 创建文件保存路径
Directory.CreateDirectory(saveDirectory);
string savePath = Path.Combine(saveDirectory, "image.jpg"); // 要保存的文件名
// 将图片内容保存到本地文件
File.WriteAllBytes(savePath, imageBytes);
Console.WriteLine("图片下载完成。");
}
catch (Exception ex)
{
throw new Exception($"图片下载失败:{ex.Message}");
Console.WriteLine($"图片下载失败:{ex.Message}");
}
}
}
public async Task<string> GetAccessTokenAsync()
{
try
{
var client = new RestClient($"https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=FrktGAjFVjSSA6S3Tcs0f&client_secret=IFL6FbU6rPoCjaSiKjMLYnvvRrCGd");
var request = new RestRequest(String.Empty, Method.Post);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
var body = @"";
request.AddParameter("application/json", body, ParameterType.RequestBody);
var response = await client.ExecuteAsync(request);
var result = JsonConvert.DeserializeObject<dynamic>(response.Content??"");
Console.WriteLine(result?.access_token.ToString());
return result?.access_token.ToString()??"";
}
catch (Exception e)
{
throw new Exception($"可能次数用完了:{e.Message}");
}
}
public async Task<string> GetCodeAsync(string base64)
{
var token = await GetAccessTokenAsync();
var client = new RestClient($"https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic?access_token={token}");
var request = new RestRequest(String.Empty, Method.Post);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddHeader("Accept", "application/json");
// image 可以通过 GetFileBase64Content('C:\fakepath\captcha.png') 方法获取
request.AddParameter("image", base64);
var response = await client.ExecuteAsync(request);
Console.WriteLine(response.Content);
return response.Content??"";
}
public string GetFileContentAsBase64(string path)
{
using (FileStream filestream = new FileStream(path, FileMode.Open))
{
byte[] arr = new byte[filestream.Length];
filestream.Read(arr, 0, (int)filestream.Length);
string base64 = Convert.ToBase64String(arr);
return base64;
}
}
}
}
using CodeIdentify.Servers;
using Microsoft.AspNetCore.Mvc;
namespace CodeIdentify.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CodeIdentityController : ControllerBase
{
private readonly IBaiDuCodeIdentity _baiDuCodeIdentity;
public CodeIdentityController(IBaiDuCodeIdentity baiDuCodeIdentity)
{
_baiDuCodeIdentity = baiDuCodeIdentity;
}
[HttpPost]
public async Task<IActionResult> GetCode([FromBody] string url)
{
string path = "Images\\image.jpg";
string deletepath = "Images";
await _baiDuCodeIdentity.DownloadImageAsync(url, "Images");
string code = await _baiDuCodeIdentity.GetCodeAsync(_baiDuCodeIdentity.GetFileContentAsBase64(path));
if(string.IsNullOrWhiteSpace(code)) return BadRequest("空字符串,识别有误");
_baiDuCodeIdentity.DeleteImage(deletepath);
return Ok(code);
}
}
}