从零实现一个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。然后会转到登录页,我们正常登录,如下图跳转成功。

相关推荐
ClareXi2 小时前
react项目通过http调用后端springboot服务最简单示例
spring boot·react.js·http
咔咔库奇14 小时前
react动态路由
前端·react.js·前端框架
yqcoder15 小时前
react 中 FC 模块作用
前端·react.js·前端框架
刘志辉16 小时前
react的创建与书写
前端·react.js·前端框架
奔跑草-19 小时前
【前端】深入浅出的React.js详解
前端·react.js·前端框架
小牛itbull20 小时前
ReactPress:深入解析技术方案设计与源码
javascript·react.js·reactpress
秃头女孩y20 小时前
【React】条件渲染——逻辑与&&运算符
前端·react.js·前端框架
小满zs20 小时前
React第十五章(useEffect)
前端·react.js
破浪前行·吴1 天前
使用@react-three/fiber,@mkkellogg/gaussian-splats-3d加载.splat,.ply,.ksplat文件
前端·react.js·three.js
咔咔库奇1 天前
react之了解jsx
前端·javascript·react.js