day03_登录注销(前端接入登录,异常处理, 图片验证码,获取用户信息接口,退出功能)

时间:2024-03-03 21:08:37

文章目录

  • 1. 前端接入登录
    • 1.1 修改前端代码
    • 1.2 跨域请求
      • 1.2.1 跨域请求简介
      • 1.2.2 COSR概述
        • CORS简介
        • CORS原理
      • 1.2.3 CORS解决跨域
  • 2. 异常处理
    • 2.1 提示空消息分析
    • 2.2 系统异常分类
    • 2.3 异常处理
      • 2.2.1 方案一
      • 2.2.2 方案二
  • 3. 图片验证码
    • 3.1 图片验证码意义
    • 3.2 实现思路
    • 3.3 后端接口编写
      • 3.3.1 实体类创建
      • 3.3.2 IndexController
      • 3.3.3 ValidateCodeService
    • 3.4 前端接入
      • 3.4.1 实现思路
      • 3.4.3 代码实现
        • 页面表单项
        • 规则校验
        • api/login.js
        • onMounted
    • 3.5 校验验证码
  • 4. 获取用户信息接口
    • 4.1 前端源码分析
      • 4.1.1 请求发送分析
      • 3.1.2 用户信息使用
      • 3.1.3 token传递
    • 4.2 后端接口
      • 4.2.1 IndexController
      • 4.2.2 SysUserService
    • 4.3 前端接入
    • 4.4 进入首页
  • 5. 退出功能
    • 5.1 需求分析
    • 5.2 代码实现
      • 5.2.1 后端接口
        • IndexController
        • SysUserService
      • 5.2.2 前端接入
        • login.js
        • Userinfo.vue
        • Userinfo.vue

1. 前端接入登录

当后端接口开发好了以后就可以让前端去请求该登录接口完成登录操作。

1.1 修改前端代码

修改src/utils/request.js更改基础请求路径

const service = axios.create({
  // 后端服务的ip地址和端口号
  baseURL: 'http://localhost:8503',    
  timeout: 10000,
  withCredentials: true,
})

修改src/api/login.js更改登录接口地址

// 登录接口
export const Login = data => {
  return request({
    url: '/admin/system/index/login',
    method: 'post',
    data,
  })
}

发送登录请求,那么此时会报一个错误:

在这里插入图片描述

报错的原因是因为此时的请求是一个跨域的请求。

1.2 跨域请求

1.2.1 跨域请求简介

跨域请求:通过一个域的JavaScript脚本和另外一个域的内容进行交互

域的信息:协议、域名、端口号

在这里插入图片描述

同域:当两个域的协议、域名、端口号均相同

如下所示:

在这里插入图片描述

同源【域】策略:在浏览器中存在一种安全策略就是同源策略,同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功

能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实

现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。

1.2.2 COSR概述

CORS简介

官网地址:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

CORS的全称为Cross-origin Resource Sharing,中文含义是跨域资源共享,CORS 给了web服务器一种权限:服务器可以选择是否允许跨域请求访

问到它们的资源。

CORS原理

浏览器将CORS请求分成两类:简单请求非简单请求。怎么区分这两者呢?

简单请求

我们先来看两个条件:

(1)HTTP请求方法是以下三种之一:
	·HEAD
	·GET
	·POST
(2)只包含简单HTTP请求头,即:
	·Accept,
	·Accept-Language,
	·Content-Language,
	·Content-Type并且值是 application/x-www-form-urlencoded, multipart/form-data, 或者 text/plain之一的(忽略参数)。

当请求满足上面的两个条件时,则该请求被视为简单请求,否则被视为非简单请求。简单请求与非简单请求的最主要区别就是跨域请求是否需要发送预检请求(preflight request)。

简单请求的跨域请求响应流程:

在这里插入图片描述

在进行跨域请求时,如果是简单请求,则浏览器会在请求中增加一个Origin请求头之后直接发送CORS请求,服务器检查该请求头的值是否在服务器设置的CORS许可范围内,如果在许可范围内,则服务器同意本次请求,如果不在许可范围内,则服务会返回一个没有包含Access-Control-Allow-

Origin 响应头的HTTP响应。

非简单请求

非简单请求的跨域请求响应流程:

在这里插入图片描述

