Chrome插件开发实战

chrome作为目前最流行的浏览器,备受前端推崇,原因除了其对于前端标准的支持这一大核心原因之外,还有就是其强大的扩展性。如今基于其内核开发的插件已经非常多,给我们的生活带来了非常大的方便。

这里我们来讲一下其便利的插件开发

前置知识

chrome插件开发非常简单,只需要chrome浏览器和一个编辑器,只要会js就能写一个基本的插件。

插件目录结构

  • manifest.json 必须,插件的配置文件
  • icon.png 19*19的半透明png图片
  • popup.html 弹出页面
  • popup.js 弹出页面的js

manifest配置

这是一个chrome插件最重要也是必不可少的文件,它就是插件的门户,用来配置和插件相关的所有配置,其中manifest_versionnameversion是必不可少的,descriptionicons是推荐的

{
    // 配置文件的版本,指定为2
    "manifest_version": 2,
    // 插件名称
    "name": "plugin-name",
    // 插件版本
    "version": "1.0.0",
    // 插件描述
    "description": "这是插件描述",
    // 插件图标
    "icons":
    {
        "16": "img/icon.png",
        "48": "img/icon.png",
        "128": "img/icon.png"
    },
    // 常驻后台JS或页面
    "background":
    {
        // 2种指定方式,如果指定JS,那么会自动生成一个背景页
        "page": "background.html"
        //"scripts": ["js/background.js"]
    },
    // 浏览器右上角图标设置,browser_action、page_action、app必须三选一
    "browser_action": 
    {
        "default_icon": "img/icon.png",
        // 图标悬停时的标题,可选
        "default_title": "这是一个插件demo",
        "default_popup": "popup.html"
    },
    // 当某些特定页面打开才显示的图标
    /*"page_action":
    {
        "default_icon": "img/icon.png",
        "default_title": "我是pageAction",
        "default_popup": "popup.html"
    },*/
    // 需要直接注入页面的JS
    "content_scripts": 
    [
        {
            // "<all_urls>" 表示匹配所有地址
            "matches": ["<all_urls>"],
            // 注入js脚本,多个JS按顺序注入
            "js": ["js/jquery-1.8.3.js", "js/content-script.js"],
            // 注入css样式
            "css": ["css/custom.css"],
            // 注入时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
            "run_at": "document_start"
        }
    ],
    // 权限申请
    "permissions":
    [
        "storage", // 插件本地存储
    ],
    // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
    "web_accessible_resources": ["js/inject.js"],
    // 插件主页
    "homepage_url": "https://www.baidu.com",
    // 覆盖浏览器默认页面
    "chrome_url_overrides":
    {
        // 覆盖浏览器默认的新标签页
        "newtab": "newtab.html"
    },
    // Chrome40以前的插件配置页写法
    "options_page": "options.html",
    // Chrome40以后的插件配置页写法,如果2个都写,新版Chrome只认后面这一个
    "options_ui":
    {
        "page": "options.html",
        // 添加一些默认的样式,推荐使用
        "chrome_style": true
    },
    // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
    "omnibox": { "keyword" : "go" },
    // 默认语言
    "default_locale": "zh_CN",
    // devtools页面入口,注意只能指向一个HTML文件,不能是JS文件
    "devtools_page": "devtools.html"
}

开发

插件配置

下面是我的插件配置,既manifest.json

{
	"manifest_version": 2,
	"name": "aliyun-login",
	"version": "1.0.2",
	"description": "这是一个免密登陆阿里云的插件,需要配置首展运维平台一起使用",
	"icons": {
		"16": "img/icon.png",
		"48": "img/icon.png",
		"128": "img/icon.png"
	},
	"permissions": [
		"storage"
	],
	"content_scripts": [
		{
			"matches": [
				"http://127.0.0.1:8888/*",
				"http://116.62.128.66:84/*",
				"http://sa.fshows.com/*",
				"https://signin.aliyun.com/*"
			],
			"js": ["cryptojs/aes.js", "cryptojs/pad-nopadding.js", "cryptojs/mode-ecb.js", "js/content-script.js"],
			"run_at": "document_end"
		}
	]
}

插件实现

我的需求是:在我的某个页面A并传递阿里云账号密码,拿到账号密码后保存至storage,在阿里云的登陆页面B检测storage是否存在密码,存在则登陆

具体实现过程如下
  • 1、运维平台页面——插件处理脚本。插件执行的时间:页面加载完成,此时,监听页面消息。监听到消息后,判断消息类型,接收账号密码,调用谷歌的api存储账号密码。
  let hiddenElement = document.createElement('input')
  hiddenElement.type = 'hidden'
  hiddenElement.id = 'ALILOGINCHECKING'
  // 向页面插入标签
  document.body.append(hiddenElement) 
  // 监听运维平台某页面传递过来的账号密码
  window.addEventListener('message', event => {
    const { type, linkurl, username, password, secretKey } = event.data
    if (type && type == 'FROM_CMDB_PAGE' && linkurl && chrome.storage) {
      // 解密
      const user = DecryptEcb(username, secretKey)
      const pwd = DecryptEcb(password, secretKey)
      chrome.storage.local.set({ userinfo: { username: user, password: pwd } })
      window.open(linkurl, '_blank')
    }
  })
  • 2、阿里云登陆页面——插件处理脚本。接收账号密码,js利用DOM完成登陆
