vue?項(xiàng)目?jī)?yōu)雅的對(duì)url參數(shù)加密詳解
實(shí)現(xiàn)方案:stringifyQuery 和 parseQuery
近期因?yàn)楣緝?nèi)部的安全檢查,說我們現(xiàn)在的系統(tǒng)中參數(shù)是明文的,包括給后端請(qǐng)求的參數(shù)和前端頁(yè)面跳轉(zhuǎn)攜帶的參數(shù),因?yàn)槭枪緝?nèi)部使用的系統(tǒng),在安全性方面的設(shè)計(jì)考慮確實(shí)不夠充分
對(duì)于參數(shù)的加密和解密很好實(shí)現(xiàn),直接采用常用的 AES 算法,前后端定義好通用的密鑰和加解密方式就好,前端加解密這里主要使用到 crypto-js 這個(gè)工具包,再通過一個(gè)類簡(jiǎn)單封裝一下加解密的算法即可
// src\utils\cipher.ts import { encrypt, decrypt } from 'crypto-js/aes' import { parse } from 'crypto-js/enc-utf8' import pkcs7 from 'crypto-js/pad-pkcs7' import ECB from 'crypto-js/mode-ecb' import UTF8 from 'crypto-js/enc-utf8' // 注意 key 和 iv 至少都需要 16 位 const AES_KEY = '1111111111000000' const AES_IV = '0000001111111111' export class AesEncryption { private key private iv constructor(key = AES_KEY, iv = AES_IV) { this.key = parse(key) this.iv = parse(iv) } get getOptions() { return { mode: ECB, padding: pkcs7, iv: this.iv, } } encryptByAES(text: string) { return encrypt(text, this.key, this.getOptions).toString() } decryptByAES(text: string) { return decrypt(text, this.key, this.getOptions).toString(UTF8) } }
對(duì)于前端頁(yè)面間跳轉(zhuǎn)攜帶參數(shù),我們項(xiàng)目使用的都是 vue-router 的 query 來攜帶參數(shù),但是有那么多頁(yè)面跳轉(zhuǎn)的地方,不可能都手動(dòng)添加加解密方法處理吧,工作量大不說,萬一漏改一個(gè)就可能導(dǎo)致整個(gè)頁(yè)面無法加載了,這鍋可不能背
首先想到的方法是在路由守衛(wèi) beforeEach
中對(duì)參數(shù)進(jìn)行加密,然后在 afterEach
守衛(wèi)中對(duì)參數(shù)進(jìn)行解密,但是這個(gè)想法在 beforeEach
中加密就無法實(shí)現(xiàn)。原因是 beforeEach(to, from, next)
的第三個(gè)參數(shù) next
函數(shù)中,如果參數(shù)是路由對(duì)象,會(huì)導(dǎo)致跳轉(zhuǎn)死循環(huán)
接下來經(jīng)過幾個(gè)小時(shí)百思不得其解(摸魚)之后,最終在 API 參考 | Vue Router (vuejs.org) 找到這樣兩個(gè) API:stringifyQuery
和 parseQuery
,官網(wǎng)的定義如下
stringifyQuery:對(duì)查詢對(duì)象進(jìn)行字符串化的自定義實(shí)現(xiàn)。不應(yīng)該在前面加上 ?
。應(yīng)該正確編碼查詢鍵和值
parseQuery:用于解析查詢的自定義實(shí)現(xiàn)。必須解碼查詢鍵和值
比如,官網(wǎng)建議如果想使用 qs 包來解析查詢,可以這樣配置
import qs from 'qs' createRouter({ // 其他配置... parseQuery: qs.parse, stringifyQuery: qs.stringify, })
現(xiàn)在最終的解決方案就很明確了,自定義兩個(gè)參數(shù)加密、解密的方法,然后在 createRouter
中添加到 stringifyQuery
和 parseQuery
這兩個(gè)方法就可以了,下面是詳細(xì)代碼
// src/router/helper/query.js import { isArray, isNull, isUndefined } from 'lodash-es' import { AesEncryption } from '@/utils/cipher' import type { LocationQuery, LocationQueryRaw, LocationQueryValue, } from 'vue-router' const aes = new AesEncryption() /** * * @description 解密:反序列化字符串參數(shù) */ export function stringifyQuery(obj: LocationQueryRaw): string { if (!obj) return '' const result = Object.keys(obj) .map((key) => { const value = obj[key] if (isUndefined(value)) return '' if (isNull(value)) return key if (isArray(value)) { const resArray: string[] = [] value.forEach((item) => { if (isUndefined(item)) return if (isNull(item)) { resArray.push(key) } else { resArray.push(key + '=' + item) } }) return resArray.join('&') } return `${key}=${value}` }) .filter((x) => x.length > 0) .join('&') return result ? `?${aes.encryptByAES(result)}` : '' } /** * * @description 解密:反序列化字符串參數(shù) */ export function parseQuery(query: string): LocationQuery { const res: LocationQuery = {} query = query.trim().replace(/^(\?|#|&)/, '') if (!query) return res query = aes.decryptByAES(query) query.split('&').forEach((param) => { const parts = param.replace(/\+/g, ' ').split('=') const key = parts.shift() const val = parts.length > 0 ? parts.join('=') : null if (!isUndefined(key)) { if (isUndefined(res[key])) { res[key] = val } else if (isArray(res[key])) { ;(res[key] as LocationQueryValue[]).push(val) } else { res[key] = [res[key] as LocationQueryValue, val] } } }) return res } // src/router/index.js // 創(chuàng)建路由使用加解密方法 import { parseQuery, stringifyQuery } from './helper/query' export const router = createRouter({ // 創(chuàng)建一個(gè) hash 歷史記錄。 history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH), routes: basicRoutes, scrollBehavior: () => ({ left: 0, top: 0 }), stringifyQuery, // 序列化query參數(shù) parseQuery, // 反序列化query參數(shù) })
加密的效果如下,我也在 github 上傳了加密方式的 demo,可以直接下載體驗(yàn)一下
更進(jìn)一步:相關(guān)實(shí)現(xiàn)原理
在實(shí)現(xiàn)完這兩個(gè)功能之后,我突然想翻一下 Vue Router 的源碼,看一下 stringifyQuery
和 parseQuery
的實(shí)現(xiàn)原理,避免以后遇到類似的問題再抓瞎
打開 Vue Router@4的源碼,整個(gè)項(xiàng)目是用 pnpm 管理 monorepo 的方式組織,通過 rollup.config.js 中定義的 input
入口可以知道,所有的方法都通過 packages/router/src/index.ts 導(dǎo)出
首先先看初始化路由實(shí)例的 createRouter
方法,這個(gè)方法主要做了這么幾件事
- 通過
createRouterMatcher
方法,根據(jù)路由配置列表創(chuàng)建 matcher,返回 5 個(gè)操作 matcher 方法。matcher 可以理解為路由頁(yè)面匹配器,包含路由所有信息和 crud 操作方法 - 定義三個(gè)路由守衛(wèi):beforeEach、beforeResolve、afterEach
- 聲明當(dāng)前路由 currentRoute,對(duì) url 參數(shù) paramas 進(jìn)行編碼處理
- 添加路由的各種操作方法,最后返回一個(gè) router 對(duì)象
一個(gè)簡(jiǎn)化版本的 createRouter
方法如下所示,前文使用到的 stringifyQuery
和 parseQuery
都是在這個(gè)方法中加載
export function createRouter(options: RouterOptions): Router { // 創(chuàng)建路由匹配器 matcher const matcher = createRouterMatcher(options.routes, options) // ! 使用到的 stringifyQuery 和 parseQuery const parseQuery = options.parseQuery || originalParseQuery const stringifyQuery = options.stringifyQuery || originalStringifyQuery // ! 路由守衛(wèi)定義 const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>() const afterGuards = useCallbacks<NavigationHookAfter>() // 聲明當(dāng)前路由 const currentRoute = shallowRef<RouteLocationNormalizedLoaded>( START_LOCATION_NORMALIZED ) let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED // leave the scrollRestoration if no scrollBehavior is provided if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) { history.scrollRestoration = 'manual' } // url 參數(shù)進(jìn)行編碼處理 const normalizeParams = applyToParams.bind( null, (paramValue) => '' + paramValue ) const encodeParams = applyToParams.bind(null, encodeParam) const decodeParams: (params: RouteParams | undefined) => RouteParams = applyToParams.bind(null, decode) }
從創(chuàng)建路由實(shí)例來看, stringifyQuery
和 parseQuery
兩個(gè)參數(shù)如果沒有自定義傳入的情況下,會(huì)使用 vue-router 默認(rèn)的解析函數(shù)
默認(rèn)的 stringifyQuery
函數(shù)用于把參數(shù)由對(duì)象形式轉(zhuǎn)換為字符串連接形式,主要流程
- 循環(huán)參數(shù) query 對(duì)象
- 特殊處理參數(shù)為 null 的情況,參數(shù)值為 null 的情況會(huì)拼接在 url 鏈接中但是沒有值,而參數(shù)值為 undefined 則會(huì)直接忽略
- 將對(duì)象轉(zhuǎn)化為數(shù)組,并且對(duì)每個(gè)對(duì)象的值進(jìn)行 encoded 處理
- 將數(shù)組拼接為字符串參數(shù)
// vue-router 默認(rèn)的序列化 query 參數(shù)的函數(shù) export function stringifyQuery(query: LocationQueryRaw): string { let search = '' for (let key in query) { const value = query[key] key = encodeQueryKey(key) // 處理參數(shù)為 null 的情況 if (value == null) { if (value !== undefined) { search += (search.length ? '&' : '') + key } continue } // 將參數(shù)處理為數(shù)組,便于后續(xù)統(tǒng)一遍歷處理 const values: LocationQueryValueRaw[] = isArray(value) ? value.map(v => v && encodeQueryValue(v)) : [value && encodeQueryValue(value)] values.forEach(value => { // 跳過參數(shù)為 undefined 的情況,只拼接有值的參數(shù) if (value !== undefined) { search += (search.length ? '&' : '') + key if (value != null) search += '=' + value } }) } return search } // 示例參數(shù),如下參數(shù)會(huì)被轉(zhuǎn)換為:name=wujieli&age=12&address // query: { // id: undefined, // name: 'wujieli', // age: 12, // address: null, // },
默認(rèn)的 parseQuery
函數(shù)用來將字符串參數(shù)解析為對(duì)象,主要流程
- 排除空字符串和字符串前的 "?"
- 對(duì)字符串用 "&" 分割,遍歷分割后的數(shù)組
- 根據(jù) "=" 截取參數(shù)的 key 和 value,并對(duì) key 和 value 做 decode 處理
- 處理 key 重復(fù)存在的情況,如果 key 對(duì)應(yīng) value 是數(shù)組,就把 value 添加進(jìn)數(shù)組中,否則就覆蓋前一個(gè) value
// vue-router 默認(rèn)的序列化 query 參數(shù)的函數(shù) export function parseQuery(search: string): LocationQuery { const query: LocationQuery = {} // 因?yàn)橐獙?duì)字符串進(jìn)行 split('&') 操作,所以優(yōu)先排除空字符串 if (search === '' || search === '?') return query // 排除解析參數(shù)前的 ? const hasLeadingIM = search[0] === '?' const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&') for (let i = 0; i < searchParams.length; ++i) { // 根據(jù) = 截取參數(shù)的 key 和 value,并做 decode 處理 const searchParam = searchParams[i].replace(PLUS_RE, ' ') const eqPos = searchParam.indexOf('=') const key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos)) const value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1)) // 處理 key 重復(fù)存在的情況 if (key in query) { // an extra variable for ts types let currentValue = query[key] if (!isArray(currentValue)) { currentValue = query[key] = [currentValue] } // we force the modification ;(currentValue as LocationQueryValue[]).push(value) } else { query[key] = value } } return query }
stringifyQuery
這個(gè)方法用在創(chuàng)建 router 實(shí)例時(shí)提供的 resolve
方法中用來生成 url,parseQuery
方法主要用在 router.push
、router.replace
等方法中解析 url 攜帶的參數(shù)
// stringifyQuery 方法的使用 function resolve( rawLocation: Readonly<RouteLocationRaw>, currentLocation?: RouteLocationNormalizedLoaded ): RouteLocation & { href: string } { // ... // 鏈接的完整 path,包括路由 path 和后面的完整參數(shù) const fullPath = stringifyURL( stringifyQuery, assign({}, rawLocation, { hash: encodeHash(hash), path: matchedRoute.path, }) ) } // parseQuery 方法會(huì)封裝在 locationAsObject 方法中使用 function locationAsObject( to: RouteLocationRaw | RouteLocationNormalized ): Exclude<RouteLocationRaw, string> | RouteLocationNormalized { return typeof to === 'string' ? parseURL(parseQuery, to, currentRoute.value.path) : assign({}, to) }
以上就是 stringifyQuery
和 parseQuery
兩個(gè)方法的實(shí)現(xiàn)原理,可以看到源碼中對(duì)于參數(shù)的加密解密考慮的處理是更多的,其實(shí)也可以把兩個(gè)方法的源碼拷貝出來,加上加密、解密的方法然后覆蓋源碼即可,更多關(guān)于vue url 參數(shù)加密的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Vue開發(fā)一個(gè)很火的卡片動(dòng)畫效果
這篇文章主要為大家詳細(xì)介紹了如何基于Vue開發(fā)一個(gè)很火的卡片動(dòng)畫效果,大致包含兩個(gè)效果,光的跟隨效果還有卡片傾斜像?3D?的效果,感興趣的可以了解一下2024-02-02vue實(shí)現(xiàn)無限消息無縫滾動(dòng)
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)無限消息無縫滾動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04vue自定義一個(gè)v-model的實(shí)現(xiàn)代碼
這篇文章主要介紹了vue自定義一個(gè)v-model的實(shí)現(xiàn)代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06