feat: 添加JWT登录鉴权和用户管理功能
- 后端功能: - 实现JWT登录鉴权,token有效期7天,超过1天自动刷新 - 添加用户管理功能,支持创建、删除、修改密码 - 首次创建的用户为默认账户,拥有管理权限 - 实现密码重置功能,支持重置密钥和频率限制(1分钟最多3次) - 所有API接口需要JWT鉴权 - WebSocket连接需要JWT鉴权,绑定用户身份 - 前端功能: - 添加登录页面和密码重置页面 - 添加用户管理页面,默认账户可管理所有用户,普通用户只能查看和修改自己 - 添加退出登录功能,带二次确认 - 未登录时不建立WebSocket连接 - API请求自动携带JWT token,认证失败自动跳转登录页 - 安全特性: - 密码使用BCrypt加密存储 - 重置密码错误信息统一处理,避免信息泄露 - 用户操作严格绑定JWT,防止数据篡改和越权
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios'
|
||||
import type { ApiResponse } from '../types'
|
||||
import { getToken, setToken, removeToken } from '../utils'
|
||||
import { wsManager } from './websocket'
|
||||
|
||||
/**
|
||||
* API 基础配置
|
||||
@@ -17,6 +19,11 @@ const apiClient: AxiosInstance = axios.create({
|
||||
*/
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
// 从 localStorage 读取 token 并添加到请求头
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
@@ -29,11 +36,31 @@ apiClient.interceptors.request.use(
|
||||
*/
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
// 检查响应头中是否有新的 token(自动刷新)
|
||||
const newToken = response.headers['x-new-token']
|
||||
if (newToken) {
|
||||
setToken(newToken)
|
||||
}
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
(error: AxiosError<ApiResponse<any>>) => {
|
||||
if (error.response) {
|
||||
console.error('API 错误:', error.response.data)
|
||||
const response = error.response
|
||||
const data = response.data
|
||||
|
||||
// 检查是否是认证错误(2001-2999)
|
||||
if (data && data.code >= 2001 && data.code < 3000) {
|
||||
// 清除 token
|
||||
removeToken()
|
||||
// 断开 WebSocket 连接
|
||||
wsManager.disconnect()
|
||||
// 跳转到登录页(避免循环跳转)
|
||||
if (window.location.pathname !== '/login' && window.location.pathname !== '/reset-password') {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
|
||||
console.error('API 错误:', data)
|
||||
} else if (error.request) {
|
||||
console.error('网络错误:', error.request)
|
||||
} else {
|
||||
@@ -47,6 +74,64 @@ apiClient.interceptors.response.use(
|
||||
* API 服务
|
||||
*/
|
||||
export const apiService = {
|
||||
/**
|
||||
* 用户管理 API
|
||||
*/
|
||||
users: {
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
list: () =>
|
||||
apiClient.post<ApiResponse<any[]>>('/users/list', {}),
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
create: (data: { username: string; password: string }) =>
|
||||
apiClient.post<ApiResponse<any>>('/users/create', data),
|
||||
|
||||
/**
|
||||
* 更新用户密码
|
||||
*/
|
||||
updatePassword: (data: { userId: number; newPassword: string }) =>
|
||||
apiClient.post<ApiResponse<void>>('/users/update-password', data),
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
delete: (data: { userId: number }) =>
|
||||
apiClient.post<ApiResponse<void>>('/users/delete', data),
|
||||
|
||||
/**
|
||||
* 用户修改自己的密码
|
||||
*/
|
||||
updateOwnPassword: (data: { newPassword: string }) =>
|
||||
apiClient.post<ApiResponse<void>>('/users/update-own-password', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 认证 API
|
||||
*/
|
||||
auth: {
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
login: (data: { username: string; password: string }) =>
|
||||
apiClient.post<ApiResponse<{ token: string }>>('/auth/login', data),
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
resetPassword: (data: { resetKey: string; username: string; newPassword: string }) =>
|
||||
apiClient.post<ApiResponse<void>>('/auth/reset-password', data),
|
||||
|
||||
/**
|
||||
* 检查是否首次使用
|
||||
*/
|
||||
checkFirstUse: () =>
|
||||
apiClient.post<ApiResponse<{ isFirstUse: boolean }>>('/auth/check-first-use', {})
|
||||
},
|
||||
|
||||
/**
|
||||
* 账户管理 API
|
||||
*/
|
||||
|
||||
@@ -53,6 +53,13 @@ class WebSocketManager {
|
||||
* 连接 WebSocket(全局共享连接)
|
||||
*/
|
||||
connect(): void {
|
||||
// 检查是否有token,未登录不允许连接
|
||||
const token = this.getToken()
|
||||
if (!token) {
|
||||
console.log('[WebSocket] 未登录,不建立连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已经连接或正在连接,直接返回
|
||||
if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) {
|
||||
return
|
||||
@@ -104,8 +111,8 @@ class WebSocketManager {
|
||||
this.isConnecting = false
|
||||
this.notifyConnectionStatus(false)
|
||||
this.stopPing()
|
||||
// 自动重连(除非正在卸载)
|
||||
if (!this.isUnmounting) {
|
||||
// 自动重连(除非正在卸载或未登录)
|
||||
if (!this.isUnmounting && this.getToken()) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
@@ -113,8 +120,8 @@ class WebSocketManager {
|
||||
console.error('[WebSocket] 创建连接失败:', error)
|
||||
this.isConnecting = false
|
||||
this.notifyConnectionStatus(false)
|
||||
// 自动重连(除非正在卸载)
|
||||
if (!this.isUnmounting) {
|
||||
// 自动重连(除非正在卸载或未登录)
|
||||
if (!this.isUnmounting && this.getToken()) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
@@ -272,6 +279,11 @@ class WebSocketManager {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有token,未登录不重连
|
||||
if (!this.getToken()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
@@ -312,14 +324,26 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket URL
|
||||
* 获取 WebSocket URL(带token认证)
|
||||
*/
|
||||
private getWebSocketUrl(): string {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
const token = this.getToken()
|
||||
if (token) {
|
||||
// 通过查询参数传递token
|
||||
return `${protocol}//${host}/ws?token=${encodeURIComponent(token)}`
|
||||
}
|
||||
return `${protocol}//${host}/ws`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取token(从localStorage)
|
||||
*/
|
||||
private getToken(): string | null {
|
||||
return localStorage.getItem('jwt_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册连接状态回调
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user