4.用户登录业务实现
hutool工具类:比较器 - 比较工具-CompareUtil - 《Hutool v5.6.0 参考文档》 - 书栈网 · BookStack
一.创建文件
1.再mapper.sysuser包下创建 : SysUserMapper.接口,同时创建对应mapper.xml

SysUserMapper注册到mybatis
package com.zhexin.mapper.sysuser;
import org.apache.ibatis.annotations.Mapper;
@Mapper //注册到mybatis
public interface SysUserMapper {
}
SysUserService注册为bean @Service
import com.zhexin.manage.service.SysUserService;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl implements SysUserService {
}
这里的包名写错了,改成:不然以后多了分不清

二.看我们的实体类
1.登录接受接口实体类
找到用户上传的数据包:dto

2.登录返回接口实体类
数据包:vo
token:返回一段唯一身份证号

3.返回数据格式类
这样返回的数据就是这样的:返回固定格式
// code:自定义的业务状态码,前端会根据具体的业务状态码给出不同的处理。比如:200表示成功、非200都是失败
// message:响应消息。比如:登录成功、用户名或者密码错误、用户无权限访问
// data:后端返回给前端的业务数据
我们需要所有的接口都返回Result格式,就可以把里面的data设置为我们返回的对象

//常量

三.业务分析
流程图信息:https://pixso.cn/app/share/p/1e1rb9KNuq7aVNswdh5M6F1SQSaiV4Ud
据说localStroage比Cooker好用

四.创建接口controller
1.导入bean, 注册post请求,返回统一结果结构
package com.zhexin.manage.controller;
import com.zhexin.manage.service.SysUserService;
import com.zhexin.model.dto.system.LoginDto;
import com.zhexin.model.vo.common.Result;
import com.zhexin.model.vo.common.ResultCodeEnum;
import com.zhexin.model.vo.system.LoginVo;
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.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "用户接口") //swagger文档注释
@RestController //注册到bean
@RequestMapping("/admin/system/index") //注册访问路径
public class IndexController {
//导入用户登录业务类bean
@Autowired
SysUserService sysUserService;
@Operation(summary = "用户登录方法") //文档注释
@PostMapping("/login") //login接口地址 post请求 ,返回通一结构Result
public Result login(@RequestBody LoginDto logindto){ //@RequestBody表示传入有个json
//调用业务实现类
LoginVo login = sysUserService.Login(logindto);
//返回结果,和提示
return Result.build(login, ResultCodeEnum.SUCCESS);
}
}
2.编写业务类:
1.添加: SysUserService接口和实现类
//登录业务
LoginVo Login(LoginDto loginDto);
//登录业务
@Override
public LoginVo Login(LoginDto loginDto) {
LoginVo loginVo = new LoginVo();
//取到账号密码
//查询数据库用户
//密码转md5 ,可以再前端生成
//判断有没有结果
//先查用户名,有没有用户
//查密码是否正确
//生成token
//存储到redis
//返回loginVO对象
return loginVo;
}
2. 实现实体类业务代码
需要做的事情:
- 取到账号密码
- 查询数据库用户
-
密码转md5 ,可以再前端生成
- 判断有没有结果
- 先查用户名,有没有用户
- 查密码是否正确
- 生成token
- 存储到redis
- 返回loginVO对象
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson2.JSON;
import com.zhexin.manage.mapper.sysuser.SysUserMapper;
import com.zhexin.manage.service.SysUserService;
import com.zhexin.model.dto.system.LoginDto;
import com.zhexin.model.entity.system.SysUser;
import com.zhexin.model.vo.system.LoginVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.concurrent.TimeUnit;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired //@Mapper会自动注册为bean
private SysUserMapper sysUserMapper;
@Autowired //加载redis
private RedisTemplate<String,String> redisTemplate;
//登录业务
@Override
public LoginVo Login(LoginDto loginDto) {
LoginVo loginVo = new LoginVo();
//1.取到账号密码
String inputUserName = loginDto.getUserName();
String inputUserPassword = loginDto.getPassword();
//2.查询数据库用户
SysUser datebeasSysuser = sysUserMapper.SelectUserinfoByname(inputUserName);
//判断有没有结果
if (datebeasSysuser == null) {
throw new RuntimeException("有没有找到该用户,请先注册!!");
}
//密码转md5 ,可以再前端生成
inputUserPassword = DigestUtils.md5DigestAsHex(inputUserPassword.getBytes());
//3.查密码是否正确
if (!datebeasSysuser.getPassword().equals(inputUserPassword)) {
throw new RuntimeException("密码错误,请检查!!");
}
//4.生成token 9e87272b-8877-4ccb-beb7-c5afb14b1f7d 并删除:-
String token = UUID.randomUUID().toString().replaceAll("-", "");
//5.存储到redis key,value, 过期时间 , 单位天
redisTemplate.opsForValue().set("用户token:"+token , JSON.toJSONString("loginDto") , 7 , TimeUnit.DAYS);
//6.存入并返回loginVO对象
loginVo.setToken(token);
return loginVo;
}
}
3. 编写数据库映射文件:

1.加入约束
文件头
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--对应的接口别弄错了-->
<mapper namespace="com.zhexin.manage.mapper.sysuser.SysUserMapper">
</mapper>
2.这样可以生成sql语句:
测试生成的代码

3.写到代码中:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--对应的接口别弄错了-->
<mapper namespace="com.zhexin.manage.mapper.sysuser.SysUserMapper">
<!--用户名查询用户-->
<!--接口名称--> <!--resultType:返回值类型-->
<select id="SelectUserinfoByname" resultType="com.zhexin.model.entity.system.SysUser">
<!--显示所有表,查询sys_user ,根据name=***-->
SELECT * FROM sys_user WHERE name=#{inputUserName}
</select>
</mapper>
4.测试代码:
先将idea连接到数据库

