从零实现一个React+Antd5.0后台管理系统-登录模块

前言

本系统在未存储tokenrefreshToken过期时会跳到登录界面,本文就来着手实现登录模块。关键点主要是

1.返回的svg图形验证码如何展示

2.记住密码的功能如何实现

3.密码不可明文存储,要加密

4.登录后如何回到上次浏览的界面

静态登录页实现

实现静态登录页我们用到了Antd5.0的Form表单组件。这里大概梳理一下用到的组件属性

Form

  • labelColwrapperCol:采用了栅格布局,分别设置标签和输入控件的span占位格数和offset左侧的间隔格数等
  • form: 经 Form.useForm() 创建的 form 控制实例,不提供时会自动创建
  • initialValues:传入的是一个对象,可以设置表单的默认值
  • onFinish:提交表单且数据验证成功后回调事件

Form.Item

  • name:字段名
  • rules:设置字段的校验逻辑

然后我们大概调整一下样式,代码如下:

src/pages/Login/index.jsx

js 复制代码
import React, { useEffect, useState } from 'react'
import { Button, Checkbox, Form, Input } from 'antd'
​
import classes from './Login.module.scss'
​
const Login = () => {
  /** 登录表单参数与方法 */
  // 获取antd的form实例
  const [form] = Form.useForm()
  // 按钮loading
  const [loading, setLoading] = useState(false)
​
  const onFinish = async (values) => {
    console.log(values)
  }
​
  // 登录页显示欢迎语
  const [welcomeText, setWelcomeText] = useState('')
  const typeWriter = (wordSplit, index) => {
    if (index < wordSplit.length - 1) {
      setWelcomeText((pre) => {
        index++
        return pre + wordSplit[index]
      })
      setTimeout(() => typeWriter(wordSplit, index), 200)
    }
  }
​
  useEffect(() => {
    let index = -1
    const wordSplit = 'Welcome'.split('')
    typeWriter(wordSplit, index)
  }, [])
​
  return (
    <div className={classes.login}>
      <div className={classes['login-container']}>
        <div className={classes['login-text']}>{welcomeText}</div>
        <div className={classes['login-form']}>
          <Form
            name="basic"
            form={form}
            labelCol={{ span: 8 }}
            wrapperCol={{ span: 16 }}
            initialValues={{ remember: false }}
            onFinish={onFinish}
            autoComplete="off">
            <Form.Item label="用户名" name="username" rules={[{ required: true, max: 12, message: '请输入用户名!' }]}>
              <Input />
            </Form.Item>
​
            <Form.Item label="密码" name="password" rules={[{ required: true, message: '请输入密码!' }]}>
              <Input.Password />
            </Form.Item>
​
            <Form.Item
              label="验证码"
              name="checkCode"
              rules={[{ required: true, min: 4, max: 4, message: '请输入正确格式验证码!' }]}>
              <Input />
            </Form.Item>
​
            <Form.Item name="remember" valuePropName="checked" wrapperCol={{ offset: 8, span: 16 }}>
              <Checkbox>记住密码</Checkbox>
            </Form.Item>
​
            <Form.Item wrapperCol={{ offset: 10, span: 16 }}>
              <Button type="primary" htmlType="submit" loading={loading}>
                登录
              </Button>
              <Button htmlType="reset" style={{ marginLeft: '32px' }}>
                重置
              </Button>
            </Form.Item>
          </Form>
        </div>
      </div>
    </div>
  )
}
export default Login

展示图形验证码

1.请求接口获取svg验证码

接口获取验证码需要唯一键值uuid,我们这里先用当前时间戳。然后再设置一个变量来存储验证码图片(格式为svg)

js 复制代码
// 导入api
import userApi from '@/api/user'
...
// 验证码uuid
const [uuid, setUuid] = useState()
// 验证码svg标签
const [verifyImg,setVerifyImg]=useState()
// 获取验证码
const getCode = () => {
  const uuid = new Date().getTime()
  setUuid(uuid)
  userApi.login.get(uuid).then((res) => {
    setVerifyImg(res.data)
  })
}
useEffect(() => {
  ...
  getCode()
}, [])

2.展示图形验证码

展示html结构在vue中可能有小伙伴知道用v-html指令。在React中也有类似的属性dangerouslySetInnerHTML,但从名称也可以看出其不当使用是有危险性的。我们在表单项输入框的后缀直接展示验证码。

js 复制代码
<Form.Item
  label="验证码"
  name="checkCode"
  rules={[{ required: true, min: 4, max: 4, message: '请输入正确格式验证码!' }]}>
  <Input
+    suffix={
+      <div
+        dangerouslySetInnerHTML={{ __html: verifyImg }}
+        className="login-captcha"
+        onClick={() => getCode()}></div>
    }
  />
</Form.Item>

验证码就能够展示在验证码输入框,效果如下图。

记住密码

记住密码的功能步骤分为两步

1.勾选了记住密码,点击登录将信息存储到浏览器localstorage,设置过期期限7天

2.未勾选记住密码,点击登录清空浏览器存储中的信息

javascript 复制代码
const onFinish = async (values) => {
  // 开始loading
  setLoading(true)
  // 记住密码存储用户信息
  if (values.remember) {
    localStorage.setItem('username', values.username, { expires: 7})
    localStorage.setItem('password', values.password, { expires: 7})
    localStorage.setItem('remember', values.remember, { expires: 7})
    } else {
  // 移除用户信息
    localStorage.removeItem('username')
    localStorage.removeItem('password')
    localStorage.removeItem('remember')
  }
  console.log(values)
}

然后当我们每次进入登录页的时候,去浏览器存储中取信息,若有则回显到表单。

