Cookies 和localStorage存储比对
实现记住密码功能时,使用 Cookies 和使用 localStorage 各有其优势和考虑因素,具体需要取决于需求和安全考量:
1、Cookies 的优势:
广泛支持:Cookies 是 HTTP 协议的一部分,因此在几乎所有的浏览器和 Web 应用程序中都有良好的支持。
服务器端处理:可以通过设置 Cookie 的过期时间来实现自动登录功能,因为浏览器会在过期时间之前自动发送该 Cookie。这使得记住密码的实现变得相对简单。
可配置性:可以通过设置 Cookie 的属性(如过期时间、域名、路径等)来控制其在不同情况下的行为,例如只在 HTTPS 连接中发送、只在特定域名下可用等。
与服务器端状态同步:在某些情况下,特别是涉及到跨站点状态共享或服务器端状态跟踪时,Cookies 可以更自然地与服务器端的状态同步。
2、localStorage 的优势:
本地存储:localStorage 存储在用户的浏览器中,相比于 Cookies,更难以通过网络请求访问,从而提供了更好的安全性。
容量较大:localStorage 的存储容量通常比 Cookies 大得多(大约5MB左右),这使得它更适合存储大量的用户信息或配置数据。
更简单的 API:使用 localStorage 比起操作 Cookies 更加简单和直观,因为它提供了简洁的 key-value 存储接口。
3、综合考虑:
安全性:对于敏感数据如密码,localStorage 通常被认为比 Cookies 更安全,因为它不会在每个 HTTP 请求中自动发送给服务器。
便捷性:Cookies 更适合需要与服务器端状态同步或通过 HTTP 请求进行状态管理的场景,而 localStorage 更适合客户端本地的长期数据存储。
合规性:在 GDPR 等隐私法规的要求下,需要谨慎处理用户数据。Cookies 的使用受到更严格的监管和规范。
综上所述,选择使用 Cookies 还是 localStorage 实现记住密码功能取决于具体需求,特别是安全性、数据大小和与服务器的交互方式等方面的考量。
4、用户登录信息加密
因为每个 Cookie 的大小限制在 4KB 到 8KB 之间。这包括了 Cookie 的名称、值以及其他元数据(如域名、路径、过期时间等)。浏览器对单个域名可以存储的 Cookie 数量也有限制,通常在几百个到几千个之间。
所以对用户信息进行前端cookies进行存储时,需要提取关键信息,此方案实现只对用户密码进行公钥加密,对用户其他的非关键信息进行自定义加密函数。
5、主要加密函数示例
// auth.js import Cookies from 'js-cookie'; import CryptoJS from 'crypto-js'; const Base64 = require('js-base64').Base64; const EncryptionKey = 'encryptionKey'; // 加密 export const encrypt = word => { if (word == '' || word === undefined) { return ''; } word = Base64.encode(word); var key = CryptoJS.enc.Utf8.parse(EncryptionKey); var srcs = CryptoJS.enc.Utf8.parse(word); var encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return Base64.encode(encrypted.toString()); }; // 解密 export const decrypt = word => { if (word == '' || word === undefined) { return ''; } word = Base64.decode(word); var key = CryptoJS.enc.Utf8.parse(EncryptionKey); var decrypt = CryptoJS.AES.decrypt(word, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return Base64.decode(CryptoJS.enc.Utf8.stringify(decrypt).toString()); }; export function setCookie(name, value = '', seconds) { let expires = new Date(new Date() * 1 + seconds * 1000); Cookies.set(name, value, { expires }); } export function getCookieByName(name) { return Cookies.get(name); } // 设置记住密码信息 export function setRememberInfo(info) { const newInfo = JSON.parse(JSON.stringify(info)); const ecKey = 'abcdefg1234567xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // 公钥加密KEY newInfo.pwd = encryptFn(ecKey, newInfo.pwd); const encryptInfo = encrypt(JSON.stringify(newInfo)); setCookie('rememberInfo_' + newInfo.deptId, encryptInfo, 2592000); } // 公钥密码加密 export function encryptFn(publicKey, data) { const crypto = require('crypto'); // 使用公钥加密 const publicKeyStr = '-----BEGIN PUBLIC KEY-----\n' + publicKey + '\n' + '-----END PUBLIC KEY-----'; const encryptStr = crypto.publicEncrypt( { key: publicKeyStr, padding: crypto.constants.RSA_PKCS1_PADDING }, Buffer.from(data, 'utf8') ); const encrypted = encryptStr.toString('base64'); return encrypted; }
6、逻辑处理
1、在用户登录判断是否选了记住密码,如果勾选进行正常流程登录,并将用户信息记录用加密函数对主要信息进行信息加密存储在cookies中,看需要设置过期时间等其他cookies配置项。
2、用户重新进入登录页时、判断cookies中是否存在对应的用户信息,如果存在进行解密。拿出用户信息。
3、拿到的用户信息存在公钥加密的关键信息,比如用户密码,需要后端提供接口传递用户信息参数,后端使用私钥解密,校验用户信息是否匹配。如果匹配,一般接口返回包含token的成功响应。
4、已经拿到token了,就可以进行正常登录的后续流程实现自动登录了。
5、退出登录时,需要清除对应Cookies。
7、自动登录流程图
8、完整代码示例
只包含关键处理
<template> <!-- 登录首页 --> <div> <!-- 登录表单 --> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" auto-complete="on" label-position="left"> <el-form-item prop="userName"> <el-input placeholder="请输入用户名" v-model="loginForm.userName"> <i slot="prefix" class="el-input__icon iconfont icon-yonghu-filled"></i> </el-input> </el-form-item> <el-form-item prop="password"> <el-input placeholder="请输入密码" type="password" v-model="loginForm.password"> <i slot="prefix" class="el-input__icon iconfont icon-mima-filled"></i> </el-input> </el-form-item> <el-form-item prop="captcha" v-if="captchaType == '2'"> <el-input placeholder="请输入验证码" v-model="loginForm.captcha" maxlength="10"> <i slot="prefix" class="el-input__icon iconfont icon-yanzheng-filled"></i> <img class="image_code" slot="suffix" :src="verifyKey" alt /> </el-input> </el-form-item> </el-form> <div> <el-checkbox v-model="isRemember">记住密码</el-checkbox> </div> <el-button type="primary" @click.prevent="loginRequest()">登录</el-button> </div> </template> <script> import { loginRequest } from '@/api/user'; import { setToken, decrypt, encrypt, setRememberInfo } from '@/utils/auth'; import { getLoginInfoByRember } from '@/api/public/index'; export default { data() { return { deptId: null, loginForm: { userName: '', password: '', captchaKey: '', captcha: '', keyId: '' }, loginRules: { userName: [{ required: true, trigger: 'blur', message: '请输入用户名' }], password: [{ required: true, trigger: 'blur', message: '请输入密码' }] }, publicKey: null, isRemember: false, //是否勾选记住密码 userParams: {} // 需要存储的登录信息 }; }, created() { this.deptId = this.$route.query.deptId; const rmInfo = this.getCookieByName('rememberInfo_' + this.deptId); if (rmInfo) { this.isRemember = true; this.getRememberInfo(rmInfo); } }, methods: { // 根据记住密码获取登录信息 getRememberInfo(remberInfo) { remberInfo = JSON.parse(decrypt(remberInfo)); this.loginForm.userName = remberInfo.userName; const params = { userName: remberInfo.userName, password: remberInfo.pwd, // remberInfo.pwd是使用公钥加密的 deptId: this.deptId // ...... }; getLoginInfoByRember(params) // 此请求接口为后端提供、对前端加密的密码进行验证 .then(res => { // 校验成功、处理登录逻辑 // 一般返回携带Token的响应,例如{ result:token:"2e7d220f2f60492b8835189d2d6f3463", .... } this.loginAfterOperate(res); }) .catch(error => { // 校验失败、处理失败逻辑 this.loginErrorOperate(error); }); }, // 获取加密key initKey() { return initKey().then(res => { if (res.code === 1000) { this.publicKey = res.result.publicKey; } }); }, // 正常登录处理 async loginRequest() { await this.initKey(); const params = { userName: this.loginForm.userName.trim(), password: encrypt(this.publicKey, this.loginForm.password) // 其他登录参数...... }; loginRequest(params) .then(res => { if (this.isRemember) { this.userParams = { ...params, pwd: this.loginForm.password }; setRememberInfo(this.userParams); // 存储用户登录信息Cookies } this.loginAfterOperate(res); }) .catch(error => { this.loginErrorOperate(error); }); }, // 统一登录后处理 async loginAfterOperate(res, params = null) { const { code, result } = res; if (code == 1000) { setToken(result.token); // 后续登录逻辑...... } }, // 统一登录异常处理 async loginErrorOperate(error) { this.$message({ showClose: true, message: error.message, type: 'error', offset: 100 }); // 其他逻辑...... } } }; </script>