avatar
如何保证登陆数据传输安全
👋

如何保证登陆数据传输安全

keywords
published_date
最新编辑 2024年10月09日
Created time
2021年12月24日
slug
本文讨论了如何确保登录数据传输的安全性,涵盖了前端存储方式、Cookie的配置及使用、服务端Session管理、Token机制、JWT、Refresh Token以及单点登录(SSO)的实现。重点强调了Cookie的安全配置(如Secure和HttpOnly属性)以及加密技术在数据传输中的重要性,提供了具体的代码示例和加密解密过程。
tags
JavaScript

背景

因为HTTP是无状态的,所以HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。
在学校或公司,入学入职那一天起,会录入你的身份、账户信息,然后给你发个卡,今后在园区内,你的门禁、打卡、消费都只需要刷这张卡。
前端的存储方式
  • 挂到全局变量上,但这是个「体验卡」,一次刷新页面就没了
  • 存到 cookie、localStorage 等里,这属于「会员卡」,无论怎么刷新,只要浏览器没清掉或者过期,就一直拿着这个状态。

Cookie

cookie 也是前端存储的一种,但相比于 localStorage 等其他方式,借助 HTTP 头、浏览器能力,cookie 可以做到前端无感知
  • 在提供标记的接口,通过 HTTP 返回头的 Set-Cookie 字段,直接「种」到浏览器上
  • 浏览器发起请求时,会自动把 cookie 通过 HTTP 请求头的 Cookie 字段,带给接口
    • notion image

配置项

配置:Domain / Path【空间范围】
你不能拿欢乐谷的票进迪士尼。
Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如 www.example.com 会设为 example.com,而且以后如果访问example.com的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。
Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是域名必须一致。
配置:Expires / Max-Age【时间范围】
你用过卡了就不能再进入迪士尼
Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式。如果不设置该属性,或者设为null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。
Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。
如果同时指定了Expires和Max-Age,那么Max-Age的值将优先生效。
如果Set-Cookie字段没有指定Expires或Max-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。
配置:Secure / HttpOnly【使用方式】
必须给这个卡绑定了身份证才能进入迪士尼
Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。
HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是Document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。

读写Cookie的方式

HTTP头对Cookie的读写
HTTP返回的一个 Set-Cookie 头用于向浏览器写入「一条(且只能是一条)」cookie,格式为 cookie 键值 + 配置键值。
Set-Cookie: username=jimu; domain=jimu.com; path=/blog; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
HTTP 请求的 Cookie 头用于浏览器把符合当前「空间、时间、使用方式」配置的所有 cookie 一并发给服务端。因为由浏览器做了筛选判断,就不需要归还配置内容了,只要发送键值就可以。
Cookie: username=jimu; height=180; weight=80
前端对 cookie 的读写
前端可以创建cookie,服务端创建的 cookie 没加HttpOnly,可以修改服务端给的 cookie
document.cookie = 'username=jimu; domain=jimu.com; path=/blog; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly'; console.log(document.cookie);// username=jimu; height=180; weight=80

服务端session

notion image
  • 浏览器登录发送账号密码,服务端查用户库,校验用户
  • 服务端把用户登录状态存为 Session,生成一个 sessionId
  • 通过登录接口返回,把 sessionId set 到 cookie 上
  • 此后浏览器再请求业务接口,sessionId 随 cookie 带上
  • 服务端查 sessionId 校验 session
  • 成功后正常做业务处理,返回结果
 
node.js下的Session处理,使用express-session中间件处理,主要实现了:
  • 封装了对cookie的读写操作,并提供配置项配置字段、加密方式、过期时间等。
  • 封装了对session的存取操作,并提供配置项配置session存储方式(内存/redis)、存储规则等。
  • 给req提供了session属性,控制属性的set/get并响应到cookie和session存取上,并给req.session提供了一些方法。

token

session 的维护给服务端造成很大困扰,我们必须找地方存放它,又要考虑分布式的问题,甚至要单独为了它启用一套 Redis 集群。
可以将用户信息全部打包到cookie中,服务端不用存了,只要检验cookie有效性
notion image
  • 用户登录,服务端校验账号密码,获得用户信息
  • 把用户信息、token 配置编码成 token,通过 cookie set 到浏览器
  • 此后用户请求业务接口,通过 cookie 携带 token
  • 接口校验 token 有效性,进行正常业务接口处理

编码方式

  • base64
  • node的cookie-session - npm

防篡改

给 token 加签名,来识别 token 是否被篡改过,cdd 虽然能伪造出用户信息的base64,但伪造不出 sig 的内容,因为他不知道 secret。
notion image

JWT

但上面的做法额外增加了 cookie 数量,数据本身也没有规范的格式。
JSON Web Token (JWT) 是一个开放标准,定义了一种传递 JSON 信息的方式。这些信息通过数字签名确保可信。
notion image

refresh token

access token:业务接口用来鉴权的 token
越是权限敏感的业务,我们越希望 access token 有效期足够短,以避免被盗用。但过短的有效期会造成 access token 经常过期,办法有
  • 让用户重新登录获取新 token,显然不够友好
  • 再来一个 token,一个专门生成 access token 的 token,我们称为 refresh toke
    • access token 用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活
    • refresh token 用来获取 access token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,也可以如前面的 session 一样处理
notion image
 

SSO