除了简单请求其他的请求都是非简单请求,非简单请求会先发送一次预检请求**(OPTIONS请求),浏览器除了会带上Origin请求头**之外,还会再带

Access-Control-Request-Method 和 Access-Control-Request-Headers 这两个请求头,服务器在收到预检请求之后,会检查这三个请

求头是否与服务器的资源设置(接口)一致,如服务器的接口只允许请求方法为GET、Origin为http://www.abc.com:8080、Access-Control-Request-Header 为 content-type的请求,只要预检请求中三个请求头有任意一个值与服务器的资源(接口)设置不一致,服务器就会拒绝预检请求,

如果都一致,则服务器确认通过预检请求并返回带有Access-Control-Allow-Credentials、Access-Control-Allow-Headers、Access-Control-Allow-Methods、Access-Control-Allow-Origin、Access-Control-Max-Age【间隔多长时间在发起预检请求】等响应头的相应。当预检请求通过以后此时

就可以发送真实请求。

1.2.3 CORS解决跨域

后端服务器开启跨域支持:

方案一:在IndexController上添加**@CrossOrigin**注解

@RestController
@RequestMapping(value = "/admin/system/index")
@CrossOrigin(allowCredentials = "true" , originPatterns = "*" , allowedHeaders = "*") // maxAge默认值是30min
public class IndexController {

}

弊端:每一个controller类上都来添加这样的一个接口影响开发效率、维护性较差

方案二:添加一个配置类配置跨域请求

// com.atguigu.spzx.manager.config
@Component
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")// 添加路径规则
                .allowCredentials(true) // 是否允许在跨域的情况下传递Cookie
                .allowedOriginPatterns("*") // 允许请求来源的域规则
            	.allowedMethods("*")
                .allowedHeaders("*") ;  // 允许所有的请求头
    }
    
}

2. 异常处理

2.1 提示空消息分析

现象说明:当用户输入的用户名或者密码错误,前端页面提示空信息

问题分析:当用户名或者密码输入错误以后,此时后端服务器抛出了异常。但是在后端并没有对异常进行处理,此时就会给前端响应异常信息,在前端提供了axios的响应拦截器,那么通过axios响应拦截器拦截了异常信息,然后给出为空的提示信息。

源码查看:src/utils/request.js

// 拦截响应
service.interceptors.response.use(
  // 响应成功进入第1个函数,该函数的参数是响应对象
  response => {
    return response.data
  },
  // 响应失败进入第2个函数,该函数的参数是错误对象
  async error => {  
    ...
    try {
      ElMessage.error(error.response.data.msg)		// 打印错误信息
    } catch (err) {
      ElMessage.error(error.message)				// 打印错误信息
    }
    return Promise.reject(error)
  }
)

2.2 系统异常分类

在项目中为了更加详情的对异常出现的异常问题进行排查,那么此时应该对异常进行区分,大致可以分为如下两种异常:

1、系统异常:一般由框架本身所抛出的异常:NullPointerException、IllegalArgumentException、ConnectTimeoutException…

2、业务异常:业务异常就是对我们的业务错误进行描述的异常,往往需要进行自定义。常见的业务错误:用户名或者密码错误、用户名重复…

自定异常:

// com.atguigu.spzx.common.exception
@Data
public class GuiguException extends RuntimeException {

    private Integer code ;          // 错误状态码
    private String message ;        // 错误消息
    //真实异常
    private Throwable e;
	// 封装错误状态码和错误消息
    private ResultCodeEnum resultCodeEnum ;     

    public GuiguException(ResultCodeEnum resultCodeEnum,Throwable e) {
        this.resultCodeEnum = resultCodeEnum ;
        this.code = resultCodeEnum.getCode() ;
        this.message = resultCodeEnum.getMessage();
        this.e = e;
    }

    public GuiguException(Integer code , String message,Throwable e) {
        this.code = code ;
        this.message = message ;
        this.e = e;
    }

}

更改异常的抛出代码:

// com.atguigu.spzx.manager.service.impl.SysUserServiceImpl#login
SysUser sysUser = sysUserMapper.selectByUserName(loginDto.getUserName());
if(sysUser == null) {
    throw new GuiguException(ResultCodeEnum.SUCCESS,null) ;		// 抛出自定义的业务异常
}

