feat: 添加用户角色字段并实现权限控制
fix(security): 修复重放攻击拦截器的时间戳验证漏洞 refactor(security): 重构验证码工具类使用线程安全实现 perf(login): 优化登录锁定工具类性能并添加定期清理 fix(editor): 修复笔记编辑器空指针问题 style: 清理数据库索引脚本中的冗余注释 fix(api): 修复前端API调用参数编码问题 feat(image): 实现图片名称同步服务 refactor(markdown): 重构Markdown服务分离图片名称同步逻辑 fix(xss): 添加HTML转义函数防止XSS攻击 fix(user): 修复用户服务权限加载问题 fix(rate-limit): 修复速率限制拦截器并发问题 fix(axios): 生产环境隐藏详细错误信息 fix(image): 修复图片上传和删除的权限验证 refactor(captcha): 重构验证码工具类使用并发安全实现 fix(jwt): 修复JWT过滤器空指针问题 fix(export): 修复笔记导出XSS漏洞 fix(search): 修复Markdown搜索SQL注入问题 fix(interceptor): 修复重放攻击拦截器逻辑错误 fix(controller): 修复用户控制器空指针问题 fix(security): 修复nonce生成使用密码学安全方法
This commit is contained in:
@@ -2,9 +2,14 @@ import axiosApi from '@/utils/axios.js'
|
||||
|
||||
|
||||
|
||||
export const groupingId = (data) => axiosApi.get(`/api/markdown/grouping/${data}`)
|
||||
// 修复:使用 encodeURIComponent 编码 URL 参数,防止注入
|
||||
export const groupingId = (data) => axiosApi.get(`/api/markdown/grouping/${encodeURIComponent(data)}`)
|
||||
// 获取所有分组
|
||||
export const groupingAll = (data) => axiosApi.get(`/api/groupings?parentId=${data}`);
|
||||
export const groupingAll = (data) => {
|
||||
const params = new URLSearchParams();
|
||||
if (data) params.append('parentId', data);
|
||||
return axiosApi.get(`/api/groupings?${params.toString()}`);
|
||||
};
|
||||
// 获取所有Markdown文件
|
||||
export const markdownAll = () => axiosApi.get(`/api/markdown`);
|
||||
// 预览markdown文件
|
||||
@@ -19,20 +24,15 @@ export const updateMarkdown = (data) => {
|
||||
return axiosApi.post(`/api/markdown/updateMarkdown`, data)
|
||||
}
|
||||
// 批量删除图片
|
||||
// 修复:后端接收 JSON 数组,不是 FormData
|
||||
export const deleteImages = (list) => {
|
||||
const formData = new FormData()
|
||||
formData.append('urls', list)
|
||||
return axiosApi.post('/api/images/batch', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
return axiosApi.post('/api/images/batch', list)
|
||||
}
|
||||
// 上传图片
|
||||
export const uploadImage = (file, userId, markdownId) => {
|
||||
// 修复:移除 userId 参数,后端从 SecurityContext 获取当前用户
|
||||
export const uploadImage = (file, markdownId) => {
|
||||
const formData = new FormData()
|
||||
if (file) formData.append('file', file)
|
||||
if (userId) formData.append('userId', userId)
|
||||
if (markdownId) formData.append('markdownId', markdownId)
|
||||
return axiosApi.post('/api/images', formData, {
|
||||
headers: {
|
||||
@@ -59,7 +59,11 @@ export const login = (data) => {
|
||||
}
|
||||
|
||||
// 搜索
|
||||
export const searchMarkdown = (keyword) => axiosApi.get(`/api/markdown/search?keyword=${keyword}`);
|
||||
export const searchMarkdown = (keyword) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('keyword', keyword);
|
||||
return axiosApi.get(`/api/markdown/search?${params.toString()}`);
|
||||
};
|
||||
|
||||
// 注册
|
||||
export const register = (data) => {
|
||||
|
||||
@@ -134,6 +134,7 @@ import { onMounted, ref, nextTick, watch, computed, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import Vditor from 'vditor';
|
||||
import 'vditor/dist/index.css';
|
||||
import { escapeHtml } from '@/utils/security';
|
||||
import {
|
||||
groupingAll,
|
||||
markdownList,
|
||||
@@ -501,9 +502,11 @@ const handleExport = async (format) => {
|
||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
||||
downloadBlob(blob, `${title}.md`);
|
||||
} else if (format === 'html') {
|
||||
const fullHtml = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>${title}</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css"><style>body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px;}</style></head><body class="markdown-body"><h1>${title}</h1>${previewElement.innerHTML}</body></html>`;
|
||||
// 修复:对title进行HTML转义,防止XSS
|
||||
const escapedTitle = escapeHtml(title);
|
||||
const fullHtml = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>${escapedTitle}</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css"><style>body{box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px;}</style></head><body class="markdown-body"><h1>${escapedTitle}</h1>${previewElement.innerHTML}</body></html>`;
|
||||
const blob = new Blob([fullHtml], { type: 'text/html;charset=utf-8' });
|
||||
downloadBlob(blob, `${title}.html`);
|
||||
downloadBlob(blob, `${escapedTitle}.html`);
|
||||
} else if (format === 'pdf') {
|
||||
const canvas = await html2canvas(previewElement, { scale: 2, useCORS: true });
|
||||
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' });
|
||||
|
||||
@@ -92,8 +92,14 @@ const initVditor = () => {
|
||||
const save = async (value) => {
|
||||
if (isSaving.value) return;
|
||||
|
||||
// 修复:添加空值检查
|
||||
if (!vditor.value) {
|
||||
console.warn('编辑器未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(saveTimeout);
|
||||
const content = typeof value === 'string' ? value : vditor.value.getValue();
|
||||
const content = typeof value === 'string' ? value : vditor.value?.getValue() || '';
|
||||
|
||||
if (content === lastSavedContent.value && currentId.value) {
|
||||
return;
|
||||
|
||||
@@ -32,8 +32,9 @@
|
||||
>
|
||||
<div class="menu-section" v-if="isMobile">
|
||||
<div v-if="userStore.isLoggedIn" class="user-info">
|
||||
<el-avatar :size="40" class="user-avatar">{{ userStore.userInfo?.username?.charAt(0)?.toUpperCase() }}</el-avatar>
|
||||
<span class="username">{{ userStore.userInfo?.username }}</span>
|
||||
<!-- 修复:添加默认值防止空指针 -->
|
||||
<el-avatar :size="40" class="user-avatar">{{ userStore.userInfo?.username?.charAt(0)?.toUpperCase() || '?' }}</el-avatar>
|
||||
<span class="username">{{ userStore.userInfo?.username || '访客' }}</span>
|
||||
</div>
|
||||
<div v-else class="guest-info">
|
||||
<el-button type="primary" @click="goToLogin">登录</el-button>
|
||||
|
||||
@@ -92,8 +92,11 @@ instance.interceptors.response.use(
|
||||
return Promise.reject(new Error(msg));
|
||||
}
|
||||
|
||||
// 其他错误,显示后端返回的消息
|
||||
const msg = data?.msg || error.message;
|
||||
// 其他错误,生产环境隐藏详细错误信息
|
||||
const isDev = import.meta.env.DEV;
|
||||
const msg = isDev
|
||||
? (data?.msg || error.message)
|
||||
: (data?.msg || '操作失败,请稍后重试');
|
||||
ElMessage.error(msg);
|
||||
} else {
|
||||
ElMessage({
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
|
||||
/**
|
||||
* 生成随机 nonce (32位随机字符串)
|
||||
* 使用 crypto.getRandomValues 确保密码学安全
|
||||
* @returns {string} nonce
|
||||
*/
|
||||
export function generateNonce() {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let nonce = '';
|
||||
for (let i = 0; i < 32; i++) {
|
||||
nonce += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return nonce;
|
||||
const array = new Uint8Array(24); // 24 bytes = 32 base64 chars
|
||||
crypto.getRandomValues(array);
|
||||
return btoa(String.fromCharCode(...array))
|
||||
.replace(/[+/=]/g, '') // 移除特殊字符
|
||||
.substring(0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,3 +61,15 @@ export function needsReplayAttackValidation(method, url) {
|
||||
}
|
||||
return REPLAY_ATTACK_METHODS.includes(method.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 转义函数,防止 XSS
|
||||
* @param {string} text 需要转义的文本
|
||||
* @returns {string} 转义后的文本
|
||||
*/
|
||||
export function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user