要加引号!!!!

结果:

name 改为:username
5.转为sql字段:
这个*转为字段,为什么我也不知道,可能与username userName有关
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
id,username userName ,password,name,phone,avatar,description,status,create_time,update_time,is_deleted
</sql>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--对应的接口别弄错了-->
<mapper namespace="com.zhexin.manage.mapper.sysuser.SysUserMapper">
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
id,username userName ,password,name,phone,avatar,description,status,create_time,update_time,is_deleted
</sql>
<!--用户名查询用户-->
<!--接口名称--> <!--resultType:返回值类型-->
<select id="SelectUserinfoByname" resultType="com.zhexin.model.entity.system.SysUser">
<!--显示所有表,查询sys_user ,根据name=***-->
<!-- <include插入字段-->
SELECT <include refid="columns"/> FROM sys_user WHERE username = #{inputUserName}
</select>
</mapper>
这里我的‘id’字段报错,先用*代替后期调用的时候在回来替换测试!!!
-----------后来我测试之后发现虽然编写时报错但是,运行成功了,这可能是mysql解释器不识别他是个mysql语句吧
虽然用 * 的效果一致,但是我们还是保持和老师的一样吧!!

五.swrgger接口测试
1. 运行:

<mybatis.v>3.0.3</mybatis.v>
运行成功:

访问文档地址:localhost:10001/doc.html
来到这个界面:

运行

报错:数据库连接池有问题

原来是密码写错了,原先是root

2.再次运行,报错


最终剔除lettuce,改成jedis ,相关文章:Spring Data Redis 是如何在 Jedis 和 Lettuce 之间切换的?_springboot中spring-data-redis中将lettcue替换成jedis_码农StayUp的博客-CSDN博客
<!-- redis缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入Jedis客戶端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
3.最终成功

账号密码也可以验证

4.最后优化一下
1. 用不到的"refresh_token": null
不让他返回nulll,而是返回空字符串,,添加
loginVo.setRefresh_token("");
return loginVo;
2.这里并没有启动我们的代码设置的名称:


原因:我们用
设置为配置类,
但是spring的包在的包,和配置类的包不在一起,所以,spring扫描不到他

解决方案:
1.修改包名达到一致,但是这样分层就没有意义了
2.注册扫描的包,在spring启动类上添加:
@ComponentScan(basePackages = {"com.zhexin"})点一下可以导航的到就对了

运行看效果,成功

六.统一异常处理
实现方法很多种,这里老师用@ControllerAdvice和@ExceptionHandler实现
我之前的方法是,直接继承返回类,然后返回类中有关于返回异常的方法,直接子类调用父类方法就行了
1.捕获所有通用异常:
import com.zhexin.model.vo.common.Result;
import com.zhexin.model.vo.common.ResultCodeEnum;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice //Controller的加强注解,捕获controller的异常
@ResponseBody //类似于@RequestBody是传入,这个是返回一个json格式
public class ExceptionResult {
@ExceptionHandler(Exception.class) //当Controller发生Exception是调用此方法
public Result error(Exception e) {
//打印异常
e.printStackTrace();
//统一Result格式,"抛出数据异常"
return Result.build(null, ResultCodeEnum.DATA_ERROR);
}
}
2.捕获自定义异常:
1.创建自定义异常
需要继承某个异常才可以抛出异常
import com.zhexin.model.vo.common.ResultCodeEnum;
import lombok.Data;
@Data
public class zhexinException extends RuntimeException {
private Integer code; //错误码
private String message; //消息
// private ResultCodeEnum resultCodeEnum; //错误枚举类
//手动错误
public zhexinException(Integer code, String message) {
this.code = code;
this.message = message;
}
//枚举错误
public zhexinException(ResultCodeEnum resultCodeEnum) {
this.code = resultCodeEnum.getCode();
this.message = resultCodeEnum.getMessage();
}
}
2.修改我们抛出的异常
例如:throw new RuntimeException("有没有找到该用户,请先注册!!");
改成:throw new zhexinException(ResultCodeEnum.LOGIN_ERROR);
或者:throw new zhexinException(201,"有没有找到该用户,请先注册!!");
3.接收我们抛出的异常,然后统一格式
//接收自定义异常
@ExceptionHandler(zhexinException.class)
public Result zhexinError(ResultCodeEnum resultCodeEnum) {
return Result.build(null, resultCodeEnum);
}
@ExceptionHandler(zhexinException.class)
public Result zhexinError(zhexinException e) {
return Result.build(null, e.getCode(), e.getMessage());
}
4.运行查看效果
异常报错,参考:Error creating bean with name ‘handlerExceptionResolver‘ defined in class path resource_error creating bean with name 'handlerexceptionres-CSDN博客Error creating bean with name ‘handlerExceptionResolver‘_ttt唐老鸭的博客-CSDN博客

想了一下只保留这一个就行了:另一个不报错也多余啊,因为我已经在自定义异常里分解了枚举类
//接收自定义异常
@ExceptionHandler(zhexinException.class)
public Result zhexinError(zhexinException e) {
return Result.build(null, e.getCode(), e.getMessage());
}
成功了,可以看到失败和成功的格式是一样的


七.前端对接
1.找到目标目录和按钮地址:
i8中 是国际化

可以看到,
在找到界面,index中搜索login


进到按钮的方法里

