乐趣区

关于javascript:登录重构小记

前言

最近把小站的登录页面给重构了,之前的安全性存在很大问题,根本处于裸奔的状态,特此记录一下过程。

先说一下网站后端语言是 php,为什么用php 呢,因为 php 是世界上最好的语言吗,可能吧,不过最大的起因是因为我的网站托管在虚拟主机上,目前来说,简直所有厂商的虚拟主机都只反对 php,不过本文所波及到的php 代码都非常简略,跟 js 没啥区别。

本次布局的登录形式有三种,明码登录、手机验证码登录、第三方登录,接下来就一一来看一下。

界面

登录界面通常来说都比较简单,无非是几个输入框,对于笔者这种一线搬砖码农来说不过是三下两除二的事件,间接看最终成果:

Element UI和浓浓的 QQ 空间风交杂在一起有没有。

行为验证

当初大多数网站登录前个别都会先进行人机验证,从最早的输出各种各样字符验证码,到当初越来越风行的滑动拼图验证、文字点选验证、无感验证等等,阿里云、网易、腾讯等等大厂都有提供行为验证服务。

行为验证个别由前端和后端配合进行验证,单纯的前端验证并不平安,能够绕过,所以前端验证通过后会生成 token 等标识,传给后端,后端再调用服务商对应的接口来验证。

行为验证的原理可能波及到机器学习什么的,曾经超出笔者的能力范畴,但作为应用方来说,具体应用形式个别服务商都会有具体的例子和示例代码,在此不赘述。

明码登录

明码登录是最传统最历史悠久的登录形式了,注册的时候把账号密码保留到数据库,登录的时候再进行比对,根本准则是不能明文传输、不能明文保留。

具体实现上,首先对明码设定要求,暂定规定是长度八位到十六位,须要至多蕴含大小写字母和数字,可蕴含局部特殊字符:$@$!%*#_~?&,前后端都进行校验。

网站反对 https 的话能够不必思考传输问题,然而我的虚拟主机并不反对,所以须要手动进行加密传输。

后端接管到明码解密后再进行不可逆的加密存储。

明码规定验证

间接通过正则表达式校验即可,上述提到的明码规定的其中一个正则表达式实现:/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9$@!.%*#_~?&]{8,16}$/,后面三个括号都是 (?=p) 的模式,p是一个子模式,?=用来匹配合乎 p 模式之前的地位,整体含意是匹配以 任意字符加小写字母 任意字符加大写字母 任意字符加数字 结尾的八位以上的蕴含数字大小写字母的字符串,其中的 .* 是必要的,否则下面的正则匹配不了任何字符,因为不可能有一个字符串能同时以大小写字母及数字结尾。

加密传输

罕用的加密形式有这几种:MD5、对称加密和非对称加密,在这个场景下 MD5 不适合,因为它是把字符进行不可逆的编码,那传给服务端也解不开,再加上它并不平安,很多人也不认为它是一种加密算法;对称加密的话加密和解密用的是同一个秘钥,这意味着前端代码里也得内置这个秘钥,那只有关上源码就能看到了所以也不平安,就只能抉择非对称加密了。

非对称加密有公钥和私钥两个秘钥,加密和解密别离抉择一个,其中一个加密的数据只能应用另外一个秘钥来加密,这样在前端就能够应用公钥来加密,后端应用私钥解密,公钥就算被发现了没有私钥也没用,目前最出名也最重要的就是 RSA 加密算法了,具体理解可参考阮大神的文章:http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html。

RSA 加密平安的代价之一就是慢,比对称加密慢十分多,所以个别都是和对称加密联合进行应用,比方 https 协定,传输的信息应用对称加密算法进行加密,对称加密的秘钥应用非对称加密形式来加密进行传输。另外,RSA 加密的数据大小不能超过秘钥长度,比方你的秘钥长度为 1024 位,那么所加密的数据最大不能超过 1024/8=128 字节,首先来按登录场景来简略计算一下。

以下面百科上的 utf8 编码转换表来写一个简略的计算字符字节数的办法如下:

function strLen (str) {
    let len = 0
    for(let i = 0; i < str.length; i++) {let code = str.charCodeAt(i)
        if (code <= 0x007f) {len += 1} else if (code <= 0x07ff) {len += 2} else if (code <= 0xffff) {len += 3} else {len += 4}
    }
    return len
}

账号为手机号,也就是 11 个数字,字节大小计算出来为:11;明码以最长 16 位计算出来字节大小约为:16,都远小于 128 字节,所以能够间接应用 RSA 来进行加密,速度的话此处也能够忽略不计。

