feat: 添加用户认证系统
引入基于 Basic Auth 的用户认证系统,包括用户管理、登录界面和 API 鉴权 - 新增用户实体和管理功能 - 实现前端登录界面和凭证管理 - 重构 API 鉴权为 Basic Auth 模式 - 添加用户管理脚本工具
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user