2.源码分析:
Logind登录界面
<!--
* @Descripttion:
* @version:
* @Date: 2021-04-20 11:06:21
* @LastEditors: huzhushan@126.com
* @LastEditTime: 2022-09-27 18:24:27
* @Author: huzhushan@126.com
* @HomePage: https://huzhushan.gitee.io/vue3-element-admin
* @Github: https://github.com/huzhushan/vue3-element-admin
* @Donate: https://huzhushan.gitee.io/vue3-element-admin/donate/
-->
<template>
<div class="login">
<!-- :rules="rules" 表单验效,,里面的masser是提示气泡 -->
<el-form class="form" :model="model" :rules="rules" ref="loginForm">
<h1 class="title">Vue3 Element Admin</h1>
<el-form-item prop="userName">
<el-input class="text" v-model="model.userName" prefix-icon="User" clearable
:placeholder="$t('login.username')" />
</el-form-item>
<el-form-item prop="password">
<el-input class="text" v-model="model.password" prefix-icon="Lock" show-password clearable
:placeholder="$t('login.password')" />
</el-form-item>
<el-form-item>
<el-button :loading="loading" type="primary" class="btn" size="large" @click="submit">
{{ btnText }}
</el-button>
</el-form-item>
</el-form>
</div>
<div class="change-lang">
<change-lang />
</div>
</template>
<script>
import {
defineComponent,
getCurrentInstance,
reactive,
toRefs,
ref,
computed,
watch,
} from 'vue'
import { Login } from '@/api/login'
import { useRouter, useRoute } from 'vue-router'
import ChangeLang from '@/layout/components/Topbar/ChangeLang.vue'
import useLang from '@/i18n/useLang'
import { useApp } from '@/pinia/modules/app'
//$t 是一个特殊变量,用于访问国际化(i18n)的文本。它是一个全局可用的变量,可以在模板中使用,用于显示国际化的消息。
export default defineComponent({
components: { ChangeLang },
name: 'login',
setup() {
const { proxy: ctx } = getCurrentInstance() // 可以把ctx当成vue2中的this
const router = useRouter() //获取当前路由对象
const route = useRoute() //获取当前路由的路径信息。
const { lang } = useLang() //当前语言设置信息
watch(lang, () => { //数据发生变化时,调用规则验效
state.rules = getRules()
})
const getRules = () => ({ //定义规则
userName: [
{
required: true, //必须的
message: ctx.$t('login.rules-username'), //提示
trigger: 'blur', //在失去焦点时验证
},
],
password: [
{
required: true,
message: ctx.$t('login.rules-password'),
trigger: 'blur',
},
{
min: 6,
max: 12,
message: ctx.$t('login.rules-regpassword'),
trigger: 'blur',
},
],
})
// 好比如这是vue2中的export default
const state = reactive({
//这里好比如vue2中的data
model: {
userName: 'admin',
password: '123456',
},
rules: getRules(), //from引用规则
loading: false, //是否登录中,绑定前端数据
btnText: computed(() => //按钮显示文字,在界面中找得到
state.loading ? ctx.$t('login.logining') : ctx.$t('login.login')
),
loginForm: ref(null), //form表单
//定义方法相当于vue2中的model
submit: () => {
if (state.loading) {//防止重复点击
return //默认是false可以通过
}
//开始验效 ,,, 异步
state.loginForm.validate(async valid => { //validate对整个表单进行校验的方法,参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未通过校验的字段。若不传入回调函数,则会返回一个 promise
//验效,成功调用
if (valid) {
state.loading = true //不能再点击
const { code, data, message } = await Login(state.model) //异步请求接口 ,,接口地址去Login里找
if (+code === 200) { //+其实就是普通的一元运算符,这样计算后就把字符串变成了数字
ctx.$message.success({
message: ctx.$t('login.loginsuccess'), //气泡提示i18里的内容
duration: 1000, //持续1000秒
})
//界面跳转,只有一个home界面只能重定向到home界面,具体请看重定向配置在:\src\router\index.js
//获取路由的重定向地址
const targetPath = decodeURIComponent(route.query.redirect)
// 判断是否是http开头
if (targetPath.startsWith('http')) {
// 如果是一个url地址,跳转url
window.location.href = targetPath
} else if (targetPath.startsWith('/')) {
// 如果是内部路由地址,跳转内部url
router.push(targetPath)
} else {
router.push('/')
}
//保存token
useApp().initToken(data)
} else {
//登录失败
ctx.$message.error(message)
}
//恢复按钮的点击性
state.loading = false
}
})
},
})
// 接口暴露到外面
return {
...toRefs(state), //抛出state并实实时同步状态,而且展开为对象
}
},
})
</script>
<style lang="scss" scoped>
.login {
transition: transform 1s;
transform: scale(1);
width: 100%;
height: 100%;
overflow: hidden;
background: #2d3a4b;
.form {
width: 520px;
max-width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin: 160px auto 0;
:deep {
.el-input__wrapper {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1) inset;
background: rgba(0, 0, 0, 0.1);
}
.el-input-group--append>.el-input__wrapper {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.el-input-group--prepend>.el-input__wrapper {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.title {
color: #fff;
text-align: center;
font-size: 24px;
margin: 0 0 24px;
}
.text {
font-size: 16px;
:deep(.el-input__inner) {
color: #fff;
height: 48px;
line-height: 48px;
&::placeholder {
color: rgba(255, 255, 255, 0.2);
}
}
}
.btn {
width: 100%;
}
}
}
.change-lang {
position: fixed;
right: 20px;
top: 20px;
:deep {
.change-lang {
height: 24px;
&:hover {
background: none;
}
.icon {
color: #fff;
}
}
}
}
</style>
可以看到他在这里调用Login进行访问,进去看看

这里用跨域的配置,代理请求到了/login接口,使用post请求,把一个json传了进去

3.修改为我们的接口(这里先不考虑跨域问题):
改上我们的url

打开浏览器登录,之后看F12控制台输出
可以看到我们的接口已经被访问了,,这样其实这一步就算成功了
但是
他说被跨域方案CORS挡住了

八.跨域
简单来说就是<请求协议, 域名 ,端口号>其中任何一个和前端当前的url不相同,就会认为是跨域请求,会被隔离,不让访问
之前我有过解决这个问题的文章参考,是前端的解决方案,参考:https://www.cnblogs.com/zhexin/articles/17852966.html
前端修改的话直接参考我们文章修改这里就好:

其实最好是改前端的代码, 因为这样便于前端的接口调用, 不用每次都写域名
这里我们后端用老师的方法,前端也改成跨域便于开发
由于我们现在没有数据, 所以保留之前的接口


↓
↓
这里老师用的方法是java后端利用拦截器进行解决跨域, 文章中用的是过滤器
老师还说: 用@CrossOrigin注解放到请求接口上,可以实现单个接口跨域, 但是维护复杂,不建议使用
下面我们学习一下老师的做法:
实参webMVC配置类
实现 implements WebMvcConfigurer 接口,参考:spring的WebMvc配置 - 知乎 (zhihu.com)

重写这个接口,光看名字就知道这是解决CORS专用配置
注册为bean
@Component
public class webMVCConfigInterceptor implements WebMvcConfigurer {
//解决CORS跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //路径,匹配所有
.allowCredentials(true) //在跨域下传递cookie
.allowedOriginPatterns("*") //允许请求来源跨域
.allowedMethods("*")
.allowedHeaders("*");
}
}
成功:

九.验证码
1.流程分析

2.后端实现
到这里我们为了方便管理Redis的前缀,加一个枚举类:
并且把之前的用户登录的uuid也改成枚举类型
package com.zhexin.manage.config;
import lombok.Getter;
@Getter //存储Redis时的uuid
public enum RedisEnum {
USERUUID("用户UUID:"),
VERIFITATIONUUID("验证码UUID:")
;
private String RedisID ; // 响应消息
RedisEnum( String RedisID) {
this.RedisID = RedisID ;
}
}
1.创建生成验证码的接口,和实现类
@Operation(summary = "获取图片验证码")
@GetMapping("/verificationCode")
public Result verificationCode() {
ValidateCodeVo validateCodeVo = verificationCode.getVerificationB64();
return Result.build(validateCodeVo, ResultCodeEnum.SUCCESS);
}
2.实现获取验证码业务
需要:
-
//利用我们导入的hutool制造一个验证码 ,也可以用其他工具: java验证码样式大全,项目
-
//生成uuid,
-
//存入redis
-
//返回validateCodeVo对象
package com.zhexin.manage.service.Impl;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.core.lang.UUID;
import com.zhexin.manage.config.RedisEnum;
import com.zhexin.manage.service.VerificationCode;
import com.zhexin.model.vo.system.ValidateCodeVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class VerificationCodeImpl implements VerificationCode {
@Autowired
RedisTemplate<String,String> redisTemplate;
@Override
public ValidateCodeVo getVerificationB64() {
ValidateCodeVo validateCodeVo = new ValidateCodeVo();
//利用我们导入的hutool制造一个验证码
//宽,高,验证码位数,干扰线
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 20);
String verificationCode = circleCaptcha.getCode(); //验证码
String imageBase64 = circleCaptcha.getImageBase64(); //图片的b64位
//生成uuid,
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
//存入redis , 60秒生效期间
redisTemplate.opsForValue().set(RedisEnum.VERIFITATIONUUID+uuid , verificationCode , 60 , TimeUnit.SECONDS);
//返回validateCodeVo对象
validateCodeVo.setCodeKey(RedisEnum.VERIFITATIONUUID+uuid); //这里的名称和redis里的一致
validateCodeVo.setCodeValue("data:image/png;base64," +imageBase64); //返回为b64图片格式
return validateCodeVo;
}
}
2.修改登录接口,验证验证码
前端发来是数据包含验证码的,直接获取

