还在直接用localStorage么?全网最细:本地存储二次封装(含加密、解密、过期处理)

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 很多人在用 localStorage 或 sessionStorage 的时候喜欢直接用,明文存储,直接将信息暴露在;浏览器中,虽然一般场景下都能应付得了且简单粗暴,但特殊需求情况下,比如设置定时功能,就不能实现。就需要对其进行二次封装,为了在使用上增加些安全感,那加密也必然是少不了的了。为方便项目使用,特对常规操作进行封装。

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

背景

很多人在用 localStoragesessionStorage 的时候喜欢直接用,明文存储,直接将信息暴露在;浏览器中,虽然一般场景下都能应付得了且简单粗暴,但特殊需求情况下,比如设置定时功能,就不能实现。就需要对其进行二次封装,为了在使用上增加些安全感,那加密也必然是少不了的了。为方便项目使用,特对常规操作进行封装。

结构设计

在封装一系列操作本地存储的API之前,先准备了一个全局对象,对具体的操作进行判断,如下:

interface globalConfig {
   
   
  type: 'localStorage' | 'sessionStorage';
  prefix: string;
  expire: number;
  isEncrypt: boolean;
}

const config: globalConfig = {
   
   
  type: 'localStorage',              //存储类型,localStorage | sessionStorage
  prefix: 'react-view-ui_0.0.1',     //版本号
  expire: 24 * 60,                   //过期时间,默认为一天,单位为分钟
  isEncrypt: true,                   //支持加密、解密数据处理
};
  1. type 表示存储类型,为 localStoragesessionStorage
  2. prefix 表示视图唯一标识,如果配置可在浏览器视图中放在前缀显示;
  3. expire 表示过期时间,默认为一天,单位为分钟;
  4. isEncrypt 表示支持加密、解密数据处理;

加密准备工作

这里是用到了 crypto-js 来处理加密和解密,可先下载包并导入。

npm i --save-dev crypto-js

import CryptoJS from 'crypto-js';

crypto-js 设置密钥和密钥偏移量,可以采用将一个私钥经 MD5 加密生成16位密钥获得。

const SECRET_KEY = CryptoJS.enc.Utf8.parse('3333e6e143439161'); //十六位十六进制数作为密钥
const SECRET_IV = CryptoJS.enc.Utf8.parse('e3bbe7e3ba84431a'); //十六位十六进制数作为密钥偏移量

加密

