1.实现效果

2.后端实现
2.1 Maven
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.28</version>
</dependency>
2.2 后端实现代码
package com.dragon.springboot3vue3.controller;
import cn.dev33.satoken.util.SaResult;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@Tag(name = "验证码接口")
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Operation(summary = "获取验证码")
@GetMapping("/get")
public SaResult capture(){
// 设置验证码图片长、宽、验证码字符数、干扰线宽度
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(100,30,4,10);
// 获取验证码
String code = lineCaptcha.getCode();
// 存入 redis
stringRedisTemplate.opsForValue().set("verifyCode",code,60, TimeUnit.SECONDS);
// 转为 Base64 字符串
String imageBase64Data = lineCaptcha.getImageBase64Data();
return SaResult.ok().setData(imageBase64Data);
}
}
3.前端实现

<template>
<div class="container">
<div class="loginContainer">
<!-- 登录表单 -->
<el-container v-if="loginFlag">
<el-header>
<h1 class="title">登录</h1>
</el-header>
<el-main>
<el-form class="form" ref="loginRef" :model="loginForm" :rules="rules" label-width="auto">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="用户名" @keydown.enter="login" :prefix-icon="User" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="密码" @keydown.enter="login" :prefix-icon="Lock" />
</el-form-item>
<el-form-item prop="verifyCode" class="verifyCode">
<div class="verifyCodeInput">
<el-input v-model="loginForm.verifyCode" placeholder="验证码" @keydown.enter="login" :prefix-icon="Message" />
</div>
<div class="verifyCodeImage">
<el-image :src="captchaSrc" @click="getCaptcha"></el-image>
</div>
</el-form-item>
</el-form>
</el-main>
<el-footer>
<div class="footer">
<div class="flex">
<el-checkbox v-model="loginForm.remenberFlag">记住我</el-checkbox>
<el-link type="primary" :underline="false">忘记密码?</el-link>
</div>
<el-button class="button" type="primary" @click="login">登录</el-button>
<el-link type="primary" :underline="false" @click="change">注册</el-link>
</div>
</el-footer>
</el-container>
<!-- 注册表单 -->
<el-container v-else>
<el-header>
<h1 class="title">注册</h1>
</el-header>
<el-main>
<el-form class="form" ref="registerRef" :model="registerForm" :rules="rules" label-position="right" label-width="auto">
<el-form-item label="用户名" prop="username">
<el-input v-model="registerForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="密码" prop="confirmPassword">
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="请输入确认密码" />
</el-form-item>
</el-form>
</el-main>
<el-footer>
<div class="footer">
<el-button class="button" type="primary" @click="register">注册</el-button>
<el-link type="primary" :underline="false" @click="change">登录</el-link>
</div>
</el-footer>
</el-container>
</div>
</div>
</template>
<script setup>
import {ref,reactive} from 'vue'
import userApi from '@/api/sys/user'
import captchaApi from '@/api/sys/captcha'
import router from '@/router';
import {useTokenStore} from '@/stores/token'
import { User,Lock,Message } from '@element-plus/icons-vue';
let loginFlag=ref(true)
const loginRef=ref()
let registerForm=reactive({
username:'',
password:'',
confirmPassword:''
})
let loginForm=reactive({
username:'',
password:'',
verifyCode:''
})
// 后端返回图片的Base64字符串
let captchaSrc=ref()
// 保存 registerFlag 和 registerForm 初始值(... 拷贝)
let initRegisterForm={ ...registerForm }
let initLoginForm={ ...loginForm }
// 注册
let register= async()=>{
await userApi.register(registerForm);
// 路由跳转
router.push('/login');
}
// 登录
let login= async()=>{
if (loginRef.value) {
loginRef.value.validate(async(valid) => {
if (valid) {
let response=await userApi.login(loginForm);
// 缓存Token
useTokenStore().set(response.data);
// 路由跳转
router.push('/home/index');
} else {
return false;
}
});
}
}
// 跳转并清空表单
let change= ()=>{
if(loginFlag.value){
loginFlag.value=false;
// 重置函数
Object.assign(registerForm, initRegisterForm);
}else{
loginFlag.value=true;
// 重置函数
Object.assign(loginForm, initLoginForm);
}
}
// 自定义校验规则函数
let checkConfirmPwd= (rule,value,callback)=>{
if(value===''){
callback(new Error("请输入确认密码"));
}else if(value!=registerForm.password){
callback(new Error("两次输入的密码不一致"));
}else{
callback();
}
}
// 定义校验规则(rules.username <-> prop='username')
let rules={
username:[
{ required: true, message:'请输入用户名', trigger: 'blur'},
{ min:3, max:16, message:"用户名长度3-16位", trigger: 'blur'}
],
password:[
{ required: true, message:'请输入密码', trigger: 'blur'},
{ min:6, max:16, message:"密码长度6-16位", trigger: 'blur'}
],
confirmPassword:[
{ required: true, validator:checkConfirmPwd, trigger:'blur' }
],
verifyCode:[
{ required: true, message:'请输入验证码', trigger: 'blur'}
],
}
let getCaptcha = async()=>{
let response=await captchaApi.get();
captchaSrc.value = response.data;
}
onMounted(()=>{
getCaptcha();
})
</script>
<style scoped lang="less">
.container{
background-image: url('@/assets/bg.jpg');
background-repeat: no-repeat;
background-size: cover;
height: 100vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.loginContainer{
height: 400px;
width: 400px;
padding: 10px;
// background-color: #fff;
/* 添加半透明背景色 */
background-color: rgba(255, 255, 255, 0.5);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.title{
color: black;
font-weight: 600;
display:flex;
justify-content:center;
}
.form{
display:flex;
flex-direction:column;
justify-content:center;
}
.footer{
display:flex;
flex-direction:column;
justify-content:center;
.flex{
display: flex;
justify-content: space-between;
}
.button{
width: 100%;
margin: 10px 0;
}
}
}
.verifyCode{
display: flex;
justify-content: space-between;
align-items: center;
}
.verifyCodeInput{
flex: 1;
}
.verifyCodeImage{
display: flex;
justify-content: space-between;
align-items: center;
margin-left: 50px;
}
</style>
4.登录时,后端校验用户名、密码、验证码