需要:
-
//验证验证码
-
//获取key和码
-
//查询redis
- //忽略大小写对比验证码
-
//是否放行
//登录业务
@Override
public LoginVo Login(LoginDto loginDto) {
LoginVo loginVo = new LoginVo();
//验证验证码
//获取key和码
String inputyzm = loginDto.getCaptcha(); //输入的验证码
String inputkey = loginDto.getCodeKey(); //验证码的key
//查询redis
String redisyzm = redisTemplate.opsForValue().get(inputkey);
//忽略大小写对比验证码
if (StrUtil.isEmpty(redisyzm) || !StrUtil.equalsIgnoreCase(redisyzm , inputyzm)){
//是否放行
//删除这个验证码
redisTemplate.delete(redisyzm);
throw new zhexinException(201,"验证码错误");
}
//...........下面还所原先的登录代码...........
3.前端实现
要实现b64转图片参考 :VUE Base64编码图片展示与转换图片 - Maggieq8324 - 博客园 (cnblogs.com)
-------后来发现不需要<img>会自动解析
需要:
- 修改界面
- 修改国际化
- 修改验效
- 添加请求的验证码接口
- 添加yzmkey的变量
- 添加图片的变量
- 界面开始、用户点击、错误 都刷新验证码
- 修改登录加上上传验证码
1.修改界面
在登录上方 密码下方 加上:
<!-- 验证码 -->
<el-form-item prop="captcha">
<div class="captcha" style="display: flex; ">
<el-input class="text" v-model="model.captcha" prefix-icon="List" :placeholder="$t('login.yzm')"
style="margin-right: 40px;" />
<img :src="ImageB64" @click="gxyzm" />
</div>
</el-form-item>
2.修改国际化
其中::placeholder="$t('login.yzm')调用了国际化语言
在i8中注册: 中英文都要注册


export default {
title: '哲心商城后台登录',
username: '用户名',
password: '密码',
yzm: '验证码',
login: '登录',
logining: '登录中...',
loginsuccess: '登录成功',
'rules-username': '请输入用户名',
'rules-password': '请输入密码',
'rules-yzm': '验证码不能为空',
'rules-regpassword': '长度在 6 到 12 个字符',
}
3.修改验效
之前我们分析,这个方法定义了验效规则,按他的逻辑在加一个就行了

const getRules = () => ({ //定义规则
userName: [
{
required: true, //必须的
message: ctx.$t('login.rules-username'), //提示
trigger: 'blur', //在失去焦点时验证
},
],
password: [
{
required: true,
message: ctx.$t('login.rules-password'),
trigger: 'blur',
},
{
min: 6,
max: 12,
message: ctx.$t('login.rules-regpassword'),
trigger: 'blur',
},
],
captcha: [
{
required: true,
message: ctx.$t('login.rules-yzm'),
trigger: 'blur',
},
],
})
4.添加请求的验证码接口
点进
类里面加个接口
// 获取验证码
export const Getyzm = () => {
return request({
url: '/zx/admin/system/index/verificationCode',
method: 'get',
})
}
别忘了在index中引入,不然是调用不到的

5.添加yzmkey的变量
把我们添加的组件上的变量写上, 以及返回的key也要用一个变量保存好

6.添加图片的变量
因为Model这个方法还肩负着上传到服务器的责任, 所以不能再用它了,我们再起一个

//这里好比如vue2中的data
model: {
userName: 'admin',
password: '111111',
captcha: '', //验证码
codeKey: '' //验证码的key
},
ImageB64: ref(null), //b64的码
7.界面开始、用户点击、错误 都刷新验证码
由于我们多次调用所以写成一个方法, 直接写一个新方法:
const getyzms = async () => { //异步调用验证码接口
const { data } = await Getyzm(state.model) //异步请求接口
state.ImageB64 = data.codeValue //接收图片b64,并设置
state.model.codeKey = data.codeKey //接收当前验证码的key,并保存到变量
state.model.captcha = '' //只要刷新了验证码就要清空输入框
}
1.界面开始
由于vue3是按需引入, 所以首先引入,组件加载生命周期函数:

然后实现这个方法, 直接调用我们的验证码接口,就ok了

2.用户点击
图片控件上加一个点击事件,调用这个方法:

3.验证码错误
这里的验证码是否正确是登录接口返回的, 所以在登录接口失败的地方调用

8.修改登录加上上传验证码
这一步其实再第五步添加变量的时候,也同时实现了,哈哈哈
直接上传了model对象
9.最终前端代码
login/indtx.vue
<!--
* @Descripttion:
* @version:
* @Date: 2021-04-20 11:06:21
* @LastEditors: huzhushan@126.com
* @LastEditTime: 2022-09-27 18:24:27
* @Author: huzhushan@126.com
* @HomePage: https://huzhushan.gitee.io/vue3-element-admin
* @Github: https://github.com/huzhushan/vue3-element-admin
* @Donate: https://huzhushan.gitee.io/vue3-element-admin/donate/
-->
<template>
<div class="login">
<!-- :rules="rules" 表单验效,,里面的masser是提示气泡 -->
<el-form class="form" :model="model" :rules="rules" ref="loginForm">
<h1 class="title">{{$t('login.title')}}</h1>
<el-form-item prop="userName">
<el-input class="text" v-model="model.userName" prefix-icon="User" clearable
:placeholder="$t('login.username')" />
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input class="text" v-model="model.password" prefix-icon="Picture" show-password clearable
:placeholder="$t('login.password')" />
</el-form-item>
<!-- 验证码 -->
<el-form-item prop="captcha">
<div class="captcha" style="display: flex; ">
<el-input class="text" v-model="model.captcha" prefix-icon="List" :placeholder="$t('login.yzm')"
style="margin-right: 40px;" />
<img :src="ImageB64" @click="gxyzm" />
</div>
</el-form-item>
<!-- 按钮 -->
<el-form-item>
<el-button :loading="loading" type="primary" class="btn" size="large" @click="submit">
{{ btnText }}
</el-button>
</el-form-item>
</el-form>
</div>
<div class="change-lang">
<change-lang />
</div>
</template>
<script>
import {
defineComponent,
getCurrentInstance,
reactive,
toRefs,
ref,
computed,
watch,
onMounted
} from 'vue'
import { Login, Getyzm } from '@/api/login' //引入登录和验证码的接口
import { useRouter, useRoute } from 'vue-router'
import ChangeLang from '@/layout/components/Topbar/ChangeLang.vue'
import useLang from '@/i18n/useLang'
import { useApp } from '@/pinia/modules/app'
//$t 是一个特殊变量,用于访问国际化(i18n)的文本。它是一个全局可用的变量,可以在模板中使用,用于显示国际化的消息。
export default defineComponent({
components: { ChangeLang },
name: 'login',
setup() {
const { proxy: ctx } = getCurrentInstance() // 可以把ctx当成vue2中的this
const router = useRouter() //获取当前路由对象
const route = useRoute() //获取当前路由的路径信息。
const { lang } = useLang() //当前语言设置信息
onMounted(() => {
getyzms()
})
const getyzms = async () => { //异步调用验证码接口
const { data } = await Getyzm(state.model) //异步请求接口
state.ImageB64 = data.codeValue //接收图片b64,并设置
state.model.codeKey = data.codeKey //接收当前验证码的key,并保存到变量
state.model.captcha = '' //只要刷新了验证码就要清空输入框
}
watch(lang, () => { //数据发生变化时,调用规则验效
state.rules = getRules()
})
const getRules = () => ({ //定义规则
userName: [
{
required: true, //必须的
message: ctx.$t('login.rules-username'), //提示
trigger: 'blur', //在失去焦点时验证
},
],
password: [
{
required: true,
message: ctx.$t('login.rules-password'),
trigger: 'blur',
},
{
min: 6,
max: 12,
message: ctx.$t('login.rules-regpassword'),
trigger: 'blur',
},
],
captcha: [
{
required: true,
message: ctx.$t('login.rules-yzm'),
trigger: 'blur',
},
],
})
// 好比如这是vue2中的export default
const state = reactive({
//这里好比如vue2中的data
model: {
userName: 'admin',
password: '111111',
captcha: '', //验证码
codeKey: '' //验证码的key
},
ImageB64: ref(null), //b64的码
rules: getRules(), //from引用规则
loading: false, //是否登录中,绑定前端数据
btnText: computed(() => //按钮显示文字,在界面中找得到
state.loading ? ctx.$t('login.logining') : ctx.$t('login.login')
),
loginForm: ref(null), //form表单
gxyzm: () => { getyzms() },
//定义方法相当于vue2中的model
submit: () => {
if (state.loading) {//防止重复点击
return //默认是false可以通过
}
//开始验效 ,,, 异步
state.loginForm.validate(async valid => { //validate对整个表单进行校验的方法,参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未通过校验的字段。若不传入回调函数,则会返回一个 promise
//验效,成功调用
if (valid) {
state.loading = true //不能再点击
const { code, data, message } = await Login(state.model) //异步请求接口 ,,接口地址去Login里找
if (+code === 200) { //+其实就是普通的一元运算符,这样计算后就把字符串变成了数字
ctx.$message.success({
message: ctx.$t('login.loginsuccess'), //气泡提示i18里的内容
duration: 1000, //持续1000秒
})
//界面跳转,只有一个home界面只能重定向到home界面,具体请看重定向配置在:\src\router\index.js
//获取路由的重定向地址
const targetPath = decodeURIComponent(route.query.redirect)
// 判断是否是http开头
if (targetPath.startsWith('http')) {
// 如果是一个url地址,跳转url
window.location.href = targetPath
} else if (targetPath.startsWith('/')) {
// 如果是内部路由地址,跳转内部url
router.push(targetPath)
} else {
router.push('/')
}
//保存token
useApp().initToken(data)
} else {
//登录失败,输出返回的错误信息,刷新验证码
ctx.$message.error(message)
state.gxyzm()
}
//恢复按钮的点击性
state.loading = false
}
})
},
})
// 接口暴露到外面
return {
...toRefs(state), //抛出state并实实时同步状态,而且展开为对象
}
},
})
</script>
<style lang="scss" scoped>
.login {
transition: transform 1s;
transform: scale(1);
width: 100%;
height: 100%;
overflow: hidden;
background: #2d3a4b;
.form {
width: 520px;
max-width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin: 160px auto 0;
:deep {
.el-input__wrapper {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1) inset;
background: rgba(0, 0, 0, 0.1);
}
.el-input-group--append>.el-input__wrapper {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.el-input-group--prepend>.el-input__wrapper {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.title {
color: #fff;
text-align: center;
font-size: 24px;
margin: 0 0 24px;
}
.text {
font-size: 16px;
:deep(.el-input__inner) {
color: #fff;
height: 48px;
line-height: 48px;
&::placeholder {
color: rgba(255, 255, 255, 0.2);
}
}
}
.btn {
width: 100%;
}
}
}
.change-lang {
position: fixed;
right: 20px;
top: 20px;
:deep {
.change-lang {
height: 24px;
&:hover {
background: none;
}
.icon {
color: #fff;
}
}
}
}
</style>
4.验证码优化
到这里前面的已经可以正常运行了
这里是我突然想到的, 我多次点击验证码, 会产生多个redis的键值对,
虽然60秒过期但是也会造成资源的浪费
我们可以让接口传入一个字符串类型
前端调用验证码的接口时, 传入当前的验证码UUID, 然后我们先删了旧的,
再生成新的
我就不进行实现了,跟这老师步伐走
十.获取用户信息
思路:
前端请求头携带token
后台解析从redis里找到用到用户返回
1.后端:
get接口
@Operation(summary = "读取用户信息")
@GetMapping("/userInfo")
public Result userInfo(@RequestHeader("token") String uuidtoken) { //请求头中读取token
SysUser userInfos = userInfoService.getuserInfo(uuidtoken);
return Result.build(userInfos, ResultCodeEnum.SUCCESS);
}
///---------------------------------------------------------------------------------------------------------
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
RedisTemplate<String,String> redisTemplate;
@Override
public SysUser getuserInfo(String uuidtoken) {
//通过token读取user
String Sysuser = redisTemplate.opsForValue().get(uuidtoken);
//json转对象
SysUser sysUserObj = JSON.parseObject(Sysuser, SysUser.class);
//返回
return sysUserObj;
}
}
2.后端
1.先修改请求头

可以看到他自己带了,但是他这个还需要解析,为了方便直接改成不用解析的:

//请求携带token
config.headers.token = authorization.token
2.调用接口
在哪里调用呢????, 之前写登录接口时侯带了一个:

直接改个接口就行了


十一.用户退出

前面创建接口的方法还是一样,就不多重复了,直接看业务实现
思路:
传入用户的key --> 在redis中删除key
1.业务实现
@Service
public class UserExitServicImpl implements UserExitServic {
@Autowired //加载redis
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean exit(String uuidkey) {
return redisTemplate.delete(uuidkey);
}
}
2.前端调用
1.搜索退出界面:

修改:

2.导入请求接口:
import request from '@/utils/request'
3.编写请求
//退出登录接口
const logoutdate = () => {
return request({
url: '/zx/admin/system/index/userExit',
method: 'get',
})
}
4.调用
可以看到他的可以是固定的,调试也对应的上:


我们获取他,然后传到接口里:
// 退出
const logout = async () => {
//从 localStorage 里读取key
var tokenjson = window.localStorage.getItem("VEA-TOKEN");
//调用接口
await logoutdate()
// 清除token
useApp().clearToken()
// 跳转
router.push('/login')
}
用RedisGUI测试了成功

十二.登录状态管理
因为,无论退出用户,还是查询用户信息等等,否需要先登录用户才能操作,
1.思路:
- 前端上传数据携带token
- 后端拦截器验证有没有这个token ,有则放行,没有返回208
- 前端接收208,删除token,返回登录
- 放行不用拦截的接口
- 为了防止重复查询redis,把查出来的用户信息放到Threadlocal里面,这个是请求启动时自动生成的一个线程存储类
- 更新redis过期时间

2.后端:
1.准备工作:创建Theadlocal
package com.zhexin.common.uilt.Thread;
import com.zhexin.common.uilt.model.entity.system.SysUser;
/**
* 存储请求的Sysuser
*/
public class ThreadlocalUser {
private static final ThreadLocal<SysUser> thread = new ThreadLocal<>();
//获取数据
public static SysUser getSysUser() {
return thread.get();
}
//设置数据
public static void setSysUser(SysUser sysUser) {
thread.set(sysUser);
}
//删除数据
public static void removeSysUser(){
thread.remove();
}
}
2.创建拦截器
创建一个类实现:implements HandlerInterceptor
需要
- //1.获取token
- //2.放行测试接口,用于浏览器测试通信 http CORS options请求(预检请求)详解 - 知乎 (zhihu.com)
- //3.查询redis
- //4.判断是退出
- //5.存放到ThreadLocal
- //6.延长Redis的生效时间
- //7.放行
package com.zhexin.manage.config.interceptor;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.zhexin.common.uilt.Thread.ThreadlocalUser;
import com.zhexin.model.entity.system.SysUser;
import com.zhexin.model.vo.common.Result;
import com.zhexin.model.vo.common.ResultCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;
@Component
public class UserAuthentication implements HandlerInterceptor {
@Autowired
RedisTemplate<String,String> redisTemplate;
//方法之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取token
String token = request.getHeader("token"); //从请求头里读取token
//2.放行测试接口
String method = request.getMethod();
if (method.equals("OPTIONS")){ //用于浏览器预检
return true;
}
//3.查询redis
String redisToken = redisTemplate.opsForValue().get(token);
//4.判断是退出
if (StrUtil.isEmpty(redisToken)){
Result result = Result.build(null, ResultCodeEnum.LOGIN_AUTH); //创建返回格式
//初始化发送通道
//设置编码
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try (PrintWriter writer = response.getWriter()) { //用完自动关闭通道
//获取发送通道
writer.print(JSON.toJSONString(result)); //发送通道只能发送文本,转为文本
writer.flush(); //发射
} catch (IOException e) {
e.printStackTrace();
}
//退出
return false;
}
//5.存放到ThreadLocal
SysUser sysUser = JSON.parseObject(redisToken , SysUser.class);
ThreadlocalUser.setSysUser(sysUser);
//6.延长Redis的生效时间
redisTemplate.expire(token , 30 , TimeUnit.MINUTES);
//7.放行
return true;
}
//方法之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
//最最最最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//删除用户对象缓存
ThreadlocalUser.removeSysUser();
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
3.注册到springboot里
在之前的 //解决CORS跨域 的类里
@Autowired
UserAuthentication userAuthentication ;
//用户状态拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userAuthentication) //要注册的拦截器
.excludePathPatterns("/admin/system/index/login" ,
"/admin/system/index/generateValidateCode") //要放行的接口
.addPathPatterns("/**");//要拦截的接口
}
4.优化
1.这个地方太臃肿,我们把它提取到配置文件

1.创建配置类

打开他们的文档发现,他要我们添加这个配置:前缀不能使用驼峰命名
package com.zhexin.manage.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@Data
@ConfigurationProperties(prefix = "zhexin.interceptor-release") // 前缀不能使用驼峰命名
public class InterceptorRelease {
private List<String> ReleaseList ; //这个名称需要和yaml的保持一致,才能读取到
}
没办法我只能屈服于文档,给他加上
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2.写yaml
#配置放行的接口
zhexin:
interceptor-release:
ReleaseList:
-/admin/system/index/login
-/admin/system/index/verificationCode
-/doc.html
3.修改为调用yaml

4.让springboot扫描他

5.报错

这是没有经过过滤器呀。导致查询到Redis是空的。
原来是这里:
这里空格不能少

2.查寻
老师说这里,我们已经存放到了ThreadLocal这里面,可以直接从ThreadLocal里面取到,不用走redis
但是我没明白
我感觉我们在用户调用完接口后,删除了里面的信息,应该查不到啊??????
这里先不改了,写完再测试吧


------------后来发现老师说的是:后端获取用户信息接口,
这倒是,我们之前创建ThreadLocal的初衷就是为了防止重复调用redis
修改:
/**
* 查询用户信息
*/
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Override
public SysUser getuserInfo(String uuidtoken) {
//通过token读取user
//String Sysuser = redisTemplate.opsForValue().get(uuidtoken);
//json转对象
//SysUser sysUserObj = JSON.parseObject(Sysuser, SysUser.class);
//从ThreadlocalUser里查
SysUser sysUserObj = ThreadlocalUser.getSysUser();
//返回
return sysUserObj;
}
}
3.前端:
之前我们再前端请求前拦截器里加上了token
现在我们再前端请求后拦截器里加上了判断208

