diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/common/advice/GlobalExceptionHandler.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/common/advice/GlobalExceptionHandler.java index f8d48fc..8c927c3 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/common/advice/GlobalExceptionHandler.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/common/advice/GlobalExceptionHandler.java @@ -7,6 +7,7 @@ import com.test.bijihoudaun.common.response.ResultCode; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -71,6 +72,13 @@ public class GlobalExceptionHandler { return R.fail(ResultCode.VALIDATE_FAILED.getCode(), "参数错误"); } + // 修复:添加权限拒绝异常处理 + @ExceptionHandler(AuthorizationDeniedException.class) + public R handleAuthorizationDeniedException(AuthorizationDeniedException e, HttpServletRequest request) { + log.warn("Access denied at {}: {}", request.getRequestURI(), e.getMessage()); + return R.fail(ResultCode.FORBIDDEN.getCode(), "无权操作,需要管理员权限"); + } + @ExceptionHandler(Exception.class) public R handleException(Exception e, HttpServletRequest request) { log.error("Unexpected error at {} - Error type: {}", request.getRequestURI(), e.getClass().getSimpleName()); diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/config/SecurityConfig.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/config/SecurityConfig.java index 6bb5d59..becee4a 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/config/SecurityConfig.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/config/SecurityConfig.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -22,6 +23,7 @@ import java.util.Arrays; @Configuration @EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Autowired diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageController.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageController.java index caa4b09..81e8117 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageController.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/controller/ImageController.java @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.util.StreamUtils; diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/XSSInterceptor.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/XSSInterceptor.java index 4e87599..e59ce20 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/XSSInterceptor.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/interceptor/XSSInterceptor.java @@ -10,7 +10,11 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; +import java.io.PrintWriter; +import java.util.Arrays; import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; /** * XSS 过滤拦截器 @@ -21,6 +25,29 @@ import java.util.Enumeration; @Slf4j public class XSSInterceptor implements HandlerInterceptor { + // 修复:添加不需要检查的请求头白名单(浏览器标准请求头) + private static final Set HEADER_WHITELIST = new HashSet<>(Arrays.asList( + "sec-ch-ua", + "sec-ch-ua-mobile", + "sec-ch-ua-platform", + "sec-fetch-dest", + "sec-fetch-mode", + "sec-fetch-site", + "sec-fetch-user", + "user-agent", + "accept", + "accept-encoding", + "accept-language", + "cache-control", + "connection", + "host", + "referer", + "upgrade-insecure-requests", + "content-type", + "content-length", + "origin" + )); + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 过滤请求头,发现 XSS 攻击则拒绝请求 @@ -46,6 +73,10 @@ public class XSSInterceptor implements HandlerInterceptor { while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); + // 修复:跳过白名单中的请求头 + if (HEADER_WHITELIST.contains(headerName.toLowerCase())) { + continue; + } String headerValue = request.getHeader(headerName); if (StrUtil.isNotBlank(headerValue)) { String filteredValue = HtmlUtil.filter(headerValue); @@ -84,8 +115,12 @@ public class XSSInterceptor implements HandlerInterceptor { response.setContentType("application/json;charset=UTF-8"); response.setStatus(400); ObjectMapper mapper = new ObjectMapper(); - response.getWriter().write(mapper.writeValueAsString( - R.fail(ResultCode.FAILED.getCode(), message) - )); + // 修复:使用 try-with-resources 确保 PrintWriter 关闭 + try (PrintWriter writer = response.getWriter()) { + writer.write(mapper.writeValueAsString( + R.fail(ResultCode.FAILED.getCode(), message) + )); + writer.flush(); + } } } diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/mapper/UserMapper.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/mapper/UserMapper.java index c97f90f..cae5f28 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/mapper/UserMapper.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/mapper/UserMapper.java @@ -8,6 +8,6 @@ import org.apache.ibatis.annotations.Select; @Mapper public interface UserMapper extends BaseMapper { // 自定义查询方法示例 - @Select("SELECT id, `username`, `password`, `email`, created_at, updated_at, `token`, token_enddata FROM `user` WHERE username = #{username}") + @Select("SELECT id, `username`, `password`, `email`, `role`, created_at, updated_at, `token`, token_enddata FROM `user` WHERE username = #{username}") User findByUsername(String username); } \ No newline at end of file diff --git a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/RegistrationCodeServiceImpl.java b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/RegistrationCodeServiceImpl.java index fef734c..7a0b2d1 100644 --- a/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/RegistrationCodeServiceImpl.java +++ b/biji-houdaun/src/main/java/com/test/bijihoudaun/service/impl/RegistrationCodeServiceImpl.java @@ -18,14 +18,18 @@ import java.util.UUID; @Transactional public class RegistrationCodeServiceImpl extends ServiceImpl implements RegistrationCodeService { + // 修复:定义日期时间格式器 + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + @Override public String generateCode(String creator) { RegistrationCode registrationCode = new RegistrationCode(); String code = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16); registrationCode.setCode(code); registrationCode.setCreatedBy(creator); - registrationCode.setCreatedAt(LocalDateTime.now().toString()); - registrationCode.setExpiryTime(LocalDateTime.now().plusDays(1).toString()); + // 修复:使用格式化后的日期字符串 + registrationCode.setCreatedAt(LocalDateTime.now().format(DATE_TIME_FORMATTER)); + registrationCode.setExpiryTime(LocalDateTime.now().plusDays(1).format(DATE_TIME_FORMATTER)); save(registrationCode); return code; } @@ -41,9 +45,16 @@ public class RegistrationCodeServiceImpl extends ServiceImpl().lt("expiry_time", LocalDateTime.now().toString())); + // 修复:使用格式化后的日期字符串进行比较 + remove(new QueryWrapper().lt("expiry_time", LocalDateTime.now().format(DATE_TIME_FORMATTER))); } -} \ No newline at end of file +} diff --git a/biji-qianduan/src/api/CommonApi.js b/biji-qianduan/src/api/CommonApi.js index 4ac9e3a..43cd4f1 100644 --- a/biji-qianduan/src/api/CommonApi.js +++ b/biji-qianduan/src/api/CommonApi.js @@ -6,9 +6,10 @@ import axiosApi from '@/utils/axios.js' export const groupingId = (data) => axiosApi.get(`/api/markdown/grouping/${encodeURIComponent(data)}`) // 获取所有分组 export const groupingAll = (data) => { - const params = new URLSearchParams(); - if (data) params.append('parentId', data); - return axiosApi.get(`/api/groupings?${params.toString()}`); + if (data) { + return axiosApi.get(`/api/groupings?parentId=${encodeURIComponent(data)}`); + } + return axiosApi.get('/api/groupings'); }; // 获取所有Markdown文件 export const markdownAll = () => axiosApi.get(`/api/markdown`); diff --git a/biji-qianduan/src/components/CaptchaDialog.vue b/biji-qianduan/src/components/CaptchaDialog.vue index 8f0a7d4..6631e77 100644 --- a/biji-qianduan/src/components/CaptchaDialog.vue +++ b/biji-qianduan/src/components/CaptchaDialog.vue @@ -92,7 +92,8 @@ const refreshCaptcha = async () => { captchaCode.value = ''; } } catch (error) { - ElMessage.error('获取验证码失败,请重试'); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('获取验证码失败:', error); } }; diff --git a/biji-qianduan/src/components/HomePage.vue b/biji-qianduan/src/components/HomePage.vue index 0f383f2..8f5becd 100644 --- a/biji-qianduan/src/components/HomePage.vue +++ b/biji-qianduan/src/components/HomePage.vue @@ -256,7 +256,8 @@ const resetToHomeView = async () => { groupMarkdownFiles.value = await getRecentFiles(100) || []; updateDisplayedFiles(); } catch (error) { - ElMessage.error('获取最近文件失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('获取最近文件失败:', error); groupMarkdownFiles.value = []; displayedFiles.value = []; } @@ -298,7 +299,8 @@ const handleSelectFile = async (data) => { showEditor.value = false; activeMenu.value = `group-${data.id}`; } catch (error) { - ElMessage.error('获取笔记列表失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('获取笔记列表失败:', error); groupMarkdownFiles.value = []; displayedFiles.value = []; } @@ -379,7 +381,8 @@ const previewFile = async (file) => { selectedFile.value = { ...file, content, isLoading: false }; } } catch (error) { - ElMessage.error('获取笔记内容失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('获取笔记内容失败:', error); selectedFile.value = null; } }; @@ -401,7 +404,8 @@ const deleteNote = (file) => { await fetchGroupings(); await resetToHomeView(); } catch (error) { - ElMessage.error('删除笔记失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('删除笔记失败:', error); } }); }; @@ -465,7 +469,8 @@ const handleSearch = async () => { showEditor.value = false; activeMenu.value = 'search'; } catch (error) { - ElMessage.error('搜索失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('搜索失败:', error); } }; @@ -528,7 +533,8 @@ const handleExport = async (format) => { } ElMessage.success(`${format.toUpperCase()} 导出成功`); } catch (error) { - ElMessage.error(`导出失败: ${error.message}`); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('导出失败:', error); } }; diff --git a/biji-qianduan/src/components/TrashPage.vue b/biji-qianduan/src/components/TrashPage.vue index 15ae8b9..fdbe82a 100644 --- a/biji-qianduan/src/components/TrashPage.vue +++ b/biji-qianduan/src/components/TrashPage.vue @@ -71,7 +71,8 @@ const fetchTrashItems = async () => { const response = await getTrash(); trashItems.value = response || []; } catch (error) { - ElMessage.error('获取回收站内容失败'); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('获取回收站内容失败:', error); } }; @@ -81,7 +82,8 @@ const handleRestore = async (item) => { ElMessage.success('恢复成功'); fetchTrashItems(); } catch (error) { - ElMessage.error('恢复失败'); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('恢复失败:', error); } }; @@ -126,7 +128,8 @@ const handleCaptchaConfirm = async ({ captchaId, captchaCode }) => { pendingItem.value = null; fetchTrashItems(); } catch (error) { - ElMessage.error('操作失败: ' + (error.response?.data?.msg || error.message)); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('操作失败:', error); } }; diff --git a/biji-qianduan/src/components/home/NoteEditor.vue b/biji-qianduan/src/components/home/NoteEditor.vue index c139d15..d506a2e 100644 --- a/biji-qianduan/src/components/home/NoteEditor.vue +++ b/biji-qianduan/src/components/home/NoteEditor.vue @@ -81,8 +81,9 @@ const initVditor = () => { const url = res.url; const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; vditor.value.insertValue(`![${file.name}](${baseUrl}${url})`); - }).catch(() => { - ElMessage.error('图片上传失败'); + }).catch((error) => { + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('图片上传失败:', error); }); }, }, @@ -150,7 +151,8 @@ const save = async (value) => { } } catch (error) { saveStatus.value = '保存失败'; - ElMessage.error('保存失败: ' + (error.message || '未知错误')); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('保存失败:', error); } finally { isSaving.value = false; } diff --git a/biji-qianduan/src/components/home/SidebarMenu.vue b/biji-qianduan/src/components/home/SidebarMenu.vue index bd2a02c..8fa4b45 100644 --- a/biji-qianduan/src/components/home/SidebarMenu.vue +++ b/biji-qianduan/src/components/home/SidebarMenu.vue @@ -118,7 +118,8 @@ const handleDeleteGroup = (group) => { ElMessage.success('分类已删除'); emit('group-deleted'); } catch (error) { - ElMessage.error('删除分类失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('删除分类失败:', error); } }); }; diff --git a/biji-qianduan/src/components/home/dialogs/CreateGroupDialog.vue b/biji-qianduan/src/components/home/dialogs/CreateGroupDialog.vue index b3bd7ed..d1d8e46 100644 --- a/biji-qianduan/src/components/home/dialogs/CreateGroupDialog.vue +++ b/biji-qianduan/src/components/home/dialogs/CreateGroupDialog.vue @@ -113,7 +113,8 @@ const handleSubmit = async () => { emit('group-created'); // 通知父组件刷新 handleClose(); } catch (error) { - ElMessage.error('创建分类失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('创建分类失败:', error); } } }); diff --git a/biji-qianduan/src/components/home/dialogs/MoveNoteDialog.vue b/biji-qianduan/src/components/home/dialogs/MoveNoteDialog.vue index 5845d09..c406e48 100644 --- a/biji-qianduan/src/components/home/dialogs/MoveNoteDialog.vue +++ b/biji-qianduan/src/components/home/dialogs/MoveNoteDialog.vue @@ -154,7 +154,8 @@ const handleSubmit = async () => { emit('move-success'); handleClose(); } catch (error) { - ElMessage.error('移动失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('移动失败:', error); } finally { isLoading.value = false; } diff --git a/biji-qianduan/src/components/home/dialogs/PrivacyDialog.vue b/biji-qianduan/src/components/home/dialogs/PrivacyDialog.vue index 2b1584b..63e315e 100644 --- a/biji-qianduan/src/components/home/dialogs/PrivacyDialog.vue +++ b/biji-qianduan/src/components/home/dialogs/PrivacyDialog.vue @@ -68,7 +68,8 @@ const handleSubmit = async () => { emit('privacy-changed', updatedFile); handleClose(); } catch (error) { - ElMessage.error('修改笔记状态失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('修改笔记状态失败:', error); } }; diff --git a/biji-qianduan/src/components/home/dialogs/RenameDialog.vue b/biji-qianduan/src/components/home/dialogs/RenameDialog.vue index c88f294..bc52fcf 100644 --- a/biji-qianduan/src/components/home/dialogs/RenameDialog.vue +++ b/biji-qianduan/src/components/home/dialogs/RenameDialog.vue @@ -60,7 +60,8 @@ const handleSubmit = async () => { // 传递新名称给父组件 emit('renamed', newName.value); } catch (error) { - ElMessage.error('重命名失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('重命名失败:', error); } }; diff --git a/biji-qianduan/src/components/home/dialogs/SelectGroupDialog.vue b/biji-qianduan/src/components/home/dialogs/SelectGroupDialog.vue index c2d83ed..e6db793 100644 --- a/biji-qianduan/src/components/home/dialogs/SelectGroupDialog.vue +++ b/biji-qianduan/src/components/home/dialogs/SelectGroupDialog.vue @@ -80,7 +80,8 @@ const handleSubmit = () => { emit('import-success'); handleClose(); } catch (error) { - ElMessage.error('导入失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('导入失败:', error); } }; reader.readAsText(props.fileToImport); diff --git a/biji-qianduan/src/components/home/dialogs/SystemSettingsDialog.vue b/biji-qianduan/src/components/home/dialogs/SystemSettingsDialog.vue index e80befa..7f0f947 100644 --- a/biji-qianduan/src/components/home/dialogs/SystemSettingsDialog.vue +++ b/biji-qianduan/src/components/home/dialogs/SystemSettingsDialog.vue @@ -59,8 +59,8 @@ const fetchRegistrationStatus = async () => { try { isRegistrationEnabled.value = await getRegistrationStatus(); } catch (error) { + // 错误已在 axios 拦截器中显示,这里不再重复显示 console.error("Failed to fetch registration status:", error); - ElMessage.error('获取注册状态失败'); } }; @@ -69,7 +69,7 @@ const handleToggleRegistration = async (value) => { await toggleRegistration(value); ElMessage.success(`注册功能已${value ? '开启' : '关闭'}`); } catch (error) { - ElMessage.error('操作失败'); + // 错误已在 axios 拦截器中显示,这里不再重复显示 isRegistrationEnabled.value = !value; // Revert on failure } }; @@ -80,7 +80,8 @@ const handleGenerateCode = async () => { generatedCode.value = code; ElMessage.success('注册码生成成功'); } catch (error) { - ElMessage.error('生成注册码失败: ' + error.message); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('生成注册码失败:', error); } }; diff --git a/biji-qianduan/src/components/home/dialogs/UpdatePasswordDialog.vue b/biji-qianduan/src/components/home/dialogs/UpdatePasswordDialog.vue index 1255af7..f1b53ce 100644 --- a/biji-qianduan/src/components/home/dialogs/UpdatePasswordDialog.vue +++ b/biji-qianduan/src/components/home/dialogs/UpdatePasswordDialog.vue @@ -115,7 +115,8 @@ const handleCaptchaConfirm = async ({ captchaId, captchaCode }) => { emit('password-updated'); handleClose(); } catch (error) { - ElMessage.error('密码修改失败: ' + (error.response?.data?.msg || error.message)); + // 错误已在 axios 拦截器中显示,这里不再重复显示 + console.error('密码修改失败:', error); } finally { loading.value = false; } diff --git a/biji-qianduan/src/utils/axios.js b/biji-qianduan/src/utils/axios.js index fde5874..7314ea9 100644 --- a/biji-qianduan/src/utils/axios.js +++ b/biji-qianduan/src/utils/axios.js @@ -71,6 +71,13 @@ instance.interceptors.response.use( return Promise.reject(error); } + // 403 - 权限不足 + if (status === 403) { + const msg = data?.msg || '无权操作'; + ElMessage.error(msg); + return Promise.reject(new Error(msg)); + } + // 429 - 请求过于频繁 if (status === 429) { const msg = data?.msg || '请求过于频繁,请稍后再试';