Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/frontend/login.md
T
2026-10-08 17:50:29 +08:00

137 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 9.8 登录流程
:::tip 概述
本节说明前端登录流程的实现,包括登录表单提交、验证码联调、Token 存储、路由守卫和登出处理。
:::
## 前端登录流程
```text
1. 用户输入用户名、密码、验证码
2. 调用 POST /api/login 接口(参数:username / password / key / code)
3. 成功 → 存储 access_token 到 localStorage(键名 ACCESS-TOKEN,仅存访问令牌)
4. 路由守卫检测到 Token → 获取用户信息(GET /api/index/getUser)
5. 生成动态路由 → 跳转首页
```
## 登录页面代码
```vue
<!-- src/views/login/index.vue -->
<template>
<el-form :model="loginForm" @submit.prevent="handleLogin">
<el-form-item>
<el-input v-model="loginForm.username" placeholder="用户名" />
</el-form-item>
<el-form-item>
<el-input v-model="loginForm.password" type="password" placeholder="密码" />
</el-form-item>
<el-form-item>
<el-input v-model="loginForm.code" placeholder="验证码" />
<img :src="captchaImage" @click="refreshCaptcha" style="cursor: pointer" />
</el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form>
</template>
<script setup>
import { useUserStore } from '@/store/modules/user';
import { getCaptcha } from '@/api/common/user';
const userStore = useUserStore();
const loginForm = ref({ username: '', password: '', code: '', key: '' });
const captchaImage = ref('');
// 获取验证码
// 响应字段:key = 验证码标识,captcha = Base64 图片
const refreshCaptcha = async () => {
const res = await getCaptcha();
loginForm.value.key = res.key;
captchaImage.value = res.captcha;
};
// 登录
const handleLogin = async () => {
await userStore.login(loginForm.value);
router.push('/');
};
onMounted(() => refreshCaptcha());
</script>
```
:::warning 字段命名约定(与后端一致)
- 验证码接口 `/api/captcha` 返回 **`key` + `captcha`**(不是 `image`)
- 登录接口 `/api/login` 接收 **`key` + `code`**(不是 `captchaKey` / `captchaCode`)
- 登录响应 data 为 **`access_token` / `refresh_token`**(snake_case,登录接口 raw 返回,见 [8.6 Token 使用说明](/zh/api-spec/token.html))
- 登录失败(含验证码错误)应**刷新验证码并清空输入**
:::
## 完整登录示例(含失败处理)
```typescript
const loading = ref(false);
const handleLogin = async () => {
await formRef.value.validate();
loading.value = true;
try {
await userStore.login(loginForm.value);
ElMessage.success('登录成功');
router.push('/');
} catch (e: any) {
// 登录失败(验证码错误/密码错误等),刷新验证码
ElMessage.error(e.message || '登录失败');
refreshCaptcha();
loginForm.value.code = '';
} finally {
loading.value = false;
}
};
```
### 刷新验证码的时机
| 时机 | 说明 |
|------|------|
| 页面加载时 | `onMounted` 自动获取 |
| 点击图片时 | 用户手动刷新 |
| 登录失败后 | 一次性已消费,自动刷新并清空输入 |
| 超过 5 分钟 | 验证码过期(`CAPTCHA_EXPIRE=300`),用户点击刷新 |
## Token 存储
```typescript
// 登录成功后存储(摘自 ui/src/store/modules/user.ts,仅存 access_token)
const ex = 7 * 24 * 60 * 60 * 1000;
storage.set(ACCESS_TOKEN, data.access_token, ex); // 键名 'ACCESS-TOKEN'
// 请求拦截器自动携带(ui/src/utils/http/axios/index.ts)
config.headers['Authorization'] = `Bearer ${storage.get('ACCESS-TOKEN', '')}`;
// 响应拦截器检测 401 → 弹窗提示 → 清存储 → 跳转登录页
// (本项目未内置自动续签,见 8.6 Token 使用说明)
```
## 登出
```typescript
// userStore.logout():POST /api/logout(携带 access_token)
// 后端同时执行 revokeToken()(吊销 refresh_token,Redis jti 删除)
// 前端 storage.remove(ACCESS_TOKEN) / remove(CURRENT_USER) / storage.clear() → 跳转登录页
```
:::tip 登出必须调接口
本项目 refresh_token 存 Redis(有状态)——**必须调用 `/api/logout`** 才能让服务端令牌即时失效;仅清前端 localStorage 的话,旧 refresh_token 在 7 天内仍可用于续签。
:::
## 常见问题
| 现象 | 原因 | 处理 |
|------|------|------|
| 登录提示验证码错误 | 字段名不符(key/code)或已过期 | 按约定传参;失败后刷新验证码 |
| 登录成功但刷新页面回到登录页 | Token 未正确存入 Storage | 检查 `storage.set` 与拦截器读取键名 |
| 登录接口 500 | 后端 Redis 未启动 / JWT_SECRET 异常 | 见 [2.9 常见问题 FAQ](/zh/quickstart/faq.html) |
| 登录后菜单空白 | 用户无角色 | 角色管理分配菜单(见 [9.5 路由与动态菜单](/zh/frontend/router.html)) |