// 验证密码是否正确
String inputPassword = loginDto.getPassword();
String md5InputPassword = DigestUtils.md5DigestAsHex(inputPassword.getBytes());	// 抛出自定义的业务异常
if(!md5InputPassword.equals(sysUser.getPassword())) {
    throw new GuiguException(ResultCodeEnum.LOGIN_ERROR,null) ;
}

2.3 异常处理

要解决上述问题,那么此时就需要对异常进行处理。统一向前端响应200的http的状态码,然后通过不同的业务状态码区分登录成功还是失败。

2.2.1 方案一

在controller方法中使用try…catch捕获业务层方法所抛出的异常。如下所示:

// IndexController#login方法
@PostMapping(value = "/login")
public Result<LoginVo> login(@RequestBody LoginDto loginDto) {
    try {
        LoginVo loginVo = sysUserService.login(loginDto) ;
        return Result.ok().data(loginVo) ;
    }catch (GuiguException exception) {
        return Result.error() ;
    }
}

2.2.2 方案二

使用spring mvc的全局异常处理器进行异常的处理,整体的工作流程如下所示:

在这里插入图片描述

开发一个全局异常处理器:

// com.atguigu.spzx.common.exception
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(value = GuiguException.class)     // 处理自定义异常
    public Result guiguExceptionHandler(GuiguException exception) {
        if(e.getE()!=null){//获取真实异常信息打印
            //打印异常堆栈日志
            //org.apache.commons.lang3.exception.ExceptionUtils
            log.error(ExceptionUtils.getStackTrace(e.getE()));
        }
        return Result.error().code(e.getCode()).message(e.getMessage()) ;
    }

    @ExceptionHandler(value = Exception.class)          // 处理系统异常
    public Result systemExceptionHandler(Exception exception) {
        if(e!=null){
            //打印异常堆栈日志
    //org.apache.commons.lang3.exception.ExceptionUtils
            log.error(ExceptionUtils.getStackTrace(e));
        }
        return Result.error();
    }

}

在spzx-manager中使用全局异常处理器:

方式一:在启动类上使用@Import注解导入全局异常处理器到spring容器中

@Import(value = GlobalExceptionHandler.class)

方式二:自定义注解对@Import注解进行封装,然后在启动类上使用自定义注解

// com.atguigu.spzx.common.anno
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
@Import(value = GlobalExceptionHandler.class)
public @interface EnableGlobaleExceptionHandler {		// 启动类上添加该注解

}

方式三:使用spring boot3的自动化配置完成全局异常处理器的自动化配置

步骤:

1、在common-service模块中的resources目录下创建一个META-INF/spring文件夹,在该文件夹下创建一个文件,名称为:

org.springframework.boot.autoconfigure.AutoConfiguration.imports

2、在该文件中添加全局异常处理器的全类名

com.atguigu.spzx.common.exception.GlobalExceptionHandler

3. 图片验证码

3.1 图片验证码意义

验证码是全自动区分计算机和人类的图灵测试的缩写,是一种区分用户是计算机还是人的公共全自动程序,可以防止恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登录尝试。

页面效果如下所示:

在这里插入图片描述

3.2 实现思路

整体的实现思路,如下图所示:

在这里插入图片描述

3.3 后端接口编写

3.3.1 实体类创建

创建一个实体类封装,给前端返回的验证码数据:

// com.atguigu.spzx.model.vo.system;
@Data
public class ValidateCodeVo {

    private String codeKey ;        // 验证码的key
    private String codeImage ;      // 图片验证码对应的字符串数据

}

3.3.2 IndexController

在IndexController中添加获取验证码接口方法:

// com.atguigu.spzx.manager.controller.IndexController
@GetMapping(value = "/generateValidateCode")
public Result<ValidateCodeVo> generateValidateCode() {
    ValidateCodeVo validateCodeVo = validateCodeService.generateValidateCode();
    return Result.build(validateCodeVo , ResultCodeEnum.SUCCESS) ;
}

3.3.3 ValidateCodeService

业务层代码实现:

// com.atguigu.spzx.manager.service
public interface ValidateCodeService {

    // 获取验证码图片
    public abstract ValidateCodeVo generateValidateCode();

}

// com.atguigu.spzx.manager.service.impl
@Service
public class ValidateCodeServiceImpl implements ValidateCodeService {