// 拦截响应
service.interceptors.response.use(
// 响应成功进入第1个函数,该函数的参数是响应对象
response => {
const res = response.data
if (res.code == 208) { //判断返回的状态码
const redirect = encodeURIComponent(window.location.href) // 当前地址栏的url
router.push(`/login?redirect=${redirect}`) //跳转界面
return Promise.reject(new Error(res.message || 'Error')) //抛出异常
}
return response.data
},
十三. 后来修改补充
1.UUID前缀
运行发现UUID前面的前缀并不是我们设置的
修复:
所有RedisEnum.USERTOKEN枚举类,后面加他的get方法:
RedisEnum.USERTOKEN.getRedisID() 
2.常量

这里不能用中文,redis可以中文,但是前端的请求头不允许


3.异地登录
实现一个账号只能登录一个设备
思路,登陆成功把用户名和token存到一个新的redis里, 登录的时候,删除之前的用户名
package com.zhexin.manage.service.Impl;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.zhexin.common.service.exception.zhexinException;
import com.zhexin.manage.mapper.sysuser.SysUserMapper;
import com.zhexin.manage.config.RedisEnum;
import com.zhexin.manage.service.SysUserService;
import com.zhexin.model.dto.system.LoginDto;
import com.zhexin.model.entity.system.SysUser;
import com.zhexin.model.vo.common.ResultCodeEnum;
import com.zhexin.model.vo.system.LoginVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.concurrent.TimeUnit;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired //@Mapper会自动注册为bean
private SysUserMapper sysUserMapper;
@Autowired //加载redis
private RedisTemplate<String, String> redisTemplate;
//登录业务
@Override
public LoginVo Login(LoginDto loginDto) {
LoginVo loginVo = new LoginVo();
//验证验证码
//获取key和码
String inputyzm = loginDto.getCaptcha(); //输入的验证码
String inputkey = loginDto.getCodeKey(); //验证码的key
//查询redis
String redisyzm = redisTemplate.opsForValue().get(inputkey);
//忽略大小写对比验证码
if (StrUtil.isEmpty(redisyzm) || !StrUtil.equalsIgnoreCase(redisyzm , inputyzm)){
//是否放行
//删除这个验证码
redisTemplate.delete(inputkey);
throw new zhexinException(201,"验证码错误");
}
//登录
//1.取到账号密码
String inputUserName = loginDto.getUserName();
String inputUserPassword = loginDto.getPassword();
//2.查询数据库用户
SysUser datebeasSysuser = sysUserMapper.SelectUserinfoByname(inputUserName);
//判断有没有结果
if (datebeasSysuser == null) {
throw new zhexinException(201,"有没有找到该用户,请先注册!!");
}
//密码转md5 ,可以再前端生成
inputUserPassword = DigestUtils.md5DigestAsHex(inputUserPassword.getBytes());
//3.查密码是否正确
if (!datebeasSysuser.getPassword().equals(inputUserPassword)) {
throw new zhexinException(ResultCodeEnum.LOGIN_ERROR);
}
//4.生成token 9e87272b-8877-4ccb-beb7-c5afb14b1f7d 并删除:-
String token = UUID.randomUUID().toString().replaceAll("-", "");
//异地登录
String formerUser = redisTemplate.opsForValue().get(RedisEnum.FORMERUSER.getRedisID()+inputUserName); //查询历史记录
if (!StrUtil.isEmpty(formerUser)) { //有
redisTemplate.delete(RedisEnum.FORMERUSER.getRedisID()+inputUserName); //删除旧的记录
redisTemplate.delete(formerUser); //删除旧用户信息
}
//存储k=用户名,y=token
redisTemplate.opsForValue().set(RedisEnum.FORMERUSER.getRedisID()+inputUserName, RedisEnum.USERTOKEN.getRedisID()+ token, 7, TimeUnit.DAYS);
//5.存储到redis key,value, 过期时间 , 单位天
redisTemplate.opsForValue().set(RedisEnum.USERTOKEN.getRedisID()+ token, JSON.toJSONString(datebeasSysuser), 7, TimeUnit.DAYS);
//6.存入并返回loginVO对象
loginVo.setToken(RedisEnum.USERTOKEN.getRedisID()+ token);
loginVo.setRefresh_token("");
return loginVo;
}
}
两个客户端登录两次,看redis里只有一个用户就对了
4.放行/doc.html
在拦截器里判断一下url吧:https://blog.csdn.net/kxj19980524/article/details/85274624
#配置放行的接口
zhexin:
interceptor-release:
ReleaseList:
#用户登录
- /admin/system/index/login
#验证码
- /admin/system/index/verificationCode
#放行文档
- /doc.html
- /webjars/**
- /favicon.ico
- /v3/api-docs/**
5.拦截器bug修复:
这里需要判断两次空,(我把退出代码,提取成了方法)


共计:
字

浙公网安备 33010602011771号