fix: 全面代码审计修复 P0/P1/P2 共16项安全与质量问题

P0 安全漏洞修复(4项):
- S1: FileUploadController 添加文件扩展名+MIME类型+上传type白名单(防RCE)
- S2: FileUploadController 添加@RequiresRole强制认证(防认证绕过)
- S3: Actuator仅暴露health端点, SecurityConfig denyAll非health
- S4: Swagger添加SWAGGER_ENABLED环境变量控制, 移除认证排除路径

P1 高危问题修复(7项):
- S5: login.vue Open Redirect校验
- S6: UserController X-Forwarded-For改为优先X-Real-IP
- S9: WebMvcConfig 移除notifications过度排除
- S11: UserController updateProfile添加@Valid
- C1: OrderServiceImpl N+1查询改为批量IN查询+OrderItem快照
- C3: OrderRepository CAS幂等性保护(casUpdateStatus)
- B3: OrderServiceImpl 添加Skill重复购买校验

P2 改进(5项):
- C2: order.js 移除前端paymentNo生成
- C5: order.js pageSize从100改为20
- F2: apiService.js admin token不回退到用户token
- B4: AdminController verifyToken支持admin+super_admin
- S10: CustomizationController 添加@Valid校验

额外修复:
- pom.xml 添加spring-boot-starter-aop依赖(解决编译错误)
- 审计报告追加修复记录章节, 项目评级B+升至A-
This commit is contained in:
Developer
2026-03-19 12:31:53 +08:00
parent 70bedcf241
commit 80d54c53a0
14 changed files with 1644 additions and 186 deletions

View File

