为什么要实现密码自动填充?
密码多了记不住怎么办?每次录入太麻烦怎么办?
自己动手丰衣足食!根据应用场景不同,高度定制密码自动填充策略。在这里我将介绍如何使用Firefox的Greasemonkey插件完成这个任务。
Greasemonkey
Greasemonkey,简称GM,中文俗称为“油猴子”,是Mozilla Firefox的一个附加组件。它让用户安装一些脚本使大部分HTML为主的网页于用户端直接改变得更方便易用。随着Greasemonkey脚本常驻于浏览器,每次随着目的网页打开而自动做修改,使得运行脚本的用户印象深刻地享受其固定便利性。
上面的解释来自于度娘。正是Greasemonkey这个可以随着网页打开而执行JavaScript的功能,给了我们执行密码自动填充策略的机会。
用户脚本
执行策略的机会已经有了,接下来就是如何规划了。策略的执行主要还得依靠Greasemonkey,因为它可以让我们执行属于我们自己的用户脚本,而且是可以在网页加载的时候执行。所以基本思路就是恰好在页面加载完成的时候立即用我们自己的JavaScript程序为网页中的密码框填充密码。
具体的操作流程如下:
通过导航菜单Tools/Greasemonkey/Manage User Scripts
打开Greasemonkey插件。
在主画面的上面点击New User Script...
,填写好Name
和Namespace
,再点击OK
按钮进行确认。这时新的用户脚本就创建好了,其中的内容应该与下面的例子差不多。
// ==UserScript==
// @name autoPasswd
// @namespace KNIGHTRCOM
// @version 1
// @grant none
// ==/UserScript==
快速测试
接下来我们可以快速测试一下,录入下面的语句:
alert('test');
脚本保存之后,请随意访问一个网站,这时你会发现无论访问什么网站,总是会弹出一个消息对话框,其中的文本内容便是上门代码中的测试字符串test
。如果你的情形与现在描述的一样,那么恭喜你啦,中奖了哦!
自动化程序
测试成功后要做的就是实现自动化程序,用它来完成密码自动填充。这里需要编写JavaScript程序了,先是要定位网页中的密码输入框元素,然后再填写事先配置好的密码。我们先实现一个简单的,运行成功后可以进一步重构,请参考下面的代码:
// ==UserScript==
// @name autoPasswd
// @namespace KNIGHTRCOM
// @version 1
// @grant none
// @include /^https?:\/\/localhost:900\d\/.*$/
// ==/UserScript==
try {
if (/^https?:\/\/localhost:900\d\/.*$/.test(window.location)) {
var inputs = document.body.getElementsByTagName('input');
for (var i in inputs) {
var input = inputs[i];
if (input.name == 'j_username') {
input.value = 'admin';
} else if (input.name == 'j_password') {
input.value = 'nimda';
}
}
}
} catch (e) { console.error(e); }
这就是我们的第一个工作版本的程序。在此请让我对代码稍作一下解释。最上方的注释里的@include
后面跟随的是JavaScript语法支持的正则表达式,这表示只有满足该表达式样式的网站URL才可以执行我们这段程序。例子中的注释里只有一行@include
,而实际上你可以根据需要增加更多的@include
,如果你放弃使用它的话,那就跟我们在快速测试里实现的效果一样,无论访问哪个网站,脚本程序都会无条件执行。
@include
所处于的注释块被称为metadata,是配置脚本相关信息的地方,详情请见:http://wiki.greasespot.net/Metadata_Block
接下来是程序代码部分,第一行用于检测当前URL地址是否满足执行条件,这里可能会引起一些歧义,既然@include
已经做了类似的排他处理,为什么还需要多此一举的重复检查呢?这种做法的真实原因其实是为了对不同的URL地址定制不同的密码填充策略。@include
只是为脚本执行规划了一个白名单,而符合白名单样式的每一个网站都可能有他们自己的密码填充策略,所以我们需要用不同的样式作为匹配条件来分别实现具体的程序代码。稍后从后面的完整代码中可以更好的体会到这个好处。
代码再往下一些,我们获取了整个网页中所有的输入框控件,并循环遍历这些控件找出符合条件的用户名和密码输入框,然后分别进行赋值操作。至此,自动化脚本程序已经全部完成。不过为了防止一些意外发生,我在程序一开始就买了保险,try
且catch
。
重构 —— 分离数据与行为
现在程序基本上已经可以按照设定那样开始工作了,但为了可以让它看起来更清晰更合理,我们需要对它做进一步重构,基本思想就是把数据从行为中分离出来。
下面让我们看看经过重构之后的第一个程序版本:
// ==UserScript==
// @name autoPasswd
// @namespace KNIGHTRCOM
// @version 1
// @include /^https?://localhost:900[0-9]/.*$/
// @include /^http://epub.ituring.com.cn/Account/Login.*$/
// @include /^https?://.*/(hmc/hybris|hac)([?/].*)?$/
// @grant none
// ==/UserScript==
var globalConfig = {
'stocks': [
{
targetUrlRegex: new RegExp("^http://epub.ituring.com.cn/Account/Login.*$"),
attributeName: 'id',
usernameKey: 'UserName',
usernameVal: 'xxxxxxxxx',
passwordKey: 'Password',
passwordVal: 'xxxxxxxxx'
},
{
targetUrlRegex: new RegExp("^https?://localhost:900[0-9]/(hac)?.*$"),
attributeName: 'name',
usernameKey: 'j_username',
usernameVal: 'xxxxxxxxx',
passwordKey: 'j_password',
passwordVal: 'xxxxxxxxx'
},
{
targetUrlRegex: new RegExp("^https?://localhost:900[0-9]/hmc/hybris.*$"),
attributeName: 'id',
usernameKey: 'Main_user',
usernameVal: 'xxxxxxxxx',
passwordKey: 'Main_password',
passwordVal: 'xxxxxxxxx'
},
{
targetUrlRegex: new RegExp("^https?://(admin-)?prod-.*/(hmc/hybris|hac)/?.*$"),
attributeName: 'id',
usernameKey: 'UserName',
usernameVal: 'xxxxxxxxx',
passwordKey: 'Password',
passwordVal: 'xxxxxxxxx'
},
{
targetUrlRegex: new RegExp("^https?://(admin-)?preprod-.*/(hmc/hybris|hac)/?.*$"),
attributeName: 'id',
usernameKey: 'UserName',
usernameVal: 'xxxxxxxxx',
passwordKey: 'Password',
passwordVal: 'xxxxxxxxx'
},
{
targetUrlRegex: new RegExp("^https?://(admin-)?con-.*/(hmc/hybris|hac)/?.*$"),
attributeName: 'id',
usernameKey: 'UserName',
usernameVal: 'xxxxxxxxx',
passwordKey: 'Password',
passwordVal: 'xxxxxxxxx'
},
{
targetUrlRegex: new RegExp("^https?://(admin-)?qas-.*/(hmc/hybris|hac)/?.*$"),
attributeName: 'id',
usernameKey: 'UserName',
usernameVal: 'xxxxxxxxx',
passwordKey: 'Password',
passwordVal: 'xxxxxxxxx'
},
{
targetUrlRegex: new RegExp("^https?://(admin-)?dev-.*/(hmc/hybris|hac)/?.*$"),
attributeName: 'id',
usernameKey: 'UserName',
usernameVal: 'xxxxxxxxx',
passwordKey: 'Password',
passwordVal: 'xxxxxxxxx'
},
]
};
function autoit() {
try {
var stocks = globalConfig.stocks;
if (!stocks.length) {
return;
}
for (var idx in stocks) {
if (!stocks[idx].targetUrlRegex.test(window.location)) {
continue;
}
var inputs = document.body.getElementsByTagName('input');
for (var i in inputs) {
var input = inputs[i];
if (input[stocks[idx].attributeName] == stocks[idx].usernameKey) {
input.value = stocks[idx].usernameVal;
} else if (input[stocks[idx].attributeName] == stocks[idx].passwordKey) {
input.value = stocks[idx].passwordVal;
}
}
}
} catch (e) {
alert(e.message);
}
}
setTimeout(autoit, 100);
从代码中我们可以看出,所有的配置信息都已经完全从行为逻辑中分离出来了,以配置信息形式存在,并且经过重构之后的行为逻辑较之未重构之前的代码更为灵活通用。另外还有一点要提的是,我们在前面曾经解释过为什么在已经设置了@include
网站URL的情况下,仍要对程序中的网址做二次校验,观察重构后的代码可以发现,实际校验的网址和@include
中配置的网址并不相同,我们可以说程序中的网址更为详细,是@include
网址样式的子集,是一种更为细致的控制。
现在数据和行为逻辑已经分离了,但看起来感觉好像还差点什么似的。
- 密码竟然是明文的,太儿戏了吧?!
- 配置看起来有些繁琐,是否还有优化的空间呢?
好吧,既然还有问题,那就让我们继续开启下一个任务吧!
信息加密与简化配置
明文密码当然要加密,但我并不对自己实现加密功能有信心,那怎么办,总不能等着天上掉馅饼吧?还真别说,馅饼年年有,今年特别多!开源喽 —— SJCL库,详情请见:https://github.com/bitwiseshiftleft/sjcl/wiki
。
The Stanford Javascript Crypto Library is a project by the Stanford Computer Security Lab to build a secure, powerful, fast, small, easy-to-use, cross-browser library for cryptography in Javascript.
下面是代码加密解密的示例代码,我们可以把这段逻辑直接引入密码自动填充功能里。
var sjcl = require('./sjcl.js')
var ciphertext = sjcl.encrypt("any_encrypt_key_string", "Hello World!")
var plaintext = sjcl.decrypt("any_encrypt_key_string", ciphertext)
console.log(ciphertext)
console.log(plaintext)
程序输出结果:
{
"iv":"mfzPs58R3vonO0tLZyyDTg==","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"gOLZzYQ6sHI=","ct":"yhiREfrAFIu99e1WEVPG7VCKQUA="}
Hello World!
下面是二次重构后的代码,一方面引入了SJCL库,为密码实现了加密,另一方面重构了数据配置,使得配置更简单。整体结构一共有四部分组成,分别为SJCL库引用、配置信息、配置结构定义和自动化处理。
SJCL库加载 —— 为了实现脚本的快速执行,这里直接把SJCL库的源码粘贴进来,这样就节省了从网络中加载SJCL库的时间。
信息配置 —— 经过再次重构之后的配置内容与初次重构里的差不多,只是密码设置需要稍作调整。这里采用的密码看起来就是一个字符串类型的JSON数据,这种数据是经过加密处理得出的,具体加密操作请参考上面的例子。
配置结构定义 —— 新的结构可以让我们只关注配置的内容,无需考虑配置结构,以及密码解密的细节。
自动化处理 —— 和第一次重构时一样,没有任何变化。
其他要点 —— ENCRYPTION_KEY和setTimeout
ENCRYPTION_KEY:这个是密码加密和解密时默认使用的密匙,如果有必要的话,你可以使用你自己的专属密匙来对密码进行加密和解密。如果使用了自定义密匙,在做内容配置的时候就需要为函数
pwdbox
额外再多传入一个密匙参数,这个参数已经在pwdbox
函数中预留出来了,只是在现有的例子中没有使用罢了。setTimeout:为了防止与其他一些自动化程序冲突,这里使用setTimeout来实现延时处理。另外还请注意一下
setTimeout
语句上面的注释内容alert(ENCRYPTION_KEY);
,打开它可以方便我们调试,因为使用Greasemonkey不方便的是,当脚本发生了什么语法错误导致程序无法运行,程序不会有任何表现,所以在这里我用一个alert
来作为程序能过正确通过语法检查的一个信号。
// ==UserScript==
// @name autoPasswd
// @namespace KNIGHTRCOM
// @version 1
// @include /^https?://localhost:900[0-9]/.*$/
// @include /^http://epub.ituring.com.cn/Account/Login.*$/
// @include /^https?://.*/(hmc/hybris|hac)([?/].*)?$/
// @grant none
// ==/UserScript==
"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){
this.toString=function(){
return"CORRUPT: "+this.message};this.message=a},invalid:function(a){
this.toString=function(){
return"INVALID: "+this.message};this.message=a},bug:function(a){
this.toString=function(){
return"BUG: "+this.message};this.message=a},notReady:function(a){
this.toString=function(){
return"NOT READY: "+this.message};this.message=a}}};
"undefined"!==typeof module&&module.exports&&(module.exports=sjcl);"function"===typeof define&&define([],function(){
return sjcl});
sjcl.cipher.aes=function(a){
this.s[0][0][0]||this.O();var b,c,d,e,g=this.s[0][4],f=this.s[1];b=a.length;var h=1;if(4!==b&&6!==b&&8!==b)throw new sjcl.exception.invalid("invalid aes key size");this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=g[c>>>24]<<24^g[c>>16&255]<<16^g[c>>8&255]<<8^g[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:f[0][g[c>>>24]]^f[1][g[c>>16&255]]^f[2][g[c>>8&255]]^f[3][g[c&
255]]};
sjcl.cipher.aes.prototype={encrypt:function(a){
return u(this,a,0)},decrypt:function(a){
return u(this,a,1)},s:[[[],[],[],[],[]],[[],[],[],[],[]]],O:function(){
var a=this.s[0],b=this.s[1],c=a[4],d=b[4],e,g,f,h=[],l=[],k,m,n,p;for(e=0;0x100>e;e++)l[(h[e]=e<<1^283*(e>>7))^e]=e;for(g=f=0;!c[g];g^=k||1,f=l[f]||1)for(n=f^f<<1^f<<2^f<<3^f<<4,n=n>>8^n&255^99,c[g]=n,d[n]=g,m=h[e=h[k=h[g]]],p=0x1010101*m^0x10001*e^0x101*k^0x1010100*g,m=0x101*h[n]^0x1010100*n,e=0;4>e;e++)a[e][g]=m=m<<24^m>>>8,b[e][n]=p=p<<24^p>>>8;for(e=
0;5>e;e++)a[e]=a[e].slice(