吐司问卷(React低代码):用户登陆模块实现

吐司问卷:用户登陆

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 分为三部分:

  1. 头部(Header)eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  2. 载荷(Payload)eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
  3. 签名(Signature)SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT 工作流程通常如下:

  1. 用户登录: 用户提供用户名和密码,后端验证身份。
  2. 生成 JWT: 登录成功后,后端生成 JWT,包含用户身份信息,并通过密钥签名,确保其不可篡改。
  3. 返回 JWT: 后端将 JWT 发送给客户端,客户端通常将 JWT 存储在浏览器的 LocalStorage 或 Cookie 中。
  4. 客户端请求: 客户端在后续请求中将 JWT 作为 Authorization 头传递给后端(通常是 Authorization: Bearer <token>)。
  5. 服务器验证 JWT: 服务器根据 JWT 中的签名和密钥验证 JWT 的合法性,如果合法,则执行相应的操作,否则返回错误信息。

项目的 JWT 的认证流程:

image.png

关键代码实现:

登录时存储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,请求后端接口
  • 后端返回数据,显示登陆成功
  • 用户跳转至登陆页面

image.png

用户界面 Register组件 注册服务 后端API 填写注册表单 调用registerService(username, password, nickname) POST /api/user/register 返回注册结果 注册成功 跳转至登录页(LOGIN_PATHNAME) 用户界面 Register组件 注册服务 后端API

登录流程:

  • 用户填写表单
  • Login组件调用 LoginService,请求后端接口
  • 后端返回 token 和 用户信息
  • 前端 Redux 更新用户信息,如果用户勾选记住密码的话,还需要本地存储信息
  • 用户跳转问卷列表页

image.png

用户界面 Login组件 登录服务 后端API Redux 本地存储 localStorage 填写登录表单 调用loginService(username, password) POST /api/user/login 返回token和用户信息 dispatch(loginReducer) setToken(token) 保存用户名密码 清除保存的凭证 alt [记住密码] 跳转至问卷列表页(MANAGE_INDEX_PATHNAME) 用户界面 Login组件 登录服务 后端API Redux 本地存储 localStorage



信息认证流程

认证流程

认证流程:

  • 路由系统加载用户信息
  • 路由守卫根据白名单进行鉴权,跳转对应页面

image.png

路由系统 useNavPage Hook useLoadUserData Hook 用户服务 后端API Redux 加载用户数据 调用getUserInfoService() GET /api/user/info 返回用户信息 更新用户状态 返回加载状态 执行路由守卫 访问登录页则跳转管理页 需要登录的页面跳转登录页 alt [已登录状态] [未登录状态] 路由系统 useNavPage Hook useLoadUserData Hook 用户服务 后端API Redux


状态管理流程

状态管理流程:

  1. 基于JWT的token认证机制
  2. 自动登录状态保持
  3. 路由级别的权限控制
  4. 用户凭证的安全存储(localStorage)
  5. 全局状态管理(Redux + 自定义Hooks)

image.png

登录成功
存储token到localStorage
存储用户信息到Redux
路由跳转至管理页
页面刷新
自动加载用户信息
Redux中有数据?
维持当前路由
发起用户信息请求
更新Redux状态


路由守卫

路由守卫管理流程:

  1. 路由分层
  • 将路由分为「无需鉴权」(登录/注册/首页)和「需要登录」两类
  • 通过isNoNeedUserInfo工具函数维护白名单路径
  1. 权限判断
// 检查路径是否在免登录白名单
if (需要用户信息 && 未登录) {
  重定向到登录页
} else if (已登录 && 访问登录/注册页) {
  重定向到问卷管理页
}
  1. 状态管理
  • 使用Redux全局存储用户信息
  • 本地存储持久化token(通过setToken/removeToken
  • 登录后自动获取用户信息并更新Redux状态
  1. 跳转逻辑
  • 登录成功 → 管理首页
  • 退出登录 → 登录页
  • 未登录访问受保护路由 → 登录页(保留原路径便于登录后跳回)

实现要点:

  1. 通过布局组件实现路由守卫(MainLayout包裹需要鉴权的路由)
  2. 路由路径常量统一管理(避免硬编码)
  3. 支持登录后跳转回原访问页(通过路由状态保持)
  4. Token验证通过axios拦截器自动处理

用户访问鉴权流程图:

image.png



实现要点总结

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

image.png

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: 成功登录再次闪回

问题:

2025-03-07 10.39.19.gif

原因分析:

分析思路:

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)
      },
    }
  )
  // ...其余代码不变...
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值