吐司问卷:用户登陆
Date: February 17, 2025 4:12 PM (GMT+8)
JWT
**概念:**登陆成功后,服务端返回一个 token
JWT组成:
JWT 由三个部分组成:头部(Header)、载荷(Payload) 和 签名(Signature)。它们通过点(.
)连接,格式如下:
header.payload.signature
示例:
假设用户登录成功,后端生成了一个 JWT,格式如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
这个 JWT 分为三部分:
- 头部(Header):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- 载荷(Payload):
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
- 签名(Signature):
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 工作流程通常如下:
- 用户登录: 用户提供用户名和密码,后端验证身份。
- 生成 JWT: 登录成功后,后端生成 JWT,包含用户身份信息,并通过密钥签名,确保其不可篡改。
- 返回 JWT: 后端将 JWT 发送给客户端,客户端通常将 JWT 存储在浏览器的 LocalStorage 或 Cookie 中。
- 客户端请求: 客户端在后续请求中将 JWT 作为
Authorization
头传递给后端(通常是Authorization: Bearer <token>
)。 - 服务器验证 JWT: 服务器根据 JWT 中的签名和密钥验证 JWT 的合法性,如果合法,则执行相应的操作,否则返回错误信息。
项目的 JWT 的认证流程:
关键代码实现:
登录时存储token
Login.tsx
// 登录成功后处理
onSuccess(res) {
const { token = '' } = res
setToken(token) // 存储到localStorage
nav(MANAGE_INDEX_PATHNAME)
}
HTTP请求自动携带token
services/ajax.ts
// 请求拦截器
instance.interceptors.request.use(config => {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}` // 自动携带token
}
return config
})
路由守卫验证
useNavPage.ts
useEffect(() => {
if (username || token) { // 同时验证Redux状态和本地token
if (isLoginOrRegister(pathname)) {
nav(MANAGE_INDEX_PATHNAME)
}
return
}
// ...未登录处理
}, [waitingUserData, username, pathname])
自动用户认证
useLoadUserData.ts
// 自动加载用户信息
useEffect(() => {
run() // 使用存储的token获取用户信息
}, [])
退出登录处理 (UserInfo.tsx
)
const logout = () => {
removeToken() // 清除token
dispatch(logoutReducer()) // 清除Redux状态
nav(LOGIN_PATHNAME)
}
登陆注册流程
注册流程:
注册流程:
- 用户填写表单
- Register组件调用 registerService,请求后端接口
- 后端返回数据,显示登陆成功
- 用户跳转至登陆页面
登录流程:
- 用户填写表单
- Login组件调用 LoginService,请求后端接口
- 后端返回 token 和 用户信息
- 前端 Redux 更新用户信息,如果用户勾选记住密码的话,还需要本地存储信息
- 用户跳转问卷列表页
信息认证流程
认证流程
认证流程:
- 路由系统加载用户信息
- 路由守卫根据白名单进行鉴权,跳转对应页面
状态管理流程
状态管理流程:
- 基于JWT的token认证机制
- 自动登录状态保持
- 路由级别的权限控制
- 用户凭证的安全存储(localStorage)
- 全局状态管理(Redux + 自定义Hooks)
路由守卫
路由守卫管理流程:
- 路由分层:
- 将路由分为「无需鉴权」(登录/注册/首页)和「需要登录」两类
- 通过
isNoNeedUserInfo
工具函数维护白名单路径
- 权限判断:
// 检查路径是否在免登录白名单
if (需要用户信息 && 未登录) {
重定向到登录页
} else if (已登录 && 访问登录/注册页) {
重定向到问卷管理页
}
- 状态管理:
- 使用Redux全局存储用户信息
- 本地存储持久化token(通过
setToken
/removeToken
) - 登录后自动获取用户信息并更新Redux状态
- 跳转逻辑:
- 登录成功 → 管理首页
- 退出登录 → 登录页
- 未登录访问受保护路由 → 登录页(保留原路径便于登录后跳回)
实现要点:
- 通过布局组件实现路由守卫(MainLayout包裹需要鉴权的路由)
- 路由路径常量统一管理(避免硬编码)
- 支持登录后跳转回原访问页(通过路由状态保持)
- Token验证通过axios拦截器自动处理
用户访问鉴权流程图:
实现要点总结
1-凭证存储:
user-token.ts
// 使用localStorage存储token
export const setToken = (token: string) => {
localStorage.setItem(USER_TOKEN_KEY, token)
}
2-路由守卫逻辑:
useNavPage.ts
// 路由导航控制核心逻辑
useEffect(() => {
if (username || token) { // 已登录状态
if (isLoginOrRegister(pathname)) {
nav(MANAGE_INDEX_PATHNAME) // 禁止访问登录/注册页
}
} else { // 未登录状态
if (!isNoNeedUserInfo(pathname)) {
nav(LOGIN_PATHNAME) // 重定向到登录页
}
}
}, [waitingUserData, username, pathname])
3-自动登录机制:
useLoadUserData.ts
// 组件挂载时自动加载用户信息
useEffect(() => {
if (!username) { // 仅当Redux无用户信息时执行
run()
}
}, [])
开发用户的 mock 和 service
思路:
- 封装 axios 请求拦截器:增加用户token
- 设计 用户注册登陆接口
- 设计 工具函数 user-token:用于管理用户 token
- 设计 问卷页面 的当前用户显示与退出功能
封装 axios 请求拦截器:增加用户token
ajax.ts
import axios from 'axios'
import { message } from 'antd'
import { getToken } from '../utils/user-token'
const instance = axios.create({
timeout: 10000,
})
instance.interceptors.request.use(
config => {
const token = getToken()
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
instance.interceptors.response.use(res => {
const resData = (res.data || {}) as ResType
console.log('resData', resData)
const { errno, data, msg } = resData
if (errno !== 0) {
if (msg) {
message.error(msg)
}
throw new Error(msg || '未知错误')
}
return data as any
})
export default instance
export type ResType = {
errno: number
data?: ResDataType
msg?: string
}
// key表示字段名,any表示字段值的类型
export type ResDataType = {
[key: string]: any
}
设计 用户注册登陆接口:
user.ts
import axios, { ResDataType } from './ajax'
// 获取用户信息
export async function getUserInfoService(): Promise<ResDataType> {
const url = '/api/user/info'
const data = (await axios.get(url)) as ResDataType
return data
}
// 注册用户
export async function registerService(
username: string,
password: string,
nickname?: string
): Promise<ResDataType> {
const url = '/api/user/register'
const body = {
username,
password,
nickname: nickname || username,
}
const data = (await axios.post(url, body)) as ResDataType
return data
}
// 登陆用户
export async function loginService(
username: string,
password: string
): Promise<ResDataType> {
// Updated return type
const url = '/api/user/login'
const body = {
username,
password,
}
const data = (await axios.post(url, body)) as ResDataType
return data
}
设计工具函数 user-token:用于管理用户 token:
user-token.ts
/**
* @description 管理用户token
*/
const KEY = 'USER_TOKEN'
export function setToken(token: string) {
localStorage.setItem(KEY, token)
}
export function getToken() {
return localStorage.getItem(KEY) || ''
}
export function removeToken() {
localStorage.removeItem(KEY)
}
设计 问卷页面 的当前用户显示与退出功能:
userInfo.tsx
import React, { FC } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { LOGIN_PATHNAME } from '../router'
import { Button, message } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import { getUserInfoService } from '../services/user'
import { removeToken } from '../utils/user-token'
import { useRequest } from 'ahooks'
const UserInfo: FC = () => {
const nav = useNavigate()
const { data } = useRequest(getUserInfoService)
const { username, nickname } = data || {}
const logout = () => {
removeToken()
message.success('退出成功')
nav(LOGIN_PATHNAME)
}
const UserInfo = () => {
return (
<>
<span style={{ color: '#e8e8e8' }}>
<UserOutlined />
{nickname || username}
</span>
<Button type="link" onClick={logout}>
退出
</Button>
</>
)
}
const Login = () => {
return (
<>
<Link to={LOGIN_PATHNAME}>登陆</Link>
</>
)
}
return <div>{username ? <UserInfo /> : <Login />}</div>
}
export default UserInfo
fix: 成功登录再次闪回
问题:
原因分析:
分析思路:
1-登录流程:
- 用户登录成功后,
setToken(token)
将 token 保存到 localStorage - 然后导航到问卷列表页面
nav(MANAGE_INDEX_PATHNAME)
2-路由守卫检查:
- 在 useNavPage.ts 中:
if (username) {
// 允许访问
} else {
nav(LOGIN_PATHNAME) // 重定向到登录页
}
这里只检查了 username,没有检查 token
关键问题:
- 登录时只保存了 token,没有同时保存/更新用户名
- 当导航到问卷列表页面时,useNavPage 钩子发现没有 username,就立即重定向回登录页
解决方案:
1-修改 useNavPage.ts
检查逻辑
import { useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import {
isLoginOrRegister,
isNoNeedUserInfo,
MANAGE_INDEX_PATHNAME,
LOGIN_PATHNAME,
} from '../router'
import useGetUserInfo from './useGetUserInfo'
import { getToken } from '../utils/user-token' // 导入获取token函数
function useNavPage(waitingUserData: boolean) {
const { username } = useGetUserInfo()
const { pathname } = useLocation()
const nav = useNavigate()
// 获取token状态
const token = getToken()
useEffect(() => {
if (waitingUserData) return
// 同时检查username和token
if (username || token) {
if (isLoginOrRegister(pathname)) {
nav(MANAGE_INDEX_PATHNAME)
}
return
}
if (isNoNeedUserInfo(pathname)) {
return
} else {
nav(LOGIN_PATHNAME)
}
}, [waitingUserData, username, pathname])
}
export default useNavPage
2-修改登录成功回调,手动更新Redux状态
Login.tsx
import { useDispatch } from 'react-redux'
import { loginReducer } from '../store/userReducer' // 确保路径正确
const Login: FC = () => {
// ...已有代码...
const dispatch = useDispatch()
const { run } = useRequest(
async (username: string, password: string) => {
const data = await loginService(username, password)
return data
},
{
manual: true,
onSuccess(res) {
const { token = '', username } = res
// 1. 保存token
setToken(token)
// 2. 立即更新Redux中的用户信息
dispatch(loginReducer({ username }))
// 3. 短暂延迟确保状态更新后再导航
setTimeout(() => {
message.success('登录成功')
nav(MANAGE_INDEX_PATHNAME)
}, 100)
},
}
)
// ...其余代码不变...
}