SSO 是英文 Single Sign On 的缩写,翻译过来就是单点登录。顾名思义,它把两个及以上个产品中的用户登录逻辑抽离出来,达到只输入一次用户名密码,就能同时登录多个产品的效果。
notion image
  • 在 SSO 域下,SSO 不是通过接口把 ticket 直接返回,而是通过一个带 code 的 URL 重定向到系统 A 的接口上,这个接口通常在 A 向 SSO 注册时约定
  • 浏览器被重定向到 A 域下,带着 code 访问了 A 的 callback 接口,callback 接口通过 code 换取 ticket
  • 这个 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只为了传一下换 ticket,换完就失效
  • callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
  • 在后续请求中,只需要把 cookie 中的 ticket 解析出来,去 SSO 验证就好访问 B 系统也是一样

Crypto.js加解密过程

HTTPS(443)在HTTP(80)的基础上加入了,SSL(Secure Sockets Layer 安全套接层)协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。传输前用公钥加密,服务器端用私钥解密。
对于使用http协议的web前端的加密,只能防君子不能防小人。前端是完全暴露的,包括你的加密算法。知道了加密算法,密码都是可以解出的,只是时间问题。而为了保证数据库中存储的密码更安全,则需要在后端用多种单向(非对称)加密手段混合进行加密存储。

对称加密算法

  • 前端使用 encrypted = encrypt(password+key),
  • 后端使用 password = decrypt(encrypted +key)
  • 前端只传输密码与key加密后的字符串encrypted ,这样即使请求被拦截了,也知道了加密算法,但是由于缺少key所以很难解出明文密码。所以这个key很关键。
  • 这个key是由后端控制生成与销毁的,用完即失效,所以即使可以模拟用加密后的密码来发请求模拟登录,但是key已经失效了,后端还是验证不过的。
  • 如果真要防,可以将加密算法的js文件进行压缩加密,不断更新的手段来使js文件难以获取,让黑客难以获取加密算法。变态的google就是这么干的,自己实现一个js虚拟机,通过不断更新加密混淆js文件让加密算法难以获取。这样黑客不知道加密算法就无法解出了。
常用的对称加密算法有 DES、3DES(TripleDES)、AES、RC2、RC4、RC5和Blowfis

Crypto.js使用AES方式加密

notion image
notion image
/** * Name: encode.js * Instro: * 1.加密: *     - 对sso返回来的json,转化成base64 *     - 根据制定加密算法类型和key创建加密对象 *     - 加密对象对base64转化成buffer进行加密 *     - buffer转化成字符串后,进行正则替换,得到结果 * 2.解密: *     - 放入加密结果 *     - 正则替换 *     - 基于key创建加密对象 *     - base64反解,获取结果 */ const Crypto = require('crypto'); const CryptoJs = require('crypto-js'); class SecreteAlgorithm {    constructor(key, cipher, json) {        this.key = key;        this.cipher = cipher;        this.json = json;   }    // buffer 加密    base64encode(s, urlsafe) {        if (!Buffer.isBuffer(s)) {        s = typeof Buffer.from === 'function' ? Buffer.from(s) : new Buffer(s);       }        var encode = s.toString('base64');        if (urlsafe) {        encode = encode.replace(/\+/g, '-').replace(/\//g, '_');       }        return encode;   };    // buffer解密    base64decode(encodeStr, urlsafe, encoding) {        if (urlsafe) {        encodeStr = encodeStr.replace(/\-/g, '+').replace(/_/g, '/');       }        var buf = typeof Buffer.from === 'function' ? Buffer.from(encodeStr, 'base64') : new Buffer(encodeStr, 'base64');        if (encoding === 'buffer') {            return buf;       }        return buf.toString(encoding || 'utf8');   };    // 1.加密信息    encrypt() {        const word = CryptoJs.enc.Utf8.parse(JSON.stringify(this.json));        const data = CryptoJs.enc.Base64.stringify(word)        const cipherW = Crypto.createCipher(this.cipher, this.key);                const text = cipherW.update(data, 'utf-8');        const pad = cipherW.final();        const encode = Buffer.concat([text, pad]);        const result = this.base64encode(encode, true);                return result;   }    // 2.解密信息    decrypt(data) {        if(!this.key) {            const keys = [this.key];            for (let i = 0; i < keys.concat.length; i++) {                const value = decrypt(data, keys[i]);                if(value !== false) return { value, index: i};           }            return false;       }        try {            const deRes = this.base64decode(data, true, 'buffer');            const ciphers = Crypto.createDecipher(this.cipher, this.key);            const text = ciphers.update(deRes, 'utf-8');            const pad = ciphers.final();            const decode = Buffer.concat([text, pad]);            const re = decode.toString();                        const result = CryptoJs.enc.Base64.parse(re).toString(CryptoJs.enc.Utf8);            return result;       }catch(err) {            console.log('err', err);            return false;       }   } } // 赋值 // key(盐值), cipher为固定值 const key = 'FE_Cock.cn'; const cipher = 'aes-256-cbc'; // json为sso返回值 const json = {    "sso_cock#user":"xxxx",    "_expire":1635413382104,    "_maxAge":172800000 }; // 使用方法 const scr = new SecreteAlgorithm(key, cipher, json); const encode = scr.encrypt(); const tmp = 'fnH5wvRhO4eUS6-6pcu2mieOs8eTj3_NQbRuB9K87Gytg9UP-1p3bogIvtC3ikQ73lGyWfnh1rHhVM2z-sXkX3hgoO8p1LlTFpSdzr3HMVtJ08bZNRa1AwmFUXv7xJKMpZibWCjKoaRrF3dSMdSiwA=='; const decode = scr.decrypt(tmp); // 调试方法 console.log('\x1B[45m Final Result: \x1B[0m', encode); console.log('\x1B[46m Decode Result: \x1B[0m', decode);

原文链接🔗

关于我

My desire to practice my skills and share my acquired knowledge fuels my endeavors.

联系 : znjessie858@gmail.com

订阅

订阅我的文章,收到我的最新发布通知,可随时取消订阅!