使用Jsencrypt配合axios實(shí)現(xiàn)數(shù)據(jù)傳輸加密
本文轉(zhuǎn)載自微信公眾號(hào)「粥里有勺糖」,作者粥里有勺糖。轉(zhuǎn)載本文請(qǐng)聯(lián)系粥里有勺糖公眾號(hào)。
背景
不希望應(yīng)用發(fā)送的數(shù)據(jù)能在 Devtools 中被看到,避免接口被“同行”扒下來,然后被惡意使用
要避免此問題,首先想到的就是對(duì)傳輸?shù)臄?shù)據(jù)進(jìn)行一次加密,讓后端自行解密然后處理
盡管js源碼是被瀏覽器公開的,但通過構(gòu)建工具混淆后,在沒有source map的情況下還不不易定位目標(biāo)代碼
期望加密后的樣子傳輸?shù)膬?nèi)容如下
加密方案簡述
對(duì)稱加密
對(duì)稱加密就是兩邊擁有相同的秘鑰,兩邊都知道如何將密文加密解密。
這種加密方式固然很好,但是問題就在于如何讓雙方知道秘鑰。
由于傳輸數(shù)據(jù)都是走的網(wǎng)絡(luò),如果將秘鑰通過網(wǎng)絡(luò)的方式傳遞的話,一旦秘鑰被截獲就沒有加密的意義
非對(duì)稱加密
有公鑰私鑰之分:
- 公鑰所有人都可以知道,可以將數(shù)據(jù)用公鑰加密,但是將數(shù)據(jù)解密必須使用私鑰解密
- 私鑰只有分發(fā)放公鑰的一方才知道
這種加密方式就可以完美解決對(duì)稱加密存在的問題
通過對(duì)比,選用保密性好的 非對(duì)稱加密 方案作為加密方案
本文選用 RSA[1] 對(duì)稱加密算法
公私鑰生成
根據(jù)百度經(jīng)驗(yàn)的建議,生成一個(gè)1024位的的秘鑰
這里使用openssl生成,window下建議在Git Bash下使用
私鑰
- openssl genrsa -out rsa_1024_priv.pem 1024
公鑰
- openssl rsa -pubout -in rsa_1024_priv.pem -out rsa_1024_pub.pem
jsencrypt
- jsencrypt[2]
- nodejs-jsencrypt[3]
“使用 Javascript 進(jìn)行RSA加密的解決方案
使用
安裝依賴
- # web
- npm i jsencrypt
- # node
- npm i nodejs-jsencrypt
引入
- // web
- import JSEncrypt from 'jsencrypt'
- // node
- const { JSEncrypt } = require('nodejs-jsencrypt')
公鑰加密方法
- // 上述自動(dòng)生成
- const pubKey = '上述生成的公鑰'
- function publicEncrypt(str){
- const encrypt = new JSEncrypt()
- encrypt.setPublicKey(pubKey)
- return encrypt.encrypt(str)
- }
私鑰解密方法
- const privKey = `上述生成的私鑰`
- function privDecrypt(str) {
- const encrypt = new JSEncrypt()
- encrypt.setPrivateKey(privKey)
- return encrypt.decrypt(str)
- }
可以看出API非常簡潔
使用示例
- let str = publicEncrypt('hello world')
- console.log(str)
- console.log(privDecrypt(str))
結(jié)合Axios實(shí)踐
Axios配置
- npm i axios
將加密邏輯放入到axios的請(qǐng)求攔截器中,將原內(nèi)容使用 JSON.stringify處理后再進(jìn)行加密,加密后的內(nèi)容使用value屬性傳遞,如下所示
- import axios from "axios";
- // 引入剛剛編寫的加密方法
- import { publicEncrypt } from "./utils/crypto";
- const http = axios;
- http.defaults.baseURL = '/api'
- http.defaults.headers = {
- "content-Type": "application/json"
- };
- // 請(qǐng)求攔截器
- http.interceptors.request.use(
- config => {
- // 發(fā)送之前操作config
- // 對(duì)傳遞的 data 進(jìn)行加密
- config.data = {
- value:publicEncrypt(JSON.stringify(config.data))
- }
- return config;
- },
- err => {
- // 處理錯(cuò)誤
- return Promise.reject(err);
- }
- );
- http.interceptors.response.use(
- response => {
- // 返回前操作
- return response.data;
- },
- err => {
- return Promise.reject(err);
- }
- );
- export default http;
服務(wù)端解密示例代碼
這里列舉了兩種,一種直接使用Node.js的http模塊編寫,一種使用Express編寫:
- 解密收到的內(nèi)容
- 將解密后的內(nèi)容直接返回
http模塊示例
使用data事件與end事件配合,接收傳遞的數(shù)據(jù),然后進(jìn)行解密返回
- const http = require('http')
- // 引入解密方法
- const { privDecrypt } = require('./utils/crypto')
- const server = http.createServer((req, res) => {
- res.setHeader('content-type','application/json')
- let buffer = Buffer.alloc(0)
- // 接收傳遞的數(shù)據(jù)
- req.on('data',(chunk)=>{
- buffer = Buffer.concat([buffer, chunk])
- })
- req.on('end',()=>{
- try {
- // 解密傳遞的數(shù)據(jù)
- const data = privDecrypt(JSON.parse(buffer.toString('utf-8')).value)
- res.end(data)
- } catch (error) {
- console.log(error);
- res.end('error')
- }
- })
- })
- // 啟動(dòng)
- server.listen(3000, err => {
- console.log(`listen 3000 success`);
- })
Express示例
配置一個(gè)前置的*路由,解密傳遞的內(nèi)容,然后將其重新綁定到req.body上,供后續(xù)其它路由使用
- const express = require('express')
- const { privDecrypt } = require('./utils/crypto')
- const server = express()
- server.use(express.urlencoded({ extended: false }))
- server.use(express.json({ strict: true }))
- // 首先進(jìn)入的路由
- server.route('*').all((req, res, next) => {
- console.log(`${req.method}--${req.url}`)
- req.body = JSON.parse(privDecrypt(req.body.value))
- next()
- })
- server.post('/test/demo',(req,res)=>{
- // 直接返回實(shí)際的內(nèi)容
- res.json(req.body)
- })
- // 啟動(dòng)
- server.listen(3000, err => {
- console.log(`listen 3000 success`);
- })
前端代碼示例
使用了 Vite 作為開發(fā)預(yù)覽工具
vite.config.js配置: 只做了請(qǐng)求代理,解決開發(fā)跨域問題
- export default {
- server: {
- proxy: {
- '/api': {
- target: 'http://localhost:3000',
- changeOrigin: true,
- rewrite: (path) => path.replace(/^\/api/, '')
- },
- }
- }
- }
頁面
- <body>
- <button id="send">發(fā)送</button>
- <hr>
- <h2></h2>
- <textarea id="receive" placeholder="接收的內(nèi)容"></textarea>
- <script type="module" src="./index.js"></script>
- </body>
邏輯
- import $http from './http'
- const $send = document.getElementById('send')
- const $receive = document.getElementById('receive')
- $send.addEventListener('click',function(){
- // 發(fā)送一個(gè)隨機(jī)內(nèi)容
- $http.post('/test/demo',{
- name:'xm',
- age:~~(Math.random()*1000)
- }).then((res)=>[
- updateReceive(res)
- ])
- })
- function updateReceive(data){
- $receive.value = data instanceof Object?JSON.stringify(data):data
- }
運(yùn)行結(jié)果
頁面
發(fā)送網(wǎng)絡(luò)請(qǐng)求
請(qǐng)求響應(yīng)內(nèi)容
大功告成,接入十分簡單
完整的示例代碼倉庫[4]
外鏈
[1]RSA: https://baike.baidu.com/item/RSA%E7%AE%97%E6%B3%95/263310
[2]jsencrypt: https://www.npmjs.com/package/jsencrypt
[3]nodejs-jsencrypt: https://www.npmjs.com/package/nodejs-jsencrypt
[4]完整的示例代碼倉庫: https://github.com/ATQQ/demos/tree/main/asymmetric-encryption