vue?項目優(yōu)雅的對url參數(shù)加密詳解
實現(xiàn)方案:stringifyQuery 和 parseQuery
近期因為公司內部的安全檢查,說我們現(xiàn)在的系統(tǒng)中參數(shù)是明文的,包括給后端請求的參數(shù)和前端頁面跳轉攜帶的參數(shù),因為是公司內部使用的系統(tǒng),在安全性方面的設計考慮確實不夠充分
對于參數(shù)的加密和解密很好實現(xiàn),直接采用常用的 AES 算法,前后端定義好通用的密鑰和加解密方式就好,前端加解密這里主要使用到 crypto-js 這個工具包,再通過一個類簡單封裝一下加解密的算法即可
// 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)
}
}
對于前端頁面間跳轉攜帶參數(shù),我們項目使用的都是 vue-router 的 query 來攜帶參數(shù),但是有那么多頁面跳轉的地方,不可能都手動添加加解密方法處理吧,工作量大不說,萬一漏改一個就可能導致整個頁面無法加載了,這鍋可不能背
首先想到的方法是在路由守衛(wèi) beforeEach 中對參數(shù)進行加密,然后在 afterEach 守衛(wèi)中對參數(shù)進行解密,但是這個想法在 beforeEach 中加密就無法實現(xiàn)。原因是 beforeEach(to, from, next) 的第三個參數(shù) next 函數(shù)中,如果參數(shù)是路由對象,會導致跳轉死循環(huán)
接下來經過幾個小時百思不得其解(摸魚)之后,最終在 API 參考 | Vue Router (vuejs.org) 找到這樣兩個 API:stringifyQuery 和 parseQuery,官網的定義如下
stringifyQuery:對查詢對象進行字符串化的自定義實現(xiàn)。不應該在前面加上 ?。應該正確編碼查詢鍵和值
parseQuery:用于解析查詢的自定義實現(xiàn)。必須解碼查詢鍵和值
比如,官網建議如果想使用 qs 包來解析查詢,可以這樣配置
import qs from 'qs'
createRouter({
// 其他配置...
parseQuery: qs.parse,
stringifyQuery: qs.stringify,
})
現(xiàn)在最終的解決方案就很明確了,自定義兩個參數(shù)加密、解密的方法,然后在 createRouter 中添加到 stringifyQuery 和 parseQuery 這兩個方法就可以了,下面是詳細代碼
// 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)建一個 hash 歷史記錄。
history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH),
routes: basicRoutes,
scrollBehavior: () => ({ left: 0, top: 0 }),
stringifyQuery, // 序列化query參數(shù)
parseQuery, // 反序列化query參數(shù)
})
加密的效果如下,我也在 github 上傳了加密方式的 demo,可以直接下載體驗一下

更進一步:相關實現(xiàn)原理
在實現(xiàn)完這兩個功能之后,我突然想翻一下 Vue Router 的源碼,看一下 stringifyQuery 和 parseQuery 的實現(xiàn)原理,避免以后遇到類似的問題再抓瞎
打開 Vue Router@4的源碼,整個項目是用 pnpm 管理 monorepo 的方式組織,通過 rollup.config.js 中定義的 input 入口可以知道,所有的方法都通過 packages/router/src/index.ts 導出
首先先看初始化路由實例的 createRouter 方法,這個方法主要做了這么幾件事
- 通過
createRouterMatcher方法,根據(jù)路由配置列表創(chuàng)建 matcher,返回 5 個操作 matcher 方法。matcher 可以理解為路由頁面匹配器,包含路由所有信息和 crud 操作方法 - 定義三個路由守衛(wèi):beforeEach、beforeResolve、afterEach
- 聲明當前路由 currentRoute,對 url 參數(shù) paramas 進行編碼處理
- 添加路由的各種操作方法,最后返回一個 router 對象
一個簡化版本的 createRouter 方法如下所示,前文使用到的 stringifyQuery 和 parseQuery 都是在這個方法中加載
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>()
// 聲明當前路由
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ù)進行編碼處理
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)建路由實例來看, stringifyQuery 和 parseQuery 兩個參數(shù)如果沒有自定義傳入的情況下,會使用 vue-router 默認的解析函數(shù)
默認的 stringifyQuery 函數(shù)用于把參數(shù)由對象形式轉換為字符串連接形式,主要流程
- 循環(huán)參數(shù) query 對象
- 特殊處理參數(shù)為 null 的情況,參數(shù)值為 null 的情況會拼接在 url 鏈接中但是沒有值,而參數(shù)值為 undefined 則會直接忽略
- 將對象轉化為數(shù)組,并且對每個對象的值進行 encoded 處理
- 將數(shù)組拼接為字符串參數(shù)
// vue-router 默認的序列化 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ù)會被轉換為:name=wujieli&age=12&address
// query: {
// id: undefined,
// name: 'wujieli',
// age: 12,
// address: null,
// },
默認的 parseQuery 函數(shù)用來將字符串參數(shù)解析為對象,主要流程
- 排除空字符串和字符串前的 "?"
- 對字符串用 "&" 分割,遍歷分割后的數(shù)組
- 根據(jù) "=" 截取參數(shù)的 key 和 value,并對 key 和 value 做 decode 處理
- 處理 key 重復存在的情況,如果 key 對應 value 是數(shù)組,就把 value 添加進數(shù)組中,否則就覆蓋前一個 value
// vue-router 默認的序列化 query 參數(shù)的函數(shù)
export function parseQuery(search: string): LocationQuery {
const query: LocationQuery = {}
// 因為要對字符串進行 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 重復存在的情況
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 這個方法用在創(chuàng)建 router 實例時提供的 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 方法會封裝在 locationAsObject 方法中使用
function locationAsObject(
to: RouteLocationRaw | RouteLocationNormalized
): Exclude<RouteLocationRaw, string> | RouteLocationNormalized {
return typeof to === 'string'
? parseURL(parseQuery, to, currentRoute.value.path)
: assign({}, to)
}
以上就是 stringifyQuery 和 parseQuery 兩個方法的實現(xiàn)原理,可以看到源碼中對于參數(shù)的加密解密考慮的處理是更多的,其實也可以把兩個方法的源碼拷貝出來,加上加密、解密的方法然后覆蓋源碼即可,更多關于vue url 參數(shù)加密的資料請關注腳本之家其它相關文章!