chrome.storage.local.get('userinfo', object => {
    // 取账号密码
    const USERNAME = object.userinfo ? object.userinfo.username : ''
    const PASSWORD = object.userinfo ? object.userinfo.password : ''
    if (USERNAME && PASSWORD) {
      // 清除密文
      chrome.storage.local.clear(() => {
        // ...具体的登陆
      })
    }
  })
  • 3、阿里云的子账户号登陆需要先输入账号->点击下一步->输入密码->登陆

以下是我的第一次尝试

// 输入用户名
document.getElementById('user_principal_name').value = USERNAME
// 执行下一步
document.querySelector('.step-main.step-1').setAttribute('class', 'step-main step-2')
document.querySelector('.text.form-buttons').click()
// 输入密码
document.getElementById('password_ims').value = PASSWORD
// 登陆
document.querySelectorAll('.step-main.step-2')[1].setAttribute('class', 'step-main step-2')
document.querySelector('.step-2 .fm-button.submit-btn').click()

上述脚本执行后并未完成登陆,而是到了最后一步停止了
image.png
猜想可能是阿里的点击按钮做了什么操作,最后用上了延迟大法

setTimeout(() => {
  document.querySelector('.step-2 .fm-button.submit-btn').click()
}, 60)

然而,这种方式虽然满足了需求,能够登陆。但是偶尔还是会发送无法登陆的情况,这样只能将延迟时间调大,这种方法我并不能接受。思考良久,骤然发现,我完全可以绕开阿里云的限制,否管它有什么逻辑,我直接提交表达不就ok,下面是我改进后的方案:

document.getElementById('user_principal_name').value = USERNAME // 输入用户名
document.getElementById('password_ims').value = PASSWORD // 输入密码
document.querySelector('.login-form').submit() // 登陆

代码很简洁明了,也实现了需求,目前暂未发现问题。

  • 4、运维平台的某个页面点击链接跳转阿里云——拦截处理。这里我是通过判断浏览器的window.navigator.vendor是否包含Google来判断浏览器的内核,并检测页面上是否生成了idALILOGINCHECKING的标签,满足条件则向脚本处发送明文账号密码
// 判断是否为谷歌浏览器,并且安装了插件
  const url = '待跳转的阿里云链接'	
  if (window.navigator.vendor.indexOf('Google') > -1 && document.getElementById('ALILOGINCHECKING')) {
    // 掉接口获取账号密码
    GET_ALI_ACCOUNT().then(res => {
      // 向插件发账号密码,免密登陆阿里云
      window.postMessage({ type: "FROM_CMDB_PAGE", linkurl: url, username: res.username, password: res.password, secretKey: 'ghgerEXZYaaUj8xz' }, "*")
    })
  } else {
    window.open(url, '_blank')
  }
// 定义全局的解密方法
const DecryptEcb = (keyword, secretKey) => {
  try {
    let key = CryptoJS.enc.Utf8.parse(secretKey)
    let encryptedHexStr = CryptoJS.enc.Hex.parse(keyword)
    let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr)
    let decrypt = CryptoJS.AES.decrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding })
    let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
    return decryptedStr.toString()
  } catch (error) {
    console.log(error)
    return ''
  }
}
插件的安装、调试、打包

从chrome浏览器的右上角菜单->更多工具->扩展程序可以进入插件管理页面,也可以直接在地址栏输入chrome://exensions 访问,勾选开发者模式,选择加载以解压的扩展程序完成安装。

开发中,代码有任何改动都必须重新加载插件,只需要在插件管理页按下Ctrl+R即可,以防万一最好还把页面刷新一下

打包,同样在此页面选择打包扩展程序即可

疑难点

1、两个脚本页面如何共享数据

浏览器的API提供了storage用来存储信息

chrome.storage.local.set({ userinfo: { username: user, password: pwd } })
chrome.storage.local.get('userinfo', object => {})
chrome.storage.local.clear(() => {})
2、后台页面与插件页面如何交互
// 接收
window.addEventListener('message', event => {})
// 发送
window.postMessage({ type: "FROM_CMDB_PAGE" }, "*")
3、判断插件状态

未找到浏览器自带的安装以及卸载生命周期,因此无法判断是否安装了插件。利用插件向页面插入一特定标签,页面检测是否存在此标签来解决

4、服务器下载插件无权限

插件打包成功后放置服务器,前端需要配合a标签完成下载安装,但是此时发现链接报403
解决方法: chmod 777 aliyun_login.crx

总结

每天进步一点点,积少成多,水滴石穿!


 上一篇
Python学习(一) Python学习(一)
初识python mac环境搭建 mac默认的python环境是2.x,版本比较旧,去官网下载最新的3.x包安装即可,下载地址:https://www.python.org/downloads/ python -V // 默认的版本
2019年05月27日
下一篇 
JS AES加密解密(ECB/CBC) JS AES加密解密(ECB/CBC)
今天配合后端完成账户密码的加密和解密,使用的是AES加密算法 环境配置 js文件https://code.google.com/archive/p/crypto-js/downloads 在线AES加密解密地址http://tool
2019年05月21日