客户端安全
介绍
客户端安全涵盖Web前端和移动端应用的安全防护,包括XSS防护、敏感数据处理、Token管理、请求加密、安全存储等多个方面。
核心特性:
- XSS防护 - 输入过滤、输出编码、CSP策略
- Token安全 - 安全存储、自动刷新、过期处理
- 请求加密 - AES+RSA混合加密传输
- 安全存储 - 敏感数据加密存储
- 输入验证 - 前端表单验证和过滤
XSS防护
输入过滤
typescript
/**
* XSS字符过滤
*/
export function filterXss(input: string): string {
if (!input) return ''
return input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/')
}
/**
* 富文本XSS过滤(白名单模式)
*/
export function sanitizeHtml(html: string): string {
const allowedTags = ['p', 'br', 'b', 'i', 'u', 'strong', 'em', 'span']
const allowedAttrs = ['class', 'style']
// 使用DOMPurify或类似库进行过滤
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: allowedTags,
ALLOWED_ATTR: allowedAttrs
})
}Vue模板安全
vue
<template>
<!-- ✅ 安全:自动转义 -->
<div>{{ userInput }}</div>
<!-- ❌ 危险:原始HTML -->
<div v-html="userInput"></div>
<!-- ✅ 安全:过滤后的HTML -->
<div v-html="sanitizedHtml"></div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { sanitizeHtml } from '@/utils/xss'
const props = defineProps<{ userInput: string }>()
const sanitizedHtml = computed(() => sanitizeHtml(props.userInput))
</script>CSP策略配置
html
<!-- index.html -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
">Token管理
安全存储
typescript
// Web端:使用httpOnly Cookie或加密存储
class TokenManager {
private static readonly TOKEN_KEY = 'access_token'
private static readonly REFRESH_KEY = 'refresh_token'
// 存储Token
static setToken(token: string, refreshToken?: string) {
// 优先使用sessionStorage(关闭浏览器自动清除)
sessionStorage.setItem(this.TOKEN_KEY, token)
if (refreshToken) {
sessionStorage.setItem(this.REFRESH_KEY, refreshToken)
}
}
// 获取Token
static getToken(): string | null {
return sessionStorage.getItem(this.TOKEN_KEY)
}
// 清除Token
static clearToken() {
sessionStorage.removeItem(this.TOKEN_KEY)
sessionStorage.removeItem(this.REFRESH_KEY)
}
}UniApp存储
typescript
// UniApp端:加密存储
import { encrypt, decrypt } from '@/utils/crypto'
class UniTokenManager {
private static readonly TOKEN_KEY = 'auth_token'
private static readonly SECRET = 'your-secret-key'
// 加密存储Token
static setToken(token: string) {
const encrypted = encrypt(token, this.SECRET)
uni.setStorageSync(this.TOKEN_KEY, encrypted)
}
// 解密获取Token
static getToken(): string | null {
const encrypted = uni.getStorageSync(this.TOKEN_KEY)
if (!encrypted) return null
return decrypt(encrypted, this.SECRET)
}
// 清除Token
static clearToken() {
uni.removeStorageSync(this.TOKEN_KEY)
}
}自动刷新Token
typescript
// 请求拦截器
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
// Token过期,尝试刷新
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
const refreshToken = TokenManager.getRefreshToken()
const { data } = await axios.post('/auth/refresh', { refreshToken })
TokenManager.setToken(data.accessToken, data.refreshToken)
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`
return axios(originalRequest)
} catch (refreshError) {
TokenManager.clearToken()
router.push('/login')
return Promise.reject(refreshError)
}
}
return Promise.reject(error)
}
)请求加密
前端加密实现
typescript
import CryptoJS from 'crypto-js'
import JSEncrypt from 'jsencrypt'
const RSA_PUBLIC_KEY = 'MIIBIjANBgkqhki...'
/**
* 生成随机AES密钥
*/
function generateAesKey(): string {
return CryptoJS.lib.WordArray.random(32).toString()
}
/**
* AES加密
*/
function encryptByAes(data: string, key: string): string {
return CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString()
}
/**
* RSA加密AES密钥
*/
function encryptByRsa(data: string): string {
const encrypt = new JSEncrypt()
encrypt.setPublicKey(RSA_PUBLIC_KEY)
return encrypt.encrypt(data) || ''
}
/**
* 发送加密请求
*/
export async function sendEncryptedRequest(url: string, data: any) {
// 1. 生成AES密钥
const aesKey = generateAesKey()
// 2. AES加密数据
const encryptedData = encryptByAes(JSON.stringify(data), aesKey)
// 3. RSA加密AES密钥
const encryptedKey = encryptByRsa(aesKey)
// 4. 发送请求
return axios.post(url, encryptedData, {
headers: {
'Content-Type': 'text/plain',
'encrypt-key': encryptedKey
}
})
}响应解密
typescript
/**
* AES解密
*/
function decryptByAes(data: string, key: string): string {
const decrypted = CryptoJS.AES.decrypt(data, CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return decrypted.toString(CryptoJS.enc.Utf8)
}
// 响应拦截器处理解密
axios.interceptors.response.use(response => {
const encryptKey = response.headers['encrypt-key']
if (encryptKey && typeof response.data === 'string') {
// 解密响应数据
const aesKey = decryptByRsa(encryptKey, RSA_PRIVATE_KEY)
response.data = JSON.parse(decryptByAes(response.data, aesKey))
}
return response
})输入验证
表单验证规则
typescript
// 验证规则定义
export const validationRules = {
// 用户名:4-20位字母数字下划线
username: /^[a-zA-Z0-9_]{4,20}$/,
// 密码:8-20位,包含大小写字母和数字
password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,20}$/,
// 手机号
phone: /^1[3-9]\d{9}$/,
// 邮箱
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
// 身份证
idCard: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
}
/**
* 验证输入
*/
export function validate(value: string, type: keyof typeof validationRules): boolean {
return validationRules[type].test(value)
}Element Plus表单验证
vue
<template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password" />
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_]{4,20}$/, message: '4-20位字母数字下划线', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, max: 20, message: '密码长度8-20位', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
callback(new Error('密码需包含大小写字母和数字'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
</script>UniApp表单验证
vue
<template>
<wd-form :model="form" :rules="rules" ref="formRef">
<wd-cell-group>
<wd-input
label="手机号"
v-model="form.phone"
prop="phone"
placeholder="请输入手机号"
/>
<wd-input
label="验证码"
v-model="form.code"
prop="code"
placeholder="请输入验证码"
/>
</wd-cell-group>
</wd-form>
</template>
<script lang="ts" setup>
const rules = {
phone: [
{ required: true, message: '请输入手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }
],
code: [
{ required: true, message: '请输入验证码' },
{ pattern: /^\d{6}$/, message: '验证码为6位数字' }
]
}
</script>安全存储
Web端安全存储
typescript
import CryptoJS from 'crypto-js'
class SecureStorage {
private static readonly SECRET = 'your-encryption-key'
// 加密存储
static setItem(key: string, value: any) {
const data = JSON.stringify(value)
const encrypted = CryptoJS.AES.encrypt(data, this.SECRET).toString()
localStorage.setItem(key, encrypted)
}
// 解密读取
static getItem<T>(key: string): T | null {
const encrypted = localStorage.getItem(key)
if (!encrypted) return null
try {
const bytes = CryptoJS.AES.decrypt(encrypted, this.SECRET)
const data = bytes.toString(CryptoJS.enc.Utf8)
return JSON.parse(data)
} catch {
return null
}
}
// 删除
static removeItem(key: string) {
localStorage.removeItem(key)
}
}UniApp安全存储
typescript
class UniSecureStorage {
private static readonly SECRET = 'your-encryption-key'
// 加密存储
static set(key: string, value: any) {
const data = JSON.stringify(value)
const encrypted = encrypt(data, this.SECRET)
uni.setStorageSync(key, encrypted)
}
// 解密读取
static get<T>(key: string): T | null {
const encrypted = uni.getStorageSync(key)
if (!encrypted) return null
try {
const data = decrypt(encrypted, this.SECRET)
return JSON.parse(data)
} catch {
return null
}
}
// 清除敏感数据
static clearSensitive() {
const sensitiveKeys = ['token', 'userInfo', 'payInfo']
sensitiveKeys.forEach(key => uni.removeStorageSync(key))
}
}移动端特有安全
页面权限控制
typescript
// 路由守卫
const whiteList = ['/pages/login/index', '/pages/register/index']
export function setupRouterGuard() {
const list = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab']
list.forEach(item => {
uni.addInterceptor(item, {
invoke(args) {
const token = uni.getStorageSync('token')
const url = args.url.split('?')[0]
if (!token && !whiteList.includes(url)) {
uni.redirectTo({ url: '/pages/login/index' })
return false
}
return true
}
})
})
}防截屏/录屏
typescript
// App端防截屏
// #ifdef APP-PLUS
plus.navigator.setSecureScreen(true)
// #endif安全键盘
vue
<!-- 密码输入使用安全键盘 -->
<wd-input
v-model="password"
type="password"
:adjust-position="false"
placeholder="请输入密码"
/>生物认证
typescript
// 指纹/面容认证
async function bioAuth(): Promise<boolean> {
// #ifdef APP-PLUS
const supported = await uni.checkIsSupportSoterAuthentication()
if (supported.supportMode.includes('fingerPrint')) {
try {
await uni.startSoterAuthentication({
requestAuthModes: ['fingerPrint'],
challenge: 'login-challenge'
})
return true
} catch {
return false
}
}
// #endif
return false
}安全检查清单
XSS防护
- [ ] 用户输入进行XSS过滤
- [ ] 避免使用
v-html或进行过滤 - [ ] 配置CSP策略
- [ ] 第三方脚本进行审查
Token安全
- [ ] Token存储在sessionStorage或加密存储
- [ ] 实现Token自动刷新
- [ ] 退出登录时清除所有Token
- [ ] 敏感操作验证Token有效性
数据传输
- [ ] 敏感接口使用HTTPS
- [ ] 敏感数据加密传输
- [ ] 请求参数签名验证
移动端
- [ ] 敏感页面防截屏
- [ ] 支付等操作使用安全键盘
- [ ] 实现生物认证
- [ ] 页面级权限控制
常见问题
1. XSS攻击防护
问题场景: 用户评论中包含恶意脚本
解决方案:
typescript
// 输入时过滤
const safeInput = filterXss(userInput)
// 输出时使用文本插值
// ✅ {{ userInput }} 自动转义
// ❌ v-html="userInput" 危险2. Token泄露风险
问题场景: localStorage中的Token被XSS窃取
解决方案:
typescript
// 使用sessionStorage替代localStorage
sessionStorage.setItem('token', token)
// 或使用httpOnly Cookie
// 由后端设置,前端无法通过JS访问3. 敏感数据缓存
问题场景: 支付信息等敏感数据被缓存
解决方案:
typescript
// 敏感数据不缓存,使用后立即清除
function handlePayment(cardInfo: CardInfo) {
try {
await pay(cardInfo)
} finally {
// 清除敏感数据
cardInfo = null
}
}
// 退出时清除所有敏感缓存
function logout() {
SecureStorage.clearSensitive()
TokenManager.clearToken()
}4. 移动端页面劫持
问题场景: 恶意应用截取敏感页面
解决方案:
typescript
// 敏感页面启用防截屏
onShow(() => {
// #ifdef APP-PLUS
plus.navigator.setSecureScreen(true)
// #endif
})
onHide(() => {
// #ifdef APP-PLUS
plus.navigator.setSecureScreen(false)
// #endif
})