const encrypt = (data: object | string): string => {
   
   
  //加密
  if (typeof data === 'object') {
   
   
    try {
   
   
      data = JSON.stringify(data);
    } catch (e) {
   
   
      throw new Error('encrypt error' + e);
    }
  }
  const dataHex = CryptoJS.enc.Utf8.parse(data);
  const encrypted = CryptoJS.AES.encrypt(dataHex, SECRET_KEY, {
   
   
    iv: SECRET_IV,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.ciphertext.toString();
};

解密

const decrypt = (data: string) => {
   
   
  //解密
  const encryptedHexStr = CryptoJS.enc.Hex.parse(data);
  const str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
  const decrypt = CryptoJS.AES.decrypt(str, SECRET_KEY, {
   
   
    iv: SECRET_IV,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
  return decryptedStr.toString();
};

这两个API都是将获取到的本地存储的value作为参数进行传递,这样就实现了加密和解密。

在传入数据进行处理、改变的时候需要进行解密;
在数据需要传出时需要进行加密。

核心API实现

setStorage 设置值

Storage 本身是不支持过期时间设置的,要支持设置过期时间,可以效仿 Cookie 的做法,setStorage(key, value, expire) 方法,接收三个参数,第三个参数就是设置过期时间的,用相对时间,单位分钟,要对所传参数进行类型检查。可以设置统一的过期时间,也可以对单个值得过期时间进行单独配置。

const setStorage = (key: string, value: any, expire: number = 24 * 60): boolean => {
   
   
  //设定值
  if (value === '' || value === null || value === undefined) {
   
   
    //空值重置
    value = null;
  }
  if (isNaN(expire) || expire < 0) {
   
   
    //过期时间值合理性判断
    throw new Error('Expire must be a number');
  }
  const data = {
   
   
    value, //存储值
    time: Date.now(), //存储日期
    expire: Date.now() + 1000 * 60 * expire, //过期时间
  };
  //是否需要加密,判断装载加密数据或原数据
  window[config.type].setItem(
    autoAddPreFix(key),
    config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data),
  );
  return true;
};

getStorageFromKey 根据key获取value

首先要对 key 是否存在进行判断,防止获取不存在的值而报错。对获取方法进一步扩展,只要在有效期内就可以获取 Storage 值,如果过期则直接删除该值,并返回 null。

const getStorageFromKey = (key: string) => {
   
   
  //获取指定值
  if (config.prefix) {
   
   
    key = autoAddPreFix(key);
  }
  if (!window[config.type].getItem(key)) {
   
   
    //不存在判断
    return null;
  }
  const storageVal = config.isEncrypt
    ? JSON.parse(decrypt(window[config.type].getItem(key) as string))
    : JSON.parse(window[config.type].getItem(key) as string);
  const now = Date.now();
  if (now >= storageVal.expire) {
   
   
    //过期销毁
    removeStorageFromKey(key);
    return null;
    //不过期回值
  } else {
   
   
    return storageVal.value;
  }
};

getAllStorage 获取所有存储值

const getAllStorage = () => {
   
   
  //获取所有值
  const storageList: any = {
   
   };
  const keys = Object.keys(window[config.type]);
  keys.forEach((key) => {
   
   
    const value = getStorageFromKey(key);
    if (value !== null) {
   
   
      //如果值没有过期,加入到列表中
      storageList[key] = value;
    }
  });
  return storageList;
};

getStorageLength 获取存储值数量

const getStorageLength = () => {
   
   
  //获取值列表长度
  return window[config.type].length;
};

removeStorageFromKey 根据key删除存储值

const removeStorageFromKey = (key: string) => {
   
   
  //删除值
  if (config.prefix) {
   
   
    key = autoAddPreFix(key);
  }
  window[config.type].removeItem(key);
};

clearStorage 清空存储列表

const clearStorage = () => {
   
   
  window[config.type].clear();
};

autoAddPreFix 基于全局配置的prefix参数添加前缀

const autoAddPreFix = (key: string) => {
   
   
  //添加前缀,保持浏览器Application视图唯一性
  const prefix = config.prefix || '';
  return `${prefix}_${key}`;
};

这是一个不导出的函数,作为整体封装的内部工具函数,在setStorage、getStorageFromKey、removeStorageFromKey会使用到。

导出函数列表

提供了6个函数的处理能力,足够应对实际业务的大部分操作。

export {
   
   
  setStorage,
  getStorageFromKey,
  getAllStorage,
  getStorageLength,
  removeStorageFromKey,
  clearStorage,
};

使用

在实际业务中使用,则将函数导入即可,这里先看下笔者的文件目录吧:
在这里插入图片描述
实际使用:

import {
   
   
  setStorage,
  getStorageFromKey,
  getAllStorage,
  getStorageLength,
  removeStorageFromKey,
  clearStorage
} from '../../_util/storage/config'

  setStorage('name', 'fx', 1)
  setStorage('age', {
   
    now: 18 }, 100000)
  setStorage('history', [1, 2, 3], 100000)
  console.log(getStorageFromKey('name'))
  removeStorageFromKey('name')
  console.log(getStorageFromKey('name'))
  console.log(getStorageLength());
  console.log(getAllStorage());
  clearStorage();

接下来看一下浏览器视图:

在这里插入图片描述

可以看到,key经过处理加入了config.prefix的前缀,有了唯一性。
value经过了加密处理。

再看一下通过get方式获取到的控制台值输出:

在这里插入图片描述

很完美,实际业务会把前缀清除返回进行处理,视图中有前缀绑定以及加密处理,保证了本地存储的安全性。

完整代码

config.ts:

import {
   
    encrypt, decrypt } from './encry';
import {
   
    globalConfig } from './interface';

const config: globalConfig = {
   
   
  type: 'localStorage', //存储类型,localStorage | sessionStorage
  prefix: 'react-view-ui_0.0.1', //版本号
  expire: 24 * 60, //过期时间,默认为一天,单位为分钟
  isEncrypt: true, //支持加密、解密数据处理
};

const setStorage = (key: string, value: any, expire: number = 24 * 60): boolean => {
   
   
  //设定值
  if (value === '' || value === null || value === undefined) {
   
   
    //空值重置
    value = null;
  }
  if (isNaN(expire) || expire < 0) {
   
   
    //过期时间值合理性判断
    throw new Error('Expire must be a number');
  }
  const data = {
   
   
    value, //存储值
    time: Date.now(), //存储日期
    expire: Date.now() + 1000 * 60 * expire, //过期时间
  };
  //是否需要加密,判断装载加密数据或原数据
  window[config.type].setItem(
    autoAddPreFix(key),
    config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data),
  );
  return true;
};

const getStorageFromKey = (key: string) => {
   
   
  //获取指定值
  if (config.prefix) {
   
   
    key = autoAddPreFix(key);
  }
  if (!window[config.type].getItem(key)) {
   
   
    //不存在判断
    return null;
  }

  const storageVal = config.isEncrypt
    ? JSON.parse(decrypt(window[config.type].getItem(key) as string))
    : JSON.parse(window[config.type].getItem(key) as string);
  const now = Date.now();
  if (now >= storageVal.expire) {
   
   
    //过期销毁
    removeStorageFromKey(key);
    return null;
    //不过期回值
  } else {
   
   
    return storageVal.value;
  }
};
const getAllStorage = () => {
   
   
  //获取所有值
  const storageList: any = {
   
   };
  const keys = Object.keys(window[config.type]);
  keys.forEach((key) => {
   
   
    const value = getStorageFromKey(autoRemovePreFix(key));
    if (value !== null) {
   
   
      //如果值没有过期,加入到列表中
      storageList[autoRemovePreFix(key)] = value;
    }
  });
  return storageList;
};
const getStorageLength = () => {
   
   
  //获取值列表长度
  return window[config.type].length;
};
const removeStorageFromKey = (key: string) => {
   
   
  //删除值
  if (config.prefix) {
   
   
    key = autoAddPreFix(key);
  }
  window[config.type].removeItem(key);
};
const clearStorage = () => {
   
   
  window[config.type].clear();
};
const autoAddPreFix = (key: string) => {
   
   
  //添加前缀,保持唯一性
  const prefix = config.prefix || '';
  return `${prefix}_${key}`;
};
const autoRemovePreFix = (key: string) => {
   
   
  //删除前缀,进行增删改查
  const lineIndex = config.prefix.length + 1;
  return key.substr(lineIndex);
};

export {
   
   
  setStorage,
  getStorageFromKey,
  getAllStorage,
  getStorageLength,
  removeStorageFromKey,
  clearStorage,
};

encry.ts:

import CryptoJS from 'crypto-js';

const SECRET_KEY = CryptoJS.enc.Utf8.parse('3333e6e143439161'); //十六位十六进制数作为密钥
const SECRET_IV = CryptoJS.enc.Utf8.parse('e3bbe7e3ba84431a'); //十六位十六进制数作为密钥偏移量

const encrypt = (data: object | string): string => {
   
   
  //加密
  if (typeof data === 'object') {
   
   
    try {
   
   
      data = JSON.stringify(data);
    } catch (e) {
   
   
      throw new Error('encrypt error' + e);
    }
  }
  const dataHex = CryptoJS.enc.Utf8.parse(data);
  const encrypted = CryptoJS.AES.encrypt(dataHex, SECRET_KEY, {
   
   
    iv: SECRET_IV,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.ciphertext.toString();
};

const decrypt = (data: string) => {
   
   
  //解密
  const encryptedHexStr = CryptoJS.enc.Hex.parse(data);
  const str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
  const decrypt = CryptoJS.AES.decrypt(str, SECRET_KEY, {
   
   
    iv: SECRET_IV,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
  return decryptedStr.toString();
};

export {
   
    encrypt, decrypt };

interface.ts:

interface globalConfig {
   
   
  type: 'localStorage' | 'sessionStorage';
  prefix: string;
  expire: number;
  isEncrypt: boolean;
}

export type {
   
    globalConfig };

总结

前端开发中直接使用明文存储在本地是比较常见的一件事情同时也是不安全的一件事,对本地存储进行二次封装可以提高安全性,并且有了API的支持,可以在本地存储操作时更加简单。

目录
相关文章
|
2月前
|
存储 Java 数据库
密码专辑:对密码加盐加密,对密码进行md5加密,封装成密码工具类
这篇文章介绍了如何在Java中通过加盐和加密算法(如MD5和SHA)安全地存储密码,并提供了一个密码工具类PasswordUtils和密码编码类PasswordEncoder的实现示例。
43 10
密码专辑:对密码加盐加密,对密码进行md5加密,封装成密码工具类
|
7月前
|
JSON Java 数据安全/隐私保护
java中的http请求的封装(GET、POST、form表单、JSON形式、SIGN加密形式)
java中的http请求的封装(GET、POST、form表单、JSON形式、SIGN加密形式)
557 1
|
数据安全/隐私保护
rsa 生成密匙 ,密匙导出,加密解密封装
rsa 生成密匙 ,密匙导出,加密解密封装
205 0
|
JSON 运维 监控
接口测试框架实战 | 流程封装与基于加密接口的测试用例设计
接口测试仅仅掌握 Requests 或者其他一些功能强大的库的用法,是远远不够的,还需要具备能根据公司的业务流程以及需求去定制化一个接口自动化测试框架的能力。所以,接下来,我们主要介绍下接口测试用例分析以及通用的流程封装是如何完成的。 首先在做用例分析之前,可以通过追查公司一年来所有的故障原因,定位问题起因,或者通过与 CTO、产品经理、研发、运维、测试调查,得到质量痛点,还可以分析业务架构、
|
JSON 运维 监控
接口测试框架实战 | 流程封装与基于加密接口的测试用例设计
接口测试框架实战 | 流程封装与基于加密接口的测试用例设计
|
JSON 运维 监控
接口测试框架实战 | 流程封装与基于加密接口的测试用例设计
接口测试仅仅掌握 Requests 或者其他一些功能强大的库的用法,是远远不够的,还需要具备能根据公司的业务流程以及需求去定制化一个接口自动化测试框架的能力。所以,接下来,我们主要介绍下接口测试用例分析以及通用的流程封装是如何完成的。 首先在做用例分析之前,可以通过追查公司一年来所有的故障原因,定位问题起因,或者通过与 CTO、产品经理、研发、运维、测试调查,得到质量痛点,还可以分析业务架构、
|
数据安全/隐私保护
localStorage在不同页面之间的设置值与取值--加密 localStorage与解密localStorage
localStorage在不同页面之间的设置值与取值--加密 localStorage与解密localStorage
|
设计模式 JSON 算法
接口测试框架实战 | 流程封装与基于加密接口的测试用例设计
接口测试仅仅掌握 Requests 或者其他一些功能强大的库的用法,是远远不够的,还需要具备能根据公司的业务流程以及需求去定制化一个接口自动化测试框架的能力。所以,接下来,我们主要介绍下接口测试用例分析以及通用的流程封装是如何完成的。
接口测试框架实战 | 流程封装与基于加密接口的测试用例设计
|
数据安全/隐私保护
仿抖音短视频APP源码,AES加密工具类封装
仿抖音短视频APP源码,AES加密工具类封装
682 0
|
10天前
|
存储 SQL 安全
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
随着互联网的普及,网络安全问题日益突出。本文将介绍网络安全的重要性,分析常见的网络安全漏洞及其危害,探讨加密技术在保障网络安全中的作用,并强调提高安全意识的必要性。通过本文的学习,读者将了解网络安全的基本概念和应对策略,提升个人和组织的网络安全防护能力。