feat: 添加用户认证系统

引入基于 Basic Auth 的用户认证系统,包括用户管理、登录界面和 API 鉴权
- 新增用户实体和管理功能
- 实现前端登录界面和凭证管理
- 重构 API 鉴权为 Basic Auth 模式
- 添加用户管理脚本工具
This commit is contained in:
dmy
2026-01-18 12:47:16 +08:00
parent a55dfd78d2
commit b6a6398864
30 changed files with 2042 additions and 82 deletions

View File

@@ -40,7 +40,8 @@
<el-container>
<el-header style="text-align: right; font-size: 12px">
<span>Admin</span>
<span v-if="currentUser">{{ currentUser }}</span>
<el-button v-if="currentUser" type="primary" link @click="handleLogout">退出登录</el-button>
</el-header>
<el-main>
@@ -60,11 +61,26 @@
</el-main>
</el-container>
</el-container>
<!-- 登录对话框 -->
<el-dialog v-model="loginDialogVisible" title="用户登录" width="400px" :close-on-click-modal="false" :show-close="false">
<el-form :model="loginForm" label-width="80px">
<el-form-item label="用户名">
<el-input v-model="loginForm.username" placeholder="请输入用户名" @keyup.enter="handleLogin" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" show-password @keyup.enter="handleLogin" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="handleLogin" :loading="loginLoading">登录</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import api from './utils/api'
import { ref, onMounted, onUnmounted } from 'vue'
import api, { setAuthCredentials, clearAuthCredentials, isAuthenticated } from './utils/api'
import { ElMessage } from 'element-plus'
import { DataBoard, Document, Setting, MagicStick, Connection } from '@element-plus/icons-vue'
import Dashboard from './components/Dashboard.vue'
@@ -82,6 +98,15 @@ const isCrawling = ref(false)
const total = ref(0)
const sourceOptions = ref<string[]>([])
// 登录相关状态
const loginDialogVisible = ref(false)
const loginLoading = ref(false)
const loginForm = ref({
username: '',
password: ''
})
const currentUser = ref<string | null>(null)
const handleSelect = (key: string) => {
activeIndex.value = key
}
@@ -156,8 +181,85 @@ const updateBidsByDateRange = async (startDate: string, endDate?: string, keywor
}
}
// 处理登录
const handleLogin = async () => {
if (!loginForm.value.username || !loginForm.value.password) {
ElMessage.warning('请输入用户名和密码')
return
}
loginLoading.value = true
try {
// 保存凭证到 localStorage
setAuthCredentials(loginForm.value.username, loginForm.value.password)
// 测试凭证是否有效
await api.get('/api/bids', { params: { page: 1, limit: 1 } })
// 登录成功
currentUser.value = loginForm.value.username
loginDialogVisible.value = false
ElMessage.success('登录成功')
// 清空表单
loginForm.value.username = ''
loginForm.value.password = ''
// 加载数据
fetchData()
} catch (error: any) {
console.error('登录失败:', error)
// 清除无效凭证
clearAuthCredentials()
if (error.response?.status === 401) {
ElMessage.error('用户名或密码错误')
} else {
ElMessage.error('登录失败,请稍后重试')
}
} finally {
loginLoading.value = false
}
}
// 处理登出
const handleLogout = () => {
clearAuthCredentials()
currentUser.value = null
ElMessage.success('已退出登录')
}
// 处理认证要求事件
const handleAuthRequired = () => {
loginDialogVisible.value = true
}
onMounted(() => {
fetchData()
// 检查是否已登录
if (isAuthenticated()) {
// 从凭证中提取用户名
const credentials = localStorage.getItem('authCredentials')
if (credentials) {
try {
const decoded = atob(credentials)
const [username] = decoded.split(':')
currentUser.value = username || null
fetchData()
} catch (e) {
console.error('解析凭证失败:', e)
clearAuthCredentials()
loginDialogVisible.value = true
}
}
} else {
loginDialogVisible.value = true
}
// 监听认证要求事件
window.addEventListener('auth-required', handleAuthRequired)
})
onUnmounted(() => {
window.removeEventListener('auth-required', handleAuthRequired)
})
</script>

View File

@@ -1,5 +1,38 @@
import axios, { type InternalAxiosRequestConfig, type AxiosError } from 'axios';
/**
* 认证相关工具函数
*/
/**
* 设置 Basic Auth 凭证到 localStorage
*/
export const setAuthCredentials = (username: string, password: string) => {
const credentials = btoa(`${username}:${password}`);
localStorage.setItem('authCredentials', credentials);
};
/**
* 从 localStorage 获取 Basic Auth 凭证
*/
export const getAuthCredentials = (): string | null => {
return localStorage.getItem('authCredentials');
};
/**
* 清除认证凭证
*/
export const clearAuthCredentials = () => {
localStorage.removeItem('authCredentials');
};
/**
* 检查是否已登录
*/
export const isAuthenticated = (): boolean => {
return !!localStorage.getItem('authCredentials');
};
/**
* API配置
* 配置axios实例设置baseURL和请求拦截器
@@ -13,22 +46,10 @@ const api = axios.create({
// 请求拦截器
api.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 如果 baseURL 不是 localhost自动添加 API Key
const baseURL =
(config.baseURL as string) ||
(api.defaults.baseURL as string) ||
'';
const isLocalhost =
baseURL.includes('localhost') || baseURL.includes('127.0.0.1');
if (!isLocalhost) {
// 从环境变量或 localStorage 获取 API Key
const apiKey =
(import.meta.env.VITE_API_KEY as string) ||
localStorage.getItem('apiKey');
if (apiKey && config.headers) {
config.headers['X-API-Key'] = apiKey;
}
// 添加 Basic Auth 头
const credentials = getAuthCredentials();
if (credentials && config.headers) {
config.headers['Authorization'] = `Basic ${credentials}`;
}
return config;
@@ -44,6 +65,13 @@ api.interceptors.response.use(
return response;
},
(error: AxiosError) => {
// 处理 401 未授权错误
if (error.response?.status === 401) {
// 清除无效的凭证
clearAuthCredentials();
// 触发自定义事件,通知应用需要重新登录
window.dispatchEvent(new CustomEvent('auth-required'));
}
console.error('API请求错误:', error);
return Promise.reject(error);
},