javascript 复制代码
// 获取缓存中的用户名密码信息
const getUser = () => {
  const username = localStorage.getItem('username') ?? ''
  const password = localStorage.getItem('password') ?? ''
  const remember = localStorage.getItem('remember') ?? false
  // 通过antd表单实例对象的setFieldsValue方法回显值
  form.setFieldsValue({
    username,
    password,
    remember: Boolean(remember)
  })
}
useEffect(() => {
  ...
  getUser()
}, [])

就能看到以下效果,刷新重新进入能够回显上次记住的信息。

密码加密

但记住密码后可以发现密码存储是明文的,这对于用户密码的安全性是有威胁的。我们可以对其进行加密后再进行存储,再次获取信息时进行解密。

1.安装jsencrypt

css 复制代码
npm i jsencrypt@3

2.用RSA的密钥对(不对称加密【公钥加密,私钥解密】)对jsencrypt库进行封装,导出加解密的方法

src/utils/jsencrypt.js

javascript 复制代码
import { JSEncrypt } from 'jsencrypt'
​
// 密钥对生成 http://web.chacuo.net/netrsakeypair
​
const publicKey =
  '-----BEGIN PUBLIC KEY-----\n' +
  ....此处用上面生成的公钥
  '-----END PUBLIC KEY-----'
​
const privateKey =
  '-----BEGIN PRIVATE KEY-----\n' +
  ....此处用上面生成的私钥
  '-----END PRIVATE KEY-----'
​
// 加密
export function encrypt(txt) {
  const encryptor = new JSEncrypt()
  encryptor.setPublicKey(publicKey) // 设置公钥
  return encryptor.encrypt(txt) // 对数据进行加密
}
​
// 解密
export function decrypt(txt) {
  const encryptor = new JSEncrypt()
  encryptor.setPrivateKey(privateKey) // 设置私钥
  return encryptor.decrypt(txt) // 对数据进行解密
}

3.登录组件中使用

src/pages/Login/index.jsx

javascript 复制代码
...
// 导入加密解密方法
import { encrypt, decrypt } from '@/utils/jsencrypt'
...
// 提交方法
const onFinish = async (values) => {
  // 记住密码存储用户信息
  if (values.remember) {
    localStorage.setItem('username', values.username, { expires: 7})
+   localStorage.setItem('password', encrypt(values.password), { expires: 7 })
...
}
// 获取缓存中的用户名密码信息
const getUser = () => {
  ...
  form.setFieldsValue({
    username,
    password:decrypt(password),
}

提交

以上步骤都进行完后就可以提交填写的信息到接口。提交接口我们直接分发到之前封装好的Redux登录异步方法存储tokenrefreshToken

javascript 复制代码
...
 import { useNavigate } from 'react-router-dom'
 import { useDispatch } from 'react-redux'
 import {loginAsync} from '@/store/reducers/userSlice'
 import {message} from 'antd'
...
 const dispatch = useDispatch()
 const navigate = useNavigate()
// 提交方法
const onFinish = async (values) => {
  ...
+  try {
+    await dispatch(loginAsync({ ...values, uuid }))
+    setLoading(false)
+    // 跳转到首页
+    navigate('/',{replace:true})
+    message.success('登录成功!')
+  } catch (e) {
+    console.error(e)
+    getCode()
+    setLoading(false)
}
}

然后输入我提供的默认账号:Alan,密码:123456和验证码点击登录即可跳转至首页

登录重定向至上次浏览页面

但是我们每次点击登录都跳到首页用户体验是不太好的,比如我上次输入了网址http://localhost:3000/system/user登录之后应该是跳到用户管理的页面。

会发生跳转登录页面的情况就是无token,那我们只要在它跳转的时候带一个参数为当前链接即可。

src/App.jsx

javascript 复制代码
import {useLocation} from 'react-router-dom'
useEffect(() => {
  const fetchData = async () => {
    if(getToken()){...}
    else{
      // 传递整个location作为参数
+       navigate('/login', { replace: true, state: { preLocation: location } })
    }
  }
  fetchData()
}, [dispatch])

然后我们在登录页用location.state?.preLocation?.pathname接收跳转路径即可,然后点击登录跳转至此路径

这里用可选链是因为不一定所有跳转登录页的情况都携带preLocation参数

src/page/Login/index.jsx

javascript 复制代码
const location = useLocation()
const from = location.state?.preLocation?.pathname || '/'
// 提交方法
const onFinish = async (values) => {
  ...
+  // 若上次为登录页,跳转到首页
+ if (from === '/login') navigate('/', { replace: true })
+ navigate(from, { replace: true })
  ...
}

然后我们把localStorage的缓存清掉,输入urlhttp://localhost:3000/system/user。然后会转到登录页,我们正常登录,如下图跳转成功。

相关推荐
牛仔只喝牛奶10 小时前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js
wear工程师13 小时前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
wanghowie15 小时前
LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展
前端·react.js·前端框架
csj5016 小时前
前端基础之《React(11)—state声明式》
前端·react.js
张元清17 小时前
React useLocalStorage Hook:SSR 安全的持久化状态(2026)
javascript·react.js
析数塔17 小时前
2026前端框架深度解析:React Compiler、Angular Zoneless、Vue 3.5重写游戏规则
前端·vue.js·react.js
杖雍皓17 小时前
没有服务器如何构建现代 Web 应用:平台选型与组合策略
运维·服务器·前端·react.js·github·netlify·vercel
sun_weitao17 小时前
微前端框架之qiankun 怎么用?vue3+vite+qiankun 和 react + qiankun
前端·react.js·前端框架
名字还没想好☜1 天前
React useEffect 依赖数组踩坑:闭包陷阱与清理函数
前端·javascript·react.js·react·useeffect
我是大卫2 天前
【图】React源码解析-第四部分:事件与跨层传递
react.js·源码