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

@Data是创建get、set方法
@Schema(description = "用户名") sewrgger注解,用于参数提示

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. 实现实体类业务代码

需要做的事情:

  1. 取到账号密码
  2. 查询数据库用户
  3. 密码转md5 ,可以再前端生成
  4. 判断有没有结果
    1. 先查用户名,有没有用户
    2. 查密码是否正确
  5. 生成token
  6. 存储到redis
  7. 返回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. 运行:

报错,参考:SpringBoot升级到3.2.0启动出现Invalid value type for attribute ‘factoryBeanObjectType‘: java.lang.String-CSDN博客

 <mybatis.v>3.0.3</mybatis.v>

 

运行成功:

访问文档地址:localhost:10001/doc.html

来到这个界面:

 运行

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

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

 

2.再次运行,报错

 

 

JSON.toJSONString(loginDto),这个写错了,没有引号,并且传递的应该是数据库查出来的数据datebeasSysuser,注意改掉
但这不是报错的原因,,,原因大概是redis连接的问题,
这个问题挺棘手的

 最终剔除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不相同,就会认为是跨域请求,会被隔离,不让访问

参考:什么是跨域? 出现原因及解决方法-CSDN博客

之前我有过解决这个问题的文章参考,是前端的解决方案,参考: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.流程分析

地址:https://pixso.cn/app/board/5v9WuF5mX80WRrTfRkPEdQ?showQuickFrame=true&icon_type=3&file_type=20 邀请您加入 Pixso 白板文件「验证码」

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.实现获取验证码业务

需要:

  1. //利用我们导入的hutool制造一个验证码  ,也可以用其他工具: java验证码样式大全,项目
  2. //生成uuid,
  3. //存入redis
  4. //返回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.修改登录接口,验证验证码

前端发来是数据包含验证码的,直接获取

 

需要:

  1. //验证验证码
  2. //获取key和码
  3. //查询redis
  4. //忽略大小写对比验证码
  5. //是否放行
	//登录业务
	@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>会自动解析

需要:

  1. 修改界面
  2. 修改国际化
  3. 修改验效
  4. 添加请求的验证码接口
    1. 添加yzmkey的变量
    2. 添加图片的变量
  5. 界面开始、用户点击、错误 都刷新验证码
  6. 修改登录加上上传验证码

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过期时间

导图地址https://pixso.cn/app/board/fCo2QAZjjUXuUi2jpMYaTQ?showQuickFrame=true&icon_type=3&file_type=20 邀请您加入 Pixso 白板文件「用户登录拦截器」

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

 需要

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修复:

这里需要判断两次空,(我把退出代码,提取成了方法)

共计:

posted @ 2023-12-01 13:30  哲_心  阅读(90)  评论(0)    收藏  举报