    @Autowired
    private StringRedisTemplate<String , String> stringRedisTemplate ;

    @Override
    public ValidateCodeVo generateValidateCode() {

        // 使用hutool工具包中的工具类生成图片验证码
        CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 20);
        String codeValue = circleCaptcha.getCode();
        String imageBase64 = circleCaptcha.getImageBase64();

        // 生成uuid作为图片验证码的key
        String codeKey = UUID.randomUUID().toString().replace("-", "");

        // 将验证码存储到Redis中
        stringRedisTemplate.opsForValue().set("user:login:validatecode:" + codeKey , codeValue , 5 , TimeUnit.MINUTES);

        // 构建响应结果数据
        ValidateCodeVo validateCodeVo = new ValidateCodeVo() ;
        validateCodeVo.setCodeKey(codeKey);
        //图片验证码 base64字符串前拼接data:image/png;base64, img标签可以解析展示
        validateCodeVo.setCodeImage("data:image/png;base64," + imageBase64);

        // 返回数据
        return validateCodeVo;
    }

}

使用postman进行测试。

3.4 前端接入

3.4.1 实现思路

整体实现思路:

1、登录表单中添加验证码表单项,绑定对应的数据模型(可以问GPT)

2、添加验证码输入框校验规则

3、在api/login.js中添加请求后端获取验证码接口方法

4、在首页中使用vue的onMounted钩子函数发送请求获取图片验证码

3.4.3 代码实现

页面表单项
<!-- 页面结构 -->
<el-form-item prop="code">
    <div class="code">
        <el-input
                  class="text"
                  v-model="model.code"
                  prefix-icon="Picture"
                  placeholder="请输入验证码"
                  ></el-input>
        <img :src="codeImage" @click="refreshCodeImage" />
    </div>
</el-form-item>

css样式:

// 验证码输入框样式 start
.code {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.code img {
  cursor: pointer;
  margin-left: 20px;
}
// 验证码输入框样式 end
规则校验

更改views/login/index.vue页面的vue规则校验代码

const getRules = () => ({
    captcha: [
        {
            required: true,
            message: '验证码不能为空',
            trigger: 'blur',
        },
    ],
})
api/login.js

在api/login.js中添加请求后端获取验证码接口方法

// 获取验证码
export const GetValidateCode = () => {
  return request({
    url: "/admin/system/index/generateValidateCode",
    method: 'get'
  })
}
onMounted

在首页中使用vue的onMounted钩子函数发送请求获取图片验证码

import { onMounted } from 'vue'
import { Login , GetValidateCode } from '@/api/login'
export default defineComponent({
  setup() {
    // onMounted钩子函数
    onMounted(() => {
      state.refreshCaptcha()
    })

    const state = reactive({
      model: {
        userName: 'admin',
        password: '111111',
        code: '',      // 用户输入的验证码
        codeKey: ''       // 后端返回的验证码key
      },
      codeImage: "" ,
      refreshCodeImage: async () => {
          const { data } = await GetValidateCode() ;
          state.model.codeKey = data.codeKey
          state.codeImage = data.codeImage
      }
    })

    return {
      ...toRefs(state),
    }
  },
})
</script>

3.5 校验验证码

对之前的登录方法进行修改,添加校验验证码的逻辑代码。

步骤:

1、实体类修改

// com.atguigu.spzx.model.dto.system
@Data
public class LoginDto {

    private String userName ;
    private String password ;
    private String code ;
    private String codeKey ;

}

2、SysUserServiceImpl登录方法修改

// com.atguigu.spzx.manager.service.impl.SysUserServiceImpl#login
// 校验验证码是否正确
String code = loginDto.getCode();     // 用户输入的验证码
String codeKey = loginDto.getCodeKey();     // redis中验证码的数据key

// 从Redis中获取验证码
String redisCode = stringRedisTemplate.opsForValue().get("user:login:validatecode:" + codeKey);
if(StrUtil.isEmpty(redisCode) || !StrUtil.equalsIgnoreCase(redisCode , code)) {
    throw new GuiguException(ResultCodeEnum.VALIDATE_CODE_ERROR) ;
}

// 验证通过删除redis中的验证码
stringRedisTemplate.delete("user:login:validatecode:" + codeKey) ;

// ResultCodeEnum类添加如下枚举