加固后台管理安全

This commit is contained in:
yml2213
2026-06-11 07:23:00 +08:00
parent 5255b21141
commit 88b1df64e7
41 changed files with 1276 additions and 293 deletions
+6 -12
View File
@@ -2,7 +2,6 @@ import axios from 'axios'
import { apiClient } from '@/shared/api/client'
import type { ApiResponse } from '@/shared/types/types'
import { getRefreshToken, setAuthTokens } from '@/shared/utils/authStorage'
import type { UserStatus } from '@/shared/types/status'
export interface AdminRole {
@@ -17,21 +16,19 @@ export interface AdminUser {
nickname: string
status: UserStatus
support_status: 'online' | 'offline' | 'busy'
password_must_change: boolean
roles: AdminRole[]
permissions: string[]
last_login_at?: string
}
export interface AdminTokenPair {
access_token: string
refresh_token: string
token_type: string
export interface AdminRefreshData {
refreshed: boolean
expires_in: number
}
export interface AdminLoginData {
admin: AdminUser
tokens: AdminTokenPair
}
export interface AdminCaptcha {
@@ -82,13 +79,10 @@ export async function updateSupportStatus(status: 'online' | 'offline' | 'busy')
/** Manually refresh admin token (uses raw axios to avoid interceptor recursion) */
export async function refreshAdminSession() {
const refreshToken = getRefreshToken('admin')
if (!refreshToken) throw new Error('no refresh token')
const { data } = await axios.post<ApiResponse<AdminTokenPair>>(
const { data } = await axios.post<ApiResponse<AdminRefreshData>>(
'/api/admin/auth/refresh',
{ refresh_token: refreshToken },
{ timeout: 10000 }
{},
{ timeout: 10000, withCredentials: true }
)
setAuthTokens('admin', data.data)
return data.data
}
+13 -1
View File
@@ -36,6 +36,10 @@ export interface ChangePasswordRequest {
new_password: string
}
export interface ResetPasswordRequest {
new_password: string
}
export async function fetchAdminMgrUsers(page = 1, pageSize = 20) {
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminMgrUser>>>(
'/admin/admin-users',
@@ -78,7 +82,15 @@ export async function assignAdminRoles(id: number, roleIds: number[]) {
return data.data
}
export async function changeAdminPassword(id: number, req: ChangePasswordRequest) {
export async function changeAdminPassword(req: ChangePasswordRequest) {
const { data } = await apiClient.put<ApiResponse<{ updated: boolean }>>(
'/admin/admin-users/me/password',
req
)
return data.data
}
export async function resetAdminPassword(id: number, req: ResetPasswordRequest) {
const { data } = await apiClient.put<ApiResponse<{ updated: boolean }>>(
`/admin/admin-users/${id}/password`,
req
+1 -1
View File
@@ -19,7 +19,7 @@ export {
fetchAdminMe,
updateSupportStatus,
type AdminUser,
type AdminTokenPair,
type AdminRefreshData,
type AdminLoginData,
type AdminCaptcha,
} from './api/adminAuth'
@@ -3,19 +3,20 @@ import { readError } from '@/shared/utils/error'
import { ElMessage } from 'element-plus'
import { Lock, User } from '@element-plus/icons-vue'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { fetchAdminCaptcha, type AdminCaptcha } from '@/features/admin'
import { useAdminSessionStore } from '@/stores/adminSession'
const router = useRouter()
const route = useRoute()
const adminSession = useAdminSessionStore()
const loading = ref(false)
const captchaLoading = ref(false)
const captcha = ref<AdminCaptcha | null>(null)
const form = reactive({
username: 'admin',
password: 'admin123456',
username: '',
password: '',
captchaCode: '',
})
@@ -43,7 +44,8 @@ async function handleLogin() {
form.captchaCode
)
ElMessage.success('后台登录成功')
await router.push('/admin/dashboard')
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : ''
await router.push(redirect.startsWith('/admin') ? redirect : '/admin/dashboard')
} catch (error) {
ElMessage.error(readError(error, '后台登录失败'))
await loadCaptcha()
@@ -6,7 +6,7 @@ import { ref } from 'vue'
import {
fetchAdminMgrUsers,
deleteAdminMgrUser,
changeAdminPassword,
resetAdminPassword,
type AdminMgrUser,
} from '@/features/admin/api/adminMgr'
import { useAdminPaginatedTable } from '@/features/admin/composables/useAdminPaginatedTable'
@@ -22,7 +22,7 @@ const showRolesDialog = ref(false)
const rolesAdmin = ref<AdminMgrUser | null>(null)
const showPasswordDialog = ref(false)
const passwordAdmin = ref<AdminMgrUser | null>(null)
const passwordForm = ref({ old_password: '', new_password: '' })
const passwordForm = ref({ new_password: '' })
const passwordSubmitting = ref(false)
const {
@@ -53,7 +53,7 @@ function openRoles(row: AdminMgrUser) {
function openPassword(row: AdminMgrUser) {
passwordAdmin.value = row
passwordForm.value = { old_password: '', new_password: '' }
passwordForm.value = { new_password: '' }
showPasswordDialog.value = true
}
@@ -78,17 +78,17 @@ async function handleDelete(row: AdminMgrUser) {
async function handleChangePassword() {
if (!passwordAdmin.value) return
if (!passwordForm.value.old_password || !passwordForm.value.new_password) {
ElMessage.warning('请填写完整')
if (!passwordForm.value.new_password) {
ElMessage.warning('请填写新密码')
return
}
passwordSubmitting.value = true
try {
await changeAdminPassword(passwordAdmin.value.id, passwordForm.value)
ElMessage.success('密码已修改')
await resetAdminPassword(passwordAdmin.value.id, passwordForm.value)
ElMessage.success('密码已重置,目标账号需要重新登录')
showPasswordDialog.value = false
} catch (error) {
ElMessage.error(readError(error, '修改失败'))
ElMessage.error(readError(error, '重置失败'))
} finally {
passwordSubmitting.value = false
}
@@ -169,35 +169,27 @@ const statusLabel: Record<string, string> = {
<!-- 角色分配对话框 -->
<AssignRolesDialog v-model="showRolesDialog" :admin="rolesAdmin" @saved="loadAdmins" />
<!-- 修改密码对话框 -->
<!-- 重置密码对话框 -->
<el-dialog
:model-value="showPasswordDialog"
:title="`修改密码 - ${passwordAdmin?.username || ''}`"
:title="`重置密码 - ${passwordAdmin?.username || ''}`"
width="460px"
@update:model-value="showPasswordDialog = $event"
>
<div class="dialog-body">
<el-form-item label="原密码" class="full-control">
<el-input
v-model="passwordForm.old_password"
type="password"
show-password
placeholder="请输入原密码"
/>
</el-form-item>
<el-form-item label="新密码" class="full-control">
<el-input
v-model="passwordForm.new_password"
type="password"
show-password
placeholder="请输入新密码(至少6位)"
placeholder="至少 8 位,包含字母和数字"
/>
</el-form-item>
</div>
<template #footer>
<el-button @click="showPasswordDialog = false">取消</el-button>
<el-button type="primary" :loading="passwordSubmitting" @click="handleChangePassword"
>确认修改</el-button
>确认重置</el-button
>
</template>
</el-dialog>