前言
本系统在未存储token
或refreshToken
过期时会跳到登录界面,本文就来着手实现登录模块。关键点主要是
1.返回的svg图形验证码
如何展示
2.记住密码的功能如何实现
3.密码不可明文存储,要加密
4.登录后如何回到上次浏览的界面
静态登录页实现
实现静态登录页我们用到了Antd5.0
的Form表单组件。这里大概梳理一下用到的组件属性
Form
labelCol
、wrapperCol
:采用了栅格布局,分别设置标签和输入控件的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
登录异步方法存储token
和refreshToken
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。然后会转到登录页,我们正常登录,如下图跳转成功。