前端能够应用 jsencrypt 这个库来进行 rsa 加密。在此之前须要学生成公钥和私钥,这个能够应用 openssl 命令行工具,openssl是一个开源的软件工具包,用来实现TLS(传输层平安协定),同时蕴含了次要的加密算法、罕用的密钥和证书封装治理等性能。

生成私钥:

openssl genrsa -out lx_rsa_1024_priv.pem 1024

查看上一步生成的私钥:

cat lx_rsa_1024_priv.pem

获取上述私钥的公钥:

openssl rsa -pubout -in lx_rsa_1024_priv.pem -out lx_rsa_1024_pub.pem

查看上一步生成的公钥:

cat lx_rsa_1024_pub.pem

保留好私钥和公钥,接下来前端应用公钥来加密,装置jsencrypt

npm i jsencrypt

加密代码:

import Jsencrypt from 'jsencrypt';

const rsa_pub = 'xxx'// 公钥
const password = 'xxx'

encrypt.setPublicKey(rsa_pub)
let encryptedPassword = encrypt.encrypt(password)

而后把加密后的账号和密码发送到后端,后端进行解密,php解密代码如下:

<?php 

function decryptRSA($str)
{
    // 读取私钥
    $private_key = openssl_pkey_get_private(RSA_PRIVATE);
    if (!$private_key) {return '私钥有误';}
    // 解密
    $return_de = openssl_private_decrypt(base64_decode($str), $decrypted, $private_key);
    if (!$return_de) {return ('解密失败');
    }
    return $decrypted;
}

解密的时候要先应用 base64_decode 来进行解码的起因是 RSA 加密后是二进制数据,不适宜 http 传输,个别都会应用 base64 转成字符串,从 jsencrypt 的源码里也能看出:

public encrypt(str:string) {
    // Return the encrypted string.
    try {return hex2b64(this.getKey().encrypt(str));
    } catch (ex) {return false;}
}

php解密失去账号密码后就能够去数据库进行比对,这里就须要先讨论一下明码是如何加密存储的。

明码存储

咱们常常会听到某某公司的数据库透露了的音讯,数据库透露最可怕的是什么,除了用户个人信息之外就是明码了,因为当初的各种网站 APP 切实是太多了,每个都要设置明码,所以大多数人都是一个明码走天下,那么如果明码被他人获取了是很可怕的事件,所以明码存储肯定是不可逆的。

最简略的是间接对明码应用 md5 加密,然而罕用明码很容易就被反向查问进去了,略微进阶一点的是把明码和一个简单的随机字符串,俗称盐先拼接起来,再进行 md5,这样反向查问进去的概率就比拟低了,然而如果盐也被窃取了,那人家同样也能够先加盐再进行反向查问,所以为了减少破解难度,每个明码的盐值都是不一样的,盐值和明码通常是存储在一起的。然而以当初计算机的计算能力来说破解起来还是比拟容易的,所以又呈现了一种叫PBKDF2 的办法,简略说来就是进行 N 次 md5,次数越多,破解的耗时也越久,当破解一个明码都须要耗时很久,那么总的代价会是微小的。还有一种是bcrypt 算法,能够通过参数调整计算强度,被认为是比 PBKDF2 更平安的。

以上这些 php 都有内置函数能够反对,然而限于我所用的 php 版本 PBKDF2bcrypt函数都不反对,所以只能抉择本人实现一个简略的 PBKDF2 办法。

应用 PBKDF2 算法个别都会抉择应用 sha 系列 hash 算法,本文抉择sha1,hash 它个 1000 次。

<?php 

// 生成随机字符
function randomStr($len){$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';
    $charsLen = strlen($chars) - 1;
    $str = '';
    for($i = 0; $i < $len; $i++) {$str .= $chars[mt_rand(0,$charsLen)];
    }
    return $str;
}

php生成盐应该有更平安的办法,然而搜寻了一圈,都没找都适合的办法,所以只能这样简略写一个。

接下来要实现的是 PBKDF2 办法,根本逻辑是原始明码和盐进行 hash,将失去的 hash 值再和原始明码进行 hash,这样循环 hash,直到你须要的次数。

<?php 

function PBKDF2HASH($password, $salt, $count)
{
    $curSalt = $salt;
    for($i = 0; $i < $count; $i++) {$curSalt = sha1($password . $curSalt);
    }
    return $curSalt;
}

之后再把生成的 hash 值和盐值一起保留到数据库,登录时再把盐值取出来进行上述的 hash 操作,比对最初生成的值是否统一即可。

维持登录状态

登录胜利后须要放弃登录状态,因为 http 是无状态协定,所以催生了 cookie 的诞生,cookie就是一段文本,放弃在客户端本地,每次发送 http 申请时客户端都会把它带到申请头里,这样服务端就能够通过 cookie 来判断本次会话用户的信息。