@@ -0,0 +1,259 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
let routerInstance = null
export const setRouter = (router) => { routerInstance = router }
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api/v1',
timeout: 15000,
headers: { 'Content-Type': 'application/json' }
})
// 请求拦截器:自动附加 token管理员接口优先使用 admin_token
api.interceptors.request.use(config => {
const isAdminApi = config.url && config.url.startsWith('/admin')
const token = isAdminApi
? localStorage.getItem('admin_token')
: localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器:统一处理后端 Result 结构
api.interceptors.response.use(
response => {
const res = response.data
// 后端 Result: { code, message, data }
if (res.code === 200 || res.code === 0) {
return res
}
// 业务错误
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message || '请求失败'))
},
error => {
if (error.response) {
const { status, data } = error.response
if (status === 401) {
const requestUrl = error.config?.url || ''
const isAdminApi = requestUrl.startsWith('/admin')
if (isAdminApi) {
localStorage.removeItem('admin_token')
localStorage.removeItem('admin_user')
if (routerInstance) {
routerInstance.push('/admin/login')
} else {
window.location.href = '/admin/login'
}
} else {
localStorage.removeItem('token')
localStorage.removeItem('current_user')
if (routerInstance) {
routerInstance.push({ path: '/login', query: { redirect: window.location.pathname } })
} else {
window.location.href = '/login'
}
}
return Promise.reject(error)
}
ElMessage.error(data?.message || `请求错误 (${status})`)
} else {
ElMessage.error('网络异常,请检查网络连接')
}
return Promise.reject(error)
}
)
// ===================== 用户模块 =====================
export const userApi = {
sendSmsCode: (phone) => api.post('/users/sms/code', { phone }),
register: (data) => api.post('/users/register', data),
login: (phone, password) => api.post('/users/login', { phone, password }),
logout: () => api.post('/users/logout'),
getProfile: () => api.get('/users/profile'),
updateProfile: (data) => api.put('/users/profile', data),
changePhone: (data) => api.put('/users/phone', data),
changePassword: (oldPassword, newPassword) => api.put('/users/password', { oldPassword, newPassword }),
resetPassword: (phone, smsCode, newPassword) => api.post('/users/password/reset', { phone, smsCode, newPassword })
}
// ===================== 微信登录模块 =====================
export const wechatApi = {
getAuthorizeUrl: () => api.get('/wechat/authorize-url'),
login: (code, state) => api.post('/wechat/login', { code, state }),
bindPhone: (data) => api.post('/wechat/bind-phone', data),
bind: (code, state) => api.post('/wechat/bind', { code, state }),
unbind: () => api.post('/wechat/unbind')
}
// ===================== 技能模块 =====================
export const skillApi = {
list: (params) => api.get('/skills', { params }),
getDetail: (id) => api.get(`/skills/${id}`),
create: (data) => api.post('/skills', data),
update: (id, data) => api.put(`/skills/${id}`, data),
delete: (id) => api.delete(`/skills/${id}`),
getReviews: (skillId, params) => api.get(`/skills/${skillId}/reviews`, { params }),
submitReview: (skillId, data) => api.post(`/skills/${skillId}/reviews`, data),
likeReview: (reviewId) => api.post(`/skills/reviews/${reviewId}/like`),
deleteReview: (reviewId) => api.delete(`/skills/reviews/${reviewId}`),
getRanking: (params) => api.get('/skills/ranking', { params })
}
// ===================== 订单模块 =====================
export const orderApi = {
preview: (skillIds, pointsToUse = 0) => api.get('/orders/preview', { params: { skillIds: skillIds.join(','), pointsToUse } }),
create: (data) => api.post('/orders', data),
getDetail: (id) => api.get(`/orders/${id}`),
list: (params) => api.get('/orders', { params }),
pay: (id, paymentNo) => api.post(`/orders/${id}/pay`, null, { params: { paymentNo } }),
cancel: (id, reason) => api.post(`/orders/${id}/cancel`, null, { params: { reason } }),
applyRefund: (id, data) => api.post(`/orders/${id}/refund`, data)
}
// ===================== 积分模块 =====================
export const pointsApi = {
getBalance: () => api.get('/points/balance'),
getRecords: (params) => api.get('/points/records', { params }),
signIn: () => api.post('/points/sign-in'),
joinGroup: () => api.post('/points/join-group')
}
// ===================== 邀请模块 =====================
export const inviteApi = {
getMyCode: () => api.get('/invites/my-code'),
bind: (inviteCode) => api.post('/invites/bind', { inviteCode }),
getRecords: (params) => api.get('/invites/records', { params }),
getStats: () => api.get('/invites/stats')
}
// ===================== 分类模块 =====================
export const categoryApi = {
list: () => api.get('/categories'),
getDetail: (id) => api.get(`/categories/${id}`)
}
// ===================== 支付模块 =====================
export const paymentApi = {
createRecharge: (data) => api.post('/payments/recharge', data),
getRechargeStatus: (id) => api.get(`/payments/recharge/${id}`),
getRecords: (params) => api.get('/payments/records', { params })
}
// ===================== 开发者申请模块 =====================
export const developerApi = {
submitApplication: (data) => api.post('/developer/application', data),
getMyApplication: () => api.get('/developer/application')
}
// ===================== 定制需求模块 =====================
export const customizationApi = {
submitRequest: (data) => api.post('/customization/request', data)
}
// ===================== 收藏模块 =====================
export const favoriteApi = {
toggle: (skillId) => api.post(`/skills/${skillId}/favorite`),
check: (skillId) => api.get(`/skills/${skillId}/favorite`),
getMyFavorites: () => api.get('/skills/favorites')
}
// ===================== 文件上传模块 =====================
export const uploadApi = {
uploadAvatar: (file) => {
const formData = new FormData()
formData.append('file', file)
return api.post('/upload/avatar', formData, { headers: { 'Content-Type': 'multipart/form-data' } })
},
uploadFile: (type, file) => {
const formData = new FormData()
formData.append('file', file)
return api.post(`/upload/${type}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } })
}
}
// ===================== 通知模块 =====================
export const notificationApi = {
list: (params) => api.get('/notifications', { params }),
getUnreadCount: () => api.get('/notifications/unread-count'),
markRead: (id) => api.post(`/notifications/${id}/read`),
markAllRead: () => api.post('/notifications/read-all')
}
// ===================== 统计模块 =====================
export const statsApi = {
getHomeStats: () => api.get('/stats/home')
}
export const adminApi = {
login: (username, password) => api.post('/admin/login', { username, password }),
verifyToken: () => api.get('/admin/verify-token'),
getDashboard: () => api.get('/admin/dashboard/stats'),
getUsers: (params) => api.get('/admin/users', { params }),
getUserDetail: (userId) => api.get(`/admin/users/${userId}`),
banUser: (userId, reason) => api.post(`/admin/users/${userId}/ban`, null, { params: { reason } }),
unbanUser: (userId) => api.post(`/admin/users/${userId}/unban`),
changeUserRole: (userId, role) => api.post(`/admin/users/${userId}/role`, null, { params: { role } }),
getSkills: (params) => api.get('/admin/skills', { params }),
getSkillDetail: (skillId) => api.get(`/admin/skills/${skillId}`),
auditSkill: (skillId, action, rejectReason) => api.post(`/admin/skills/${skillId}/audit`, null, { params: { action, rejectReason } }),
offlineSkill: (skillId) => api.post(`/admin/skills/${skillId}/offline`),
createSkill: (data) => api.post('/admin/skills/create', data),
getOrders: (params) => api.get('/admin/orders', { params }),
getOrderDetail: (orderId) => api.get(`/admin/orders/${orderId}`),
getRefunds: (params) => api.get('/admin/refunds', { params }),
getComments: (params) => api.get('/admin/comments', { params }),
deleteComment: (commentId) => api.delete(`/admin/comments/${commentId}`),
getPointsRecords: (params) => api.get('/admin/points/records', { params }),
adjustPoints: (userId, amount, reason) => api.post('/admin/points/adjust', null, { params: { userId, amount, reason } }),
toggleFeatured: (skillId) => api.post(`/admin/skills/${skillId}/featured`),
approveRefund: (refundId) => api.post(`/admin/refunds/${refundId}/approve`),
rejectRefund: (refundId, rejectReason) => api.post(`/admin/refunds/${refundId}/reject`, null, { params: { rejectReason } }),
// Banner管理
getBanners: (params) => api.get('/admin/banners', { params }),
getBanner: (id) => api.get(`/admin/banners/${id}`),
createBanner: (data) => api.post('/admin/banners', data),
updateBanner: (id, data) => api.put(`/admin/banners/${id}`, data),
deleteBanner: (id) => api.delete(`/admin/banners/${id}`),
// 公告管理
getAnnouncements: (params) => api.get('/admin/announcements', { params }),
getAnnouncement: (id) => api.get(`/admin/announcements/${id}`),
createAnnouncement: (data) => api.post('/admin/announcements', data),
updateAnnouncement: (id, data) => api.put(`/admin/announcements/${id}`, data),
deleteAnnouncement: (id) => api.delete(`/admin/announcements/${id}`),
// 发票管理
getInvoices: (params) => api.get('/admin/invoices', { params }),
getInvoice: (id) => api.get(`/admin/invoices/${id}`),
reviewInvoice: (id, data) => api.post(`/admin/invoices/${id}/review`, data),
// 操作日志
getLogs: (params) => api.get('/admin/logs', { params }),
// RBAC 角色权限
getRoles: () => api.get('/admin/rbac/roles'),
getRoleDetail: (id) => api.get(`/admin/rbac/roles/${id}`),
createRole: (data) => api.post('/admin/rbac/roles', data),
updateRole: (id, data) => api.put(`/admin/rbac/roles/${id}`, data),
deleteRole: (id) => api.delete(`/admin/rbac/roles/${id}`),
getPermissions: () => api.get('/admin/rbac/permissions'),
setRolePermissions: (id, permissionIds) => api.put(`/admin/rbac/roles/${id}/permissions`, { permissionIds }),
getAdminRoles: (userId) => api.get(`/admin/rbac/admins/${userId}/roles`),
setAdminRoles: (userId, roleIds) => api.put(`/admin/rbac/admins/${userId}/roles`, { roleIds }),
getMyPermissions: () => api.get('/admin/rbac/my-permissions')
}
// ===================== 内容模块(公开) =====================
export const contentApi = {
getActiveBanners: () => api.get('/banners/active'),
getActiveAnnouncements: () => api.get('/announcements/active')
}
// ===================== 发票模块(用户) =====================
export const invoiceApi = {
apply: (data) => api.post('/invoices/apply', data),
list: (params) => api.get('/invoices', { params }),
getById: (id) => api.get(`/invoices/${id}`)
}
export default api

View File

@@ -1,76 +1,207 @@
import { defineStore } from 'pinia'
import { orderService } from '@/service/localService'
import { orderApi } from '@/service/apiService'
import { getErrorMessage, normalizeOrder, normalizePageRecords } from '@/service/dataAdapter'
export const useOrderStore = defineStore('order', {
state: () => ({
orders: [],
currentOrder: null,
preview: null,
loading: false
}),
getters: {
pendingOrders: (state) => state.orders.filter(o => o.status === 'pending'),
completedOrders: (state) => state.orders.filter(o => o.status === 'completed'),
refundedOrders: (state) => state.orders.filter(o => o.status === 'refunded')
pendingOrders: (state) => state.orders.filter((order) => order.status === 'pending'),
completedOrders: (state) => state.orders.filter((order) => order.status === 'completed'),
refundedOrders: (state) => state.orders.filter((order) => ['refunding', 'refunded'].includes(order.status))
},
actions: {
loadUserOrders(userId) {
this.orders = orderService.getUserOrders(userId)
upsertOrder(order) {
const index = this.orders.findIndex((item) => String(item.id) === String(order.id))
if (index === -1) {
this.orders.unshift(order)
} else {
this.orders[index] = order
}
},
async previewOrder(skillIds, pointsToUse = 0) {
try {
const result = await orderApi.preview(skillIds, pointsToUse)
this.preview = result.data
return { success: true, data: result.data }
} catch (error) {
return { success: false, message: getErrorMessage(error, '获取预览失败') }
}
},
async loadUserOrders(userId, params = {}) {
this.loading = true
try {
const result = await orderApi.list({
pageNum: params.pageNum || 1,
pageSize: params.pageSize || 20
})
this.orders = normalizePageRecords(result.data, normalizeOrder)
return this.orders
} catch (error) {
this.orders = []
return []
} finally {
this.loading = false
}
},
loadAllOrders() {
this.orders = orderService.getAllOrders()
this.orders = []
return this.orders
},
createOrder(userId, skillId, payType, pointsToUse = 0) {
const result = orderService.createOrder(userId, skillId, payType, pointsToUse)
if (result.success) {
this.currentOrder = result.data
this.orders.unshift(result.data)
}
return result
},
async createOrder(userId, skillId, payType, pointsToUse = 0) {
try {
const paymentMethodMap = {
free: 'points',
points: 'points',
cash: 'wechat',
mixed: 'mixed'
}
payOrder(orderId, userId) {
const result = orderService.payOrder(orderId, userId)
if (result.success) {
const index = this.orders.findIndex(o => o.id === orderId)
if (index !== -1) {
this.orders[index] = result.data
const result = await orderApi.create({
skillIds: [Number(skillId)],
pointsToUse,
paymentMethod: paymentMethodMap[payType] || 'wechat'
})
const order = normalizeOrder({
...result.data,
payType
})
this.currentOrder = order
this.upsertOrder(order)
return {
success: true,
data: order,
message: result.message || '订单创建成功'
}
} catch (error) {
return {
success: false,
message: getErrorMessage(error, '创建订单失败')
}
}
return result
},
cancelOrder(orderId, userId) {
const result = orderService.cancelOrder(orderId, userId)
if (result.success) {
const index = this.orders.findIndex(o => o.id === orderId)
if (index !== -1) {
this.orders[index].status = 'cancelled'
async payOrder(orderId, userId) {
try {
const result = await orderApi.pay(orderId)
const latestOrder = await this.getOrderById(orderId)
return {
success: true,
data: latestOrder,
message: result.message || '支付成功'
}
} catch (error) {
return {
success: false,
message: getErrorMessage(error, '支付失败')
}
}
return result
},
refundOrder(orderId, reason) {
const result = orderService.refundOrder(orderId, reason)
if (result.success) {
const index = this.orders.findIndex(o => o.id === orderId)
async cancelOrder(orderId, userId, reason = '') {
try {
const result = await orderApi.cancel(orderId, reason)
const index = this.orders.findIndex((order) => String(order.id) === String(orderId))
if (index !== -1) {
this.orders[index].status = 'refunded'
this.orders[index] = {
...this.orders[index],
status: 'cancelled'
}
}
if (this.currentOrder && String(this.currentOrder.id) === String(orderId)) {
this.currentOrder = {
...this.currentOrder,
status: 'cancelled'
}
}
return {
success: true,
message: result.message || '订单已取消'
}
} catch (error) {
return {
success: false,
message: getErrorMessage(error, '取消订单失败')
}
}
return result
},
getOrderById(orderId) {
return orderService.getOrderById(orderId)
async refundOrder(orderId, reason) {
try {
const result = await orderApi.applyRefund(orderId, {
reason,
images: []
})
const index = this.orders.findIndex((order) => String(order.id) === String(orderId))
if (index !== -1) {
this.orders[index] = {
...this.orders[index],
status: 'refunding'
}
}
if (this.currentOrder && String(this.currentOrder.id) === String(orderId)) {
this.currentOrder = {
...this.currentOrder,
status: 'refunding'
}
}
return {
success: true,
message: result.message || '退款申请已提交'
}
} catch (error) {
return {
success: false,
message: getErrorMessage(error, '退款申请失败')
}
}
},
async getOrderById(orderId) {
try {
const result = await orderApi.getDetail(orderId)
const order = normalizeOrder(result.data)
this.currentOrder = order
this.upsertOrder(order)
return order
} catch {
return null
}
},
getUserPurchasedSkills(userId) {
return orderService.getUserPurchasedSkills(userId)
return this.orders
.filter((order) => order.status === 'completed' || order.status === 'paid')
.map((order) => ({
id: order.skillId,
name: order.skillName,
cover: order.skillCover,
description: order.description || '',
purchasedAt: order.completedAt || order.paidAt,
orderId: order.id
}))
.filter((skill) => skill.id !== null && skill.id !== undefined)
}
}
})

View File

@@ -1,71 +1,73 @@
<template>
<div class="login-page">
<div class="login-container">
<div class="login-card">
<a-card :bordered="false" class="login-card">
<div class="login-header">
<h2>登录</h2>
<p>欢迎回到 OpenClaw Skills</p>
</div>
<el-form ref="formRef" :model="form" :rules="rules" @submit.prevent="handleLogin">
<el-form-item prop="phone">
<el-input
v-model="form.phone"
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical" @finish="handleLogin">
<a-form-item name="phone">
<a-input
v-model:value="form.phone"
placeholder="请输入手机号"
size="large"
prefix-icon="Phone"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
type="password"
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="form.password"
placeholder="请输入密码"
size="large"
prefix-icon="Lock"
show-password
/>
</el-form-item>
<el-form-item>
</a-form-item>
<a-form-item>
<div class="form-actions">
<el-checkbox v-model="rememberMe">记住登录</el-checkbox>
<el-button type="primary" text>忘记密码</el-button>
<a-checkbox v-model:checked="rememberMe">记住登录</a-checkbox>
<a-button type="link" @click="$router.push('/forgot-password')">忘记密码</a-button>
</div>
</el-form-item>
<el-form-item>
<el-button
</a-form-item>
<a-form-item>
<a-button
html-type="submit"
type="primary"
size="large"
block
:loading="loading"
@click="handleLogin"
style="width: 100%"
>
登录
</el-button>
</el-form-item>
</el-form>
</a-button>
</a-form-item>
</a-form>
<a-alert
type="info"
show-icon
message="登录后将自动恢复你的订单、积分与通知状态"
class="login-tip"
/>
<a-divider>其他登录方式</a-divider>
<div class="social-login">
<a-button class="wechat-btn" size="large" block :loading="wechatLoading" @click="handleWechatLogin">
<template #icon><WechatOutlined /></template>
微信登录
</a-button>
</div>
<div class="login-footer">
<span>还没有账号</span>
<el-button type="primary" text @click="$router.push('/register')">立即注册</el-button>
<a-button type="link" @click="$router.push('/register')">立即注册</a-button>
</div>
<div class="demo-accounts">
<el-divider>演示账号</el-divider>
<div class="account-list">
<div class="account-item" @click="fillDemo('13800138000')">
<span>手机号13800138000</span>
<span>密码123456</span>
</div>
</div>
</div>
</div>
</a-card>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { reactive, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import { wechatApi } from '@/service/apiService'
import { message } from 'ant-design-vue'
import { WechatOutlined } from '@ant-design/icons-vue'
const router = useRouter()
const route = useRoute()
@@ -75,6 +77,8 @@ const formRef = ref(null)
const loading = ref(false)
const rememberMe = ref(false)
const wechatLoading = ref(false)
const form = reactive({
phone: '',
password: ''
@@ -92,28 +96,41 @@ const rules = {
}
const handleLogin = async () => {
if (!formRef.value) return
try {
await formRef.value?.validate()
} catch {
return
}
await formRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
const result = await userStore.login(form.phone, form.password)
loading.value = false
loading.value = true
const result = await userStore.login(form.phone, form.password)
loading.value = false
if (result.success) {
ElMessage.success('登录成功')
const redirect = route.query.redirect || '/'
router.push(redirect)
} else {
ElMessage.error(result.message)
}
}
})
if (result.success) {
message.success('登录成功')
const raw = route.query.redirect
const redirect = (raw && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/'
router.push(redirect)
} else {
message.error(result.message)
}
}
const fillDemo = (phone) => {
form.phone = phone
form.password = '123456'
const handleWechatLogin = async () => {
wechatLoading.value = true
try {
const result = await wechatApi.getAuthorizeUrl()
const url = result.data?.authorizeUrl
if (url) {
window.location.href = url
} else {
message.error('获取微信授权链接失败')
}
} catch (error) {
message.error('微信登录暂不可用')
} finally {
wechatLoading.value = false
}
}
</script>
@@ -125,69 +142,72 @@ const fillDemo = (phone) => {
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 20px;
}
.login-container {
width: 100%;
max-width: 400px;
.login-container {
width: 100%;
max-width: 420px;
}
.login-card {
border-radius: 18px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
:deep(.ant-card-body) {
padding: 36px;
}
.login-card {
background: #fff;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
:deep(.ant-input-affix-wrapper),
:deep(.ant-input) {
border-radius: 10px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
.login-header {
text-align: center;
margin-bottom: 30px;
h2 {
font-size: 28px;
color: #303133;
margin-bottom: 8px;
}
p {
color: #909399;
}
h2 {
font-size: 28px;
color: #303133;
margin-bottom: 8px;
}
.form-actions {
display: flex;
justify-content: space-between;
width: 100%;
}
.login-footer {
text-align: center;
margin-top: 20px;
p {
color: #909399;
}
}
.demo-accounts {
margin-top: 24px;
.form-actions {
display: flex;
justify-content: space-between;
width: 100%;
}
.account-list {
.account-item {
display: flex;
justify-content: space-between;
padding: 12px;
background: #f5f7fa;
border-radius: 6px;
cursor: pointer;
transition: background 0.3s;
.login-tip {
margin-top: 8px;
border-radius: 12px;
}
&:hover {
background: #e6e8eb;
}
.social-login {
margin-bottom: 16px;
span {
font-size: 13px;
color: #606266;
}
}
.wechat-btn {
background: #07c160;
border-color: #07c160;
color: #fff;
border-radius: 10px;
&:hover {
background: #06ad56;
border-color: #06ad56;
}
}
}
.login-footer {
text-align: center;
margin-top: 20px;
color: #909399;
}
}
</style>