140 lines
4.0 KiB
Markdown
140 lines
4.0 KiB
Markdown
# 登录接口逻辑说明
|
||
|
||
## 问题描述
|
||
原先的实现中,登录失败时后端会抛出 `UnauthorizedException`(HTTP 401),前端响应拦截器收到 401 后会自动清空 token 并跳转到登录页,这在登录页面本身会造成逻辑混乱。
|
||
|
||
## 解决方案
|
||
|
||
### 1. 后端修改 (`后端/src/auth/auth.controller.ts`)
|
||
|
||
**修改前**:
|
||
```typescript
|
||
async login(@Body() loginDto: LoginDto): Promise<any> {
|
||
const result = await this.authService.login(loginDto);
|
||
return { code: 0, message: 'success', data: result };
|
||
}
|
||
```
|
||
- 如果 auth.service 抛出 `UnauthorizedException`,会直接返回 HTTP 401 状态码
|
||
|
||
**修改后**:
|
||
```typescript
|
||
async login(@Body() loginDto: LoginDto): Promise<any> {
|
||
try {
|
||
const result = await this.authService.login(loginDto);
|
||
return {
|
||
code: 0,
|
||
message: '登录成功',
|
||
data: result,
|
||
};
|
||
} catch (error) {
|
||
// 登录失败返回统一格式,不抛出 HTTP 异常
|
||
return {
|
||
code: 401,
|
||
message: error.message || '用户名或密码错误',
|
||
data: null,
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键变化**:
|
||
- ✅ HTTP 状态码始终为 200
|
||
- ✅ 通过响应体的 `code` 字段区分成功(0)和失败(401)
|
||
- ✅ 错误信息通过 `message` 字段返回
|
||
|
||
### 2. 前端修改 (`管理后台/src/utils/request.ts`)
|
||
|
||
**修改前**:
|
||
```typescript
|
||
if (res.code !== 0 && res.code !== 200) {
|
||
ElMessage.error(res.message || '请求失败')
|
||
|
||
if (res.code === 401) {
|
||
const userStore = useUserStore()
|
||
userStore.logout()
|
||
router.push('/login')
|
||
}
|
||
|
||
return Promise.reject(new Error(res.message || '请求失败'))
|
||
}
|
||
```
|
||
- 所有 401 都会跳转登录页
|
||
|
||
**修改后**:
|
||
```typescript
|
||
if (res.code !== 0 && res.code !== 200) {
|
||
const isLoginRequest = response.config.url?.includes('/auth/login')
|
||
|
||
if (res.code === 401 && !isLoginRequest) {
|
||
ElMessage.error('登录已过期,请重新登录')
|
||
const userStore = useUserStore()
|
||
userStore.logout()
|
||
router.push('/login')
|
||
} else if (!isLoginRequest) {
|
||
ElMessage.error(res.message || '请求失败')
|
||
}
|
||
|
||
return Promise.reject(new Error(res.message || '请求失败'))
|
||
}
|
||
```
|
||
|
||
**关键变化**:
|
||
- ✅ 区分登录请求和其他请求
|
||
- ✅ 登录接口的 401 不会触发自动跳转
|
||
- ✅ 登录接口的错误消息不在拦截器中显示(由登录页面处理)
|
||
|
||
### 3. 前端登录页面 (`管理后台/src/views/login/index.vue`)
|
||
|
||
保持原有的错误处理逻辑:
|
||
```typescript
|
||
try {
|
||
await userStore.login(loginForm.username, loginForm.password)
|
||
ElMessage.success('登录成功')
|
||
router.push('/')
|
||
} catch (error: any) {
|
||
const errorMessage = error.message || '登录失败,请检查用户名和密码'
|
||
ElMessage.error(errorMessage)
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- ✅ 登录页面自己处理并显示错误信息
|
||
- ✅ 错误消息来自后端返回的 `message` 字段
|
||
|
||
## 最终效果
|
||
|
||
### 登录成功
|
||
```
|
||
请求: POST /api/auth/login
|
||
响应: { code: 0, message: '登录成功', data: { user: {...}, access_token: '...', refresh_token: '...' } }
|
||
结果: 显示"登录成功",跳转到首页
|
||
```
|
||
|
||
### 登录失败(用户名或密码错误)
|
||
```
|
||
请求: POST /api/auth/login
|
||
响应: { code: 401, message: '用户名或密码错误', data: null }
|
||
结果: 显示"用户名或密码错误",停留在登录页
|
||
```
|
||
|
||
### 登录失败(账户被禁用)
|
||
```
|
||
请求: POST /api/auth/login
|
||
响应: { code: 401, message: '账户已被禁用,请联系管理员', data: null }
|
||
结果: 显示"账户已被禁用,请联系管理员",停留在登录页
|
||
```
|
||
|
||
### 其他接口 Token 过期
|
||
```
|
||
请求: GET /api/case/list
|
||
响应: { code: 401, message: 'Unauthorized', data: null }
|
||
结果: 显示"登录已过期,请重新登录",清空 token,跳转到登录页
|
||
```
|
||
|
||
## 总结
|
||
|
||
这个修改确保了:
|
||
1. **登录接口**:失败时返回友好的错误信息,不会触发自动跳转
|
||
2. **其他接口**:Token 过期时自动清理状态并跳转到登录页
|
||
3. **用户体验**:错误提示清晰准确,不会出现重复提示或循环跳转
|