个别登录胜利后服务端会设置一个只容许 http 拜访的 cookie,内容个别是一个id,而后把用户信息和这个id 关联起来,这些数据能够放弃在内存里(通常应用 redis 数据库)或者长久化到 MySql 等数据库,下次申请时依据这 id 来判断有没有登录信息。

php 里应用 session 变量能够很容易实现这个需要:

<?php 

session_start();

$_SESSION['uid'] = xxx;

应用 session_start 注册一个新会话或者重用现有会话,而后给超级全局变量 $_SESSION 设置一个键值,具体要保留什么数据因你而定,我这里只保留一个用户 id,用户其余的信息依据id 再去数据库里查问。

设置完后下次收到申请时获取和退出登录时的销毁也很简略:

<?php

session_start();
// 获取
$uid = $_SESSION['uid'];
// 销毁
$_SESSION['uid'] = null;
session_destroy();// 通常来说不须要调用这个办法

当然上述是最简略的形式,毛病也很显著,浏览器敞开或者一段时间后就须要从新登录,另外对单点登录也不太敌对。

要想让登录更长久能够设置 cookie 的有效期和 session 过期工夫长一点:

<?php

// 设置 session_id 的 cookie,五个参数:过期工夫,单位 s、门路 path、域 domain、是否仅在 https 时可用、是否 httponly
session_set_cookie_params(3 * 3600, '/', '.lxqnsys.com', false, true);
// 设置 session 生存工夫
ini_set("session.gc_maxlifetime", 3 * 3600);

session_start();
// ...

然而过期工夫设置的太久是一件又危险的事件,所以最好还是思考应用其余形式。

另一种维持登录状态的形式是应用 JWT(json web token),这种形式简略来说就是登录胜利后把认证信息都返回给客户端,由客户端进行存储,每次http 申请时也带上,服务端不须要存储任何数据,而是从中取出须要的货色,当然,这个 token 是有生成规定的,分三局部组成,伪代码如下:

// 元信息
const header = base64UrlEncode({
    "alg": "HS256",
    "typ": "JWT"
}
// 内容主体
const payload = base64UrlEncode({// 能够选用预约义字段,也能够增加自定义字段})
// 签名,用来检查数据是否被篡改了,secret 是秘钥,不能泄露
const signature = HMACSHA256(`${header}.${payload}`, secret)
// 组成最终的 token
const token = `${header}.${payload}.${signature}`

能够看到生成的 token 是没有加密的,所以不能放敏感信息,硬要放的话须要对 token 再做一层加密。

更多详细信息可参考:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html。

短信登录

短信登录也是当初很遍及的一种登录形式,有些网站甚至只反对短信登录,因为发短信是要钱的,所以肯定须要做一些限度措施,图形验证之类的是必定要的,另外还要限度发送频率,比方 1 分钟或 2 分钟之内只能发送一条,以及同一个手机号一天之内只能发送多少条。

验证码和工夫限度也能够应用 session 来保留:

<?php

// 保留
$sessionArray = array();
$sessionArray['phoneNumber'] = $phoneNumber;
$sessionArray['code'] = $code;
$sessionArray['lastTime'] = time();
$_SESSION['verificationCode'] = $sessionArray;

再次收到申请时从 session 取出来判断手机号、验证码、工夫是否都正确非法。至于限度手机号一天发送的量因为服务商自带就有这个性能,所以就不本人做了。

第三方登录

最初一种要实现的形式是第三方登录,这也是目前很风行的一种登录形式,这种形式的益处是你不须要向以后网站提供第三方网站的账号和明码就能够获取到第三方网站里的一些用户信息,这样在以后网站就能够不必通过麻烦的注册来创立账号及登录,然而有多数网站你抉择了第三方登录以及登录胜利后还立马要让你填手机号明码什么的再注册一遍,不讲武德,几乎智障,我就是图不便才登录第三方账号,完了你还要我注册,说白了就是想要我手机号,如果不是什么非必须的网站,个别到这一步我就跟它说再见了。

第三方登录简略来说就是先跳转去登录第三方网站,登录胜利后会把一些信息如用户惟一的 id、昵称、头像什么的返回给以后网站,以后网站能够依据这些信息来创立新账号或者实现登录,这其中波及到的是一个叫做OAuth 2.0 的协定,这个协定有点长,外面规定了四种实现形式,有趣味的能够自行百度浏览,反正我素来没有读完过。不过目前各大网站的接入形式都是基本一致的,总结如下:

1. 去第三方网站的开放平台注册账号,填写利用信息,填写回调地址,获取一下 app keyapp secret

2. 在你的网站上点击第三方网站的图标或按钮后跳转到第三方提供的登录地址,带上 app key 以及上一步填写的回调地址,登录胜利后回跳转回回调地址页面,并带上一个code

3. 通过上一步获取到的 code 去申请第三方提供的接口获取令牌

4. 通过上一步获取到的令牌再去申请第三方提供的接口获取用户信息

接下来咱们以掘金上的第三方登录 github 账号来实现一下。

第一步去 github 上注册利用 https://github.com/settings/applications/new:

最初一个要输出的就是咱们的回调地址。

第二步在咱们的网站上增加第三方登录的按钮,个别都是应用对方的logo

点击后跳转到 github 的登录地址,掘金上点击后会弹出一个小窗口:

这能够应用 window.open 办法,不过有一些须要留神的点,如果只是简略的应用:

let url = `https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=http://xxx.com/`;
window.open(url)

默认下是间接新开一个 tab,而不是以小窗口的模式关上,想要以小窗口关上的话第三个参数不能为空,也就是你要设置一下新开窗口的款式:

window.open(url, '_blank', 'width=600, height=600')

然而经测试,浏览器全屏的状况下个别依然是新开一个 tab,并且各个浏览器的成果可能都不一样,所以不要期待能有统一的成果了。

看一下掘金登录时小窗口上的地址信息:

https://github.com/login?client_id=60483ab971aa5416e000&return_to=/login/oauth/authorize?client_id=60483ab971aa5416e000&redirect_uri=https://juejin.cn/passport/auth/login_success&scope=user:email&state=4b4b89193gASoVCgoVPZIGM4MDY0MzZmNjJlNDlhMTc1NjBmNjg1MDU3MWUxNWM2oU6-aHR0cHM6Ly9qdWVqaW4uY24vb2F1dGgtcmVzdWx0oVYBoUkAoUQAoUHRCjChTdEKMKFIqWp1ZWppbi5jbqFSBKJQTNEEFaZBQ1RJT06goUyyaHR0cHM6Ly9qdWVqaW4uY24voVTZIDEwNDlkOTIyYTE1YjUyOTdkMTA5NTk5M2UxZThiM2EwoVcAoUYAolNBAKFVww==

能够看到掘金的回调地址为:https://juejin.cn/passport/auth/login_success,另外还有几个参数,scope参数示意要求的受权范畴,这里示意掘金除了根底信息外还想获取用户的电子邮件地址,state是一个字符串,最初会一成不变的传回给你,能够用来判断是否被批改了,更多信息可参考 github 的开发文档:https://docs.github.com/cn/developers/apps/authorizing-oauth-apps。

如果用户登录胜利就会重定向到回调地址,然而问题来了,回调地址只能填写一个,然而在掘金的任何页面都能够进行登录,而且登录胜利后会主动刷新以后页面。

首先点击了第三方登录按钮后掘金会在 localStorage 上存储以后的登录发动页面的地址:

其次是监听子窗口的敞开,敞开了以后页面就进行刷新:

this.windowObj = window.open(url, '_blank', 'width=600, height=600')
this.onCloseCheck()

onCloseCheck() {if (!this.windowObj) {return}
    clearTimeout(this.closeCheckTimer)
    this.closeCheckTimer = setTimeout(() => {if(this.windowObj.closed) {location.reload()
            clearTimeout(this.closeCheckTimer)
            this.windowObj = null
        } else {this.onCloseCheck()
        }
    }, 500);
}

这样看起来这个存储的 url 仿佛并没有什么用,确实,扒了一下小窗口页面的源码发现了上面的这段代码:

能够发现存储的这个 url 只在微信环境下才用的到。然而如果你的登录页是 y

在回调地址页面获取到返回的 code 之后须要换取令牌,通过后端申请对应接口:

<?php

$code = $_POST['code'];
$data = array(
    'client_id' => 'xxx',
    'client_secret' => 'xxx',
    'code' => $code,
    'redirect_uri' => 'xxx'
);
// post 为一个发送 post 申请的办法,不是 php 的内置函数
post('https://github.com/login/oauth/access_token', $data);

获取到令牌就能够再去申请获取用户信息:

<?php

$header = array('Authorization: token' . $access_token, 'User-Agent: 现实青年实验室');
// get 为一个发送 get 申请的办法,不是 php 的内置函数
get('https://api.github.com/user', $header);

获取到用户信息就能够依据外面的用户惟一的 id 字段的值来创立账号、关联账号以及进行登录。

总结

本文简略记录了一下一个常见登录页面的一些知识点,存在谬误或平安问题的话还请指出,登录能够说的货色还有很多,比方如何实现免登录、扫码登录、单点登录、app 客户端等的登录等等,因为目前没有相干实际,所以也无从介绍,各位有趣味能够自行理解,再会。

退出移动版