健身房预约系统SSM+Mybatis实现(四、登录页面+JWT+注销)

发布于:2025-08-19 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言

环境搭建:

https://blog.csdn.net/m0_72900498/article/details/150282255?spm=1001.2014.3001.5501

增删改查的实现:

https://blog.csdn.net/m0_72900498/article/details/150351753?spm=1001.2014.3001.5502

校验 +页面完善+头像上传的实现 :
https://blog.csdn.net/m0_72900498/article/details/150418066?spm=1001.2014.3001.5501

前后端分离项目:

1.跨域通信问题

前端专注于审美和UI,后端专注于微服务

前后端分离:前端用Axios发送请求,调用后端的接口,解决了跨域通信问题

2.登录问题

Session优缺点

前后端分离的项目,vue部分一般登录不会再使用Session。

Session优点:维持会话,保存用户信息。
缺点:单机 ,不能用于分布式

分布式环境

微服务都是在分布式环境中的,那什么是分布式环境?

我们写完的程序应用是部署在多个节点上的,每个节点就是一台服务器,用户访问的时候由网关自动分配到不同的地址(节点)。这样是为了保证高可用性和负载均衡 。

在这里插入图片描述
Session缺点:单机 ,不能用于分布式。不能同时用,是存数据的,是有状态的 。

而分布式节点是不允许有状态的,必须保证每个节点完全一样,保证平等性。

那不用Seccion如何实现登录和校验呢?用JWT

JWT(“令牌”)

在这里插入图片描述
在这里插入图片描述

缺点:jwt无法阻止客户端,无法让令牌失效,也无法续期
在这里插入图片描述

一 、JWT简介

官网:

https://www.jwt.io

JWT本质就是一个加密字符串,进行加密,签名,防破解。

在这里插入图片描述

1.什么是JWT?

在介绍JWT之前,我们先来回顾一下利用token进行用户身份验证的流程:

1.客户端使用用户名和密码请求登录
2.服务端收到请求,验证用户名和密码
3.验证成功后,服务端会签发一个token,再把这个token返回给客户端
4.客户端收到token后可以把它存储起来,比如放到cookie中
5.客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或header中携带
6.服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据

这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:

  1. 支持跨域访问:cookie 是无法跨域的,而 token 由于没有用到 cookie(前提是将 token 放到请求头中),所以跨域后不会存在信息丢失问题
  2. 无状态:token 机制在服务端不需要存储 session 信息,因为 token 自身包含了所有登录用户的信息,所以可以减轻服务端压力
  3. 更适用 CDN:可以通过内容分发网络请求服务端的所有资料
  4. 更适用于移动端:当客户端是非浏览器平台时,cookie 是不被支持的,此时采用 token 认证方式会简单很多
  5. 无需考虑 CSRF:由于不再依赖 cookie,所以采用 token 认证方式不会发生 CSRF,所以也就无需考虑 CSRF 的防御.

而 JWT 就是上述流程当中 token 的一种具体实现方式,其全称是 JSON Web Token。
通俗地说,JWT 的本质就是一个字符串,它是将用户信息保存到一个 Json 字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为 Json 对象传输。JWT 的认证流程如下:

1.首先,前端通过 Web 表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过 SSL 加密的传输(HTTPS),从而避免敏感信息被嗅探
2.后端核对用户名和密码成功后,将包含用户信息的数据作为 JWT 的 Payload,将其与 JWT Header 分别进行 Base64 编码拼接后签名,形成一个 JWT Token,形成的 JWT Token 就是一个如同 lll.zzz.xxx 的字符串
3.后端将 JWT Token 字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的 JWT Token 即可
4.前端在每次请求时将 JWT Token 放入 HTTP请求头中的 Authorization 属性中(解决 XSS 和 XSRF 问题)
5.后端检查前端传过来的 JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token 的接收方是否是自己等等
6.验证通过后,后端解析出 JWT Token 中包含的用户信息,进行其它逻辑操作(一般是根据用户信息得到权限等),返回结果

在这里插入图片描述

2.为什么要用JWT

2.1 传统Session认证的弊端

我们知道 HTTP 本身是一种无状态的协议,这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,认证通过后 HTTP 协议不会记录下认证后的状态,那么下一次请求时,用户还要再一次进行认证,因为根据 HTTP 协议,我们并不知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在用户首次登录成功后,在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这是传统的基于 session 认证的过程
在这里插入图片描述

然而,传统的 session 认证有如下的问题:

  1. 每个用户的登录信息都会保存到服务器的 session 中,随着用户的增多,服务器开销会明显增大
  2. 由session是存在于服务器的物理内存中,所以在分布式系统中,这种方式将会失效。虽然可以将 session 统一保存到 Redis 中,但是这样做无疑增加了系统的复杂性,对于不需要 redis 的应用也会白白多引入一个缓存中间件
  3. 对于非浏览器的客户端、手机移动端等不适用,因为session依赖于cookie,而移动端应用通常没有cookie
  4. 因为session认证本质基于cookie,所以如果cookie被截获,用户很容易收到跨站请求伪造攻击。并且如果浏览器禁用了cookie,这种方式也会失效
  5. 前后端分离系统中更加不适用,后端部署复杂,前端发送的请求往往经过多个中间件到达后端,cookie 中关于session的信息会转发多次
  6. 由于基于Cookie,而cookie无法跨域,所以session的认证也无法跨域,对单点登录不适用

2.2 JWT认证的优势

对比传统的session认证方式,JWT的优势是:

  1. 简洁:JWT Token 数据量小,传输速度也很快
  2. 因为 JWT Token 是以 JSON 加密形式保存在客户端的,所以 JWT 是跨语言的,原则上任何 web 形式都支持
  3. 不需要在服务端保存会话信息,也就是说不依赖于cookie和session,所以没有了传统 session 认证的弊端,特别适用于分布式微服务
  4. 单点登录友好:使用 Session 进行身份认证的话,由于cookie无法跨域,难以实现单点登录。但是,使用 token 进行认证的话, token 可以被保存在客户端的任意位置的内存中,不一定是 cookie,所以不依赖 cookie,不会存在这些问题
  5. 适合移动端应用:使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端

因为这些优势,目前无论单体应用还是分布式应用,都更加推荐用JWT token的方式进行用户认证

2.3 JWT 认证的缺点

无法销毁。
自动续期实现困难。(不请求就不知道期限)
无法改变登录者的状态(如角色、权限等等)

3. JWT结构

JWT 由 3 部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将 JWT 的 3 部分分别进行 Base64 编码后用.进行连接形成最终传输的字符串

在这里插入图片描述

在这里插入图片描述

3.1 Header

JWT 头是一个描述 JWT 元数据的 JSON 对象,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为 HS256);typ 属性表示令牌的类型,JWT 令牌统一写为 JWT。最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存

{
    "alg": "HS256",
    "typ": "JWT"
}

3.2 Payload

有效载荷部分, 是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT 指定七个默认字段供选择。

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到 payload 中,如下例:

{
    "sub": "1234567890",
    "name": "Helen",
    "admin": true
}

请注意,默认情况下 JWT 是未加密的,只是采用 base64 算法,拿到 JWT 字符串后可以转换回原本的 JSON 数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到 JWT 中,以防止信息泄露。JWT 只是适合在网络中传输一些非敏感的信息

3.3 Signature

签名哈希部分是对上面两部分数据签名,需要使用 base64 编码后的 header 和 payload 数据,通过指定的算法生成哈希,以确保数据不会被篡改。 首先,需要指定一个密钥(secret)。该密钥仅仅为保存在服务器中,并且不能向用户公开。然后,使用 header 中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名
在这里插入图片描述

注意 JWT 每部分的作用,在服务端接收到客户端发送过来的 JWT token 之后:header 和 payload 可以直接利用 base64 解码出原文,从 header 中获取哈希签名的算法,从 payload 中获取有效数据 signature 由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验 token 有没有被篡改。服务端获取 header 中的加密算法之后,利用该算法加上 secretKey 对 header、payload 进行加密,比对加密后的数据和客户端发送过来的是否一致。注意 secretKey 只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于 MD5 类型的摘要加密算法,secretKey实际上 代表的是盐值

二、JWT的使用

1. Java中使用JWT

官网推荐了7个Java使用JWT的开源库(可能会随时变动),其中比较推荐使用的是java-jwt和jjwt-root。

详见:https://jwt.io/libraries?language=Java

在这里插入图片描述

引入依赖:

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>

2.实际开发中的应用

在实际的 SpringBoot 项目中,一般我们可以用如下流程做登录:
1.在登录验证通过后,给用户生成一个对应的随机 token(注意这个 token 不是指 jwt,可以用 uuid 等算法生成),然后将这个 token 作为 key 的一部分,用户信息作为 value 存入Redis,并设置过期时间,这个过期时间就是登录失效的时间
2.将第 1 步中生成的随机 token 作为 JWT 的 payload 生成 JWT 字符串返回给前端
3.前端之后每次请求都在请求头中的 Authorization 字段中携带 JWT 字符串
4.后端定义一个拦截器,每次收到前端请求时,都先从请求头中的 Authorization 字段中取出 JWT 字符串并进行验证,验证通过后解析出 payload 中的 token,然后再用这个 token 得到 key,从 Redis 中获取用户信息,如果能获取到就说明用户已经登录。

三、项目中的登录:

1.登录页面

定义路由:当用户输入login的时候跳转页面

,{
    name: "login",
    path: "/login",
    component: () => import("@/components/view/Login.vue")

}

登录页面 :Login.vue:

<template>
  <div class="main">
    <div class="login-body">
      <div class="pic"></div>
      <div class="form">
        <h1>健身会馆</h1>
        <el-form style="padding: 10px" :model="loginFormModel" :rules="rules" ref="loginFormRef">
          <el-row>
            <el-col :span="24">
              <el-form-item label="用户名:" prop="username" :label-width="80">
                <el-input v-model="loginFormModel.username" placeholder="请输入用户名" style="height: 40px;"
                          autocomplete="off"/>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="密码:" prop="password" :label-width="80">
                <el-input type="password" v-model="loginFormModel.password" show-password placeholder="请输入密码"
                          style="height: 40px" autocomplete="off"/>
              </el-form-item>
            </el-col>
          </el-row>

          <el-row>
            <el-col :span="12">
              <el-form-item label="验证码:" prop="captcha" :label-width="80">
                <el-input placeholder="请输入验证码" v-model="loginFormModel.captcha" maxlength="4" minLength="4"
                          style="height: 40px;" autocomplete="off"/>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <img class="captcha" :src="captchaUrl" style="height: 40px;" @click="refresh" alt="验证码">
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-button type="primary" class="login-btn" @click="submitLogin">&nbsp;&nbsp;</el-button>
            </el-col>
          </el-row>
        </el-form>
      </div>
    </div>
    <div class="mask"></div>
    <div class="copyright">
      <h2>&copy;版权所有 Q-健身会馆</h2>
    </div>
  </div>
</template>

<style scoped>
.main {
  height: 100%;
  background: url("@/assets/login_bg.jpg") no-repeat center center/cover;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
}

.main > .mask {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(255, 255, 255, 0.4);
}
.login-body {
  width: 600px;
  height: 450px;
  background-color: rgba(255, 255, 255, 0.6); /* 半透明白色背景 */
  display: flex;
  flex-direction: row;
  z-index: 1000;
  border-radius: 12px; /* 圆角 */
  box-shadow:
      0 6px 16px rgba(0, 0, 0, 0.1), /* 柔和阴影 */
      0 2px 8px rgba(0, 0, 0, 0.08); /* 双层阴影增强层次感 */
  backdrop-filter: blur(4px); /* 背景模糊效果(可选) */
  border: 1px solid rgba(255, 255, 255, 0.3); /* 浅色边框 */
}


.login-body > .form {
  flex-grow: 1;
}

.login-body > .form > h1 {
  font-size: 24px;
  color: #333;
  text-align: center
}

.copyright {
  position: fixed;
  bottom: 100px;
  color: #333;
  font-size: 12px;
}

.login-btn {
  width: 100%;
  height: 50px;
  background-color: #16b777;
  color: #fff;
  font-size: 18px;
}

.captcha {
  cursor: pointer;
}
</style>

<script setup>
import {ref, reactive, toRaw} from "vue";
// import {login as apiLogin} from "@/api/UserApi";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";

//验证码地址
const captchaUrl = ref("/api/users/captcha");

//刷新验证码
function refresh() {
  //此地址会被反向代理成http://localhost:8080的地址
  captchaUrl.value = "/api/users/captcha?id=" + Math.random();
}

//由于此规则不会对数据进行更改,所以没有使用ref函数
const rules = {
  username: [
    {required: true, message: "用户名不可为空"}
  ],
  password: [{
    required: true, message: "密码不可为空"
  }],
  captcha: [{
    required: true, message: "验证码不可为空"
  }, {
    min: 4, max: 4, message: "验证码必须为4位字符"
  }]
};

//登录数据模型
const loginFormModel = reactive({
  username: "admin0",
  password: "123456",
  captcha: ""
});

//只能在setup中使用
const router = useRouter();
let loginFormRef = ref();//表单实例引用
//提交登录
function submitLogin() {
  loginFormRef.value.validate(async valid => {
    if (valid) {
      let model = toRaw(loginFormModel);
      let resp = await apiLogin(model);

      if (resp.success) {
        await router.push("/main/dashboard");
      } else {
        ElMessage.error(resp.error || "登录失败");
        refresh();
      }
    } else {
      ElMessage.error("输入不合法");
      refresh();
    }
  });
}
</script>

实现效果
在这里插入图片描述

2.后端创建使用验证码:

后端:

(1)首先引入验证码的依赖:

 <dependency>
            <groupId>com.github.whvcse</groupId>
            <artifactId>easy-captcha</artifactId>
            <version>1.6.2</version>
        </dependency>

加入后 一定记得刷新Maven才能生效

(2)后端控制层匹配前端的接口,并生成验证码响应

package com.study.controller;

import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.utils.CaptchaUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;


@RestController
@RequestMapping(value = "/api/v1/users",produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
    //登录页面的:

    @GetMapping("/captcha")
    public void captcha(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        //生成验证码:首先引入依赖

       //创建验证码并设置验证码尺寸:
        SpecCaptcha captcha = new SpecCaptcha(140, 40, 4);
        resp.setContentType("image/gif");
        resp.setHeader("Pragma", "No-cache");
        resp.setHeader("Cache-Control", "no-cache");
        resp.setDateHeader("Expires", 0);
        req.getSession().setAttribute("captcha", captcha.text().toLowerCase());
        captcha.out(resp.getOutputStream());
    }

}

验证码的存储—Redis的使用

目前验证码是保存在了session中,我们可以通过使用Redis存放,不使用Redis:

(1)首先引入依赖:
 <!-- Redis的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

引入依赖以后,我们的 RedisTemplate就可以使用了

(2)依赖注入,使用 Redis存储数据
    //redis依赖注入 :
    private RedisTemplate<Object,Object> redisTemplate;
    @Autowired
    public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

然后将验证码数据保存在Redis里面,总的代码:

package com.study.controller;

import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.utils.CaptchaUtil;
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.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.time.Duration;


@RestController
@RequestMapping(value = "/api/v1/users",produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
    //redis依赖注入 :
    private RedisTemplate<Object,Object> redisTemplate;
    @Autowired
    public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    //登录页面的:验证码
    @GetMapping("/captcha")
    public void captcha(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        //生成验证码:首先引入依赖

       //创建验证码并设置验证码尺寸:
        SpecCaptcha captcha = new SpecCaptcha(140, 40, 4);
        resp.setContentType("image/gif");
        resp.setHeader("Pragma", "No-cache");
        resp.setHeader("Cache-Control", "no-cache");
        resp.setDateHeader("Expires", 0);
//        req.getSession().setAttribute("captcha", captcha.text().toLowerCase());
        //使用Redis存储数据:Duration.ofMinutes(3是失效时间三分钟
        redisTemplate.opsForValue().set("captcha", captcha.text().toLowerCase(), Duration.ofMinutes(3));
        captcha.out(resp.getOutputStream());
    }

}

(3) 进行配置,连接上Redis
  #  Redis配置:
  data:
    redis:
      password: 123456

启动Redis,启动后端, 这样验证码就会保存在Redis里面了,刷新前端页面就能看到验证码了。在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.前端发送登录请求

前端写请求方法封装参数往后台传递参数:

import api from "@/util/api.js";

const url = "/users";

function login(params) {
    return api({
        url: url + "/login",
        method: "post",
        data: params
    });
}

export {login}

在这里插入图片描述
然后页面中导入这个方法即可:

import {login} from "@/api/user.js";

至此完整的登录页面代码:

<template>
  <div class="main">
    <div class="login-body">
      <div class="pic"></div>
      <div class="form">
        <h1>健身会馆</h1>
        <el-form style="padding: 10px" :model="loginFormModel" :rules="rules" ref="loginFormRef">
          <el-row>
            <el-col :span="24">
              <el-form-item label="用户名:" prop="username" :label-width="80">
                <el-input v-model="loginFormModel.username" placeholder="请输入用户名" style="height: 40px;"
                          autocomplete="off"/>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="密码:" prop="password" :label-width="80">
                <el-input type="password" v-model="loginFormModel.password" show-password placeholder="请输入密码"
                          style="height: 40px" autocomplete="off"/>
              </el-form-item>
            </el-col>
          </el-row>

          <el-row>
            <el-col :span="12">
              <el-form-item label="验证码:" prop="captcha" :label-width="80">
                <el-input placeholder="请输入验证码" v-model="loginFormModel.captcha" maxlength="4" minLength="4"
                          style="height: 40px;" autocomplete="off"/>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <img class="captcha" :src="captchaUrl" style="height: 40px;" @click="refresh" alt="验证码">
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-button type="primary" class="login-btn" @click="submitLogin">&nbsp;&nbsp;</el-button>
            </el-col>
          </el-row>
        </el-form>
      </div>
    </div>
    <div class="mask"></div>
    <div class="copyright">
      <h2>&copy;版权所有 Q-健身会馆</h2>
    </div>
  </div>
</template>

<style scoped>
.main {
  height: 100%;
  background: url("@/assets/login_bg.jpg") no-repeat center center/cover;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
}

.main > .mask {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(255, 255, 255, 0.4);
}
.login-body {
  width: 600px;
  height: 450px;
  background-color: rgba(255, 255, 255, 0.6); /* 半透明白色背景 */
  display: flex;
  flex-direction: row;
  z-index: 1000;
  border-radius: 12px; /* 圆角 */
  box-shadow:
      0 6px 16px rgba(0, 0, 0, 0.1), /* 柔和阴影 */
      0 2px 8px rgba(0, 0, 0, 0.08); /* 双层阴影增强层次感 */
  backdrop-filter: blur(4px); /* 背景模糊效果(可选) */
  border: 1px solid rgba(255, 255, 255, 0.3); /* 浅色边框 */
}


.login-body > .form {
  flex-grow: 1;
}

.login-body > .form > h1 {
  font-size: 24px;
  color: #333;
  text-align: center
}

.copyright {
  position: fixed;
  bottom: 100px;
  color: #333;
  font-size: 12px;
}

.login-btn {
  width: 100%;
  height: 50px;
  background-color: #16b777;
  color: #fff;
  font-size: 18px;
}

.captcha {
  cursor: pointer;
}
</style>

<script setup>
import {ref, reactive, toRaw} from "vue";
import {login} from "@/api/user.js";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";

//验证码地址
const captchaUrl = ref("/api/users/captcha");

//刷新验证码
function refresh() {
  //此地址会被反向代理成http://localhost:8080的地址
  captchaUrl.value = "/api/users/captcha?id=" + Math.random();
}

//由于此规则不会对数据进行更改,所以没有使用ref函数
const rules = {
  username: [
    {required: true, message: "用户名不可为空"}
  ],
  password: [{
    required: true, message: "密码不可为空"
  }],
  captcha: [{
    required: true, message: "验证码不可为空"
  }, {
    min: 4, max: 4, message: "验证码必须为4位字符"
  }]
};

//登录数据模型
const loginFormModel = reactive({
  username: "admin0",
  password: "123456",
  captcha: ""
});

//只能在setup中使用
const router = useRouter();
let loginFormRef = ref();//表单实例引用
//提交登录
function submitLogin() {
  loginFormRef.value.validate(async valid => {
    if (valid) {
      let model = toRaw(loginFormModel);
      let resp = await login(model);

      if (resp.success) {
        await router.push("/main");
      } else {
        ElMessage.error(resp.error || "登录失败");
        refresh();
      }
    } else {
      ElMessage.error("输入不合法");
      refresh();
    }
  });
}
</script>

4.后端对前端的登录请求参数进行验证

前端传过来三个参数:用户名(管理员的手机号)、密码、验证码,将这三个可以定义成一个实体类,方便我们获取数据操作:Account.java

package com.study.model;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter@AllArgsConstructor
public class Account {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;

    @NotBlank(message = "验证码不能为空")
    private String captcha;
}

然后编写业务类:判断验证码、用户名、密码是否一致(从数据库中查 )

在验证密码的时候,我们使用强加密解密技术:jasypt.这个技术同一个密码每次生成的加密后密码也都不一样,强加密。

首先引入这个技术的依赖:

<!--加密解密库-->
        <dependency>
            <groupId>org.jasypt</groupId>
            <artifactId>jasypt</artifactId>
            <version>1.9.3</version>
        </dependency>

使用这个技术生成123456密码的加密后密码:

package com.study;

import org.jasypt.util.password.StrongPasswordEncryptor;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class AppointmentSystemApplicationTests {

    @Test
    void contextLoads() {
        StrongPasswordEncryptor pe = new StrongPasswordEncryptor();
        String encrypt = pe.encryptPassword("123456");
        System.out.println(encrypt);
    }

}

在这里插入图片描述

将数据库中保存的密码替换成这段即可。

在这里插入图片描述
然后进行密码验证即可。

上面三个的汇总验证代码 :

Service层 :

package com.study.service;

import com.study.model.Admin;

public interface UserService {
    Admin findByPhone(String phone);

}
package com.study.service.impl;

import com.study.mapper.UserMapper;
import com.study.model.Admin;
import com.study.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    private UserMapper userMapper;
    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public Admin findByPhone(String phone) {
        return userMapper.findByPhon(phone);
    }
}

Mapper层 :

package com.study.mapper;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.study.model.Admin;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<Admin> {
    default Admin findByPhon(String phone){
        LambdaQueryWrapper<Admin> qw = Wrappers.lambdaQuery();
        qw.eq(Admin::getPhone,phone);
        return selectOne(qw);
    }
}

Controller层:

package com.study.controller;

import com.study.model.Account;
import com.study.model.Admin;
import com.study.service.UserService;
import com.study.util.JsonResult;
import com.wf.captcha.SpecCaptcha;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jasypt.util.password.StrongPasswordEncryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.time.Duration;


@RestController
@RequestMapping(value = "/api/v1/users", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
    //redis依赖注入 :
    private RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    //service层对象依赖注入 :
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    //强加密加密器:
    private static final StrongPasswordEncryptor PE = new StrongPasswordEncryptor();

    //登录页面的:验证码
    @GetMapping("/captcha")
    public void captcha(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        //生成验证码:首先引入依赖

        //创建验证码并设置验证码尺寸:
        SpecCaptcha captcha = new SpecCaptcha(140, 40, 4);
        resp.setContentType("image/gif");
        resp.setHeader("Pragma", "No-cache");
        resp.setHeader("Cache-Control", "no-cache");
        resp.setDateHeader("Expires", 0);
//        req.getSession().setAttribute("captcha", captcha.text().toLowerCase());
        //使用Redis存储数据:Duration.ofMinutes(3是失效时间三分钟
        redisTemplate.opsForValue().set("captcha", captcha.text().toLowerCase(), Duration.ofMinutes(3));
        captcha.out(resp.getOutputStream());
    }


    //登录 :
    @PostMapping("/login")
    //前端传递三个参数:用户名,密码 ,验证码 ,写个登录模型:Account,这样 接收登录模型即可:
    public ResponseEntity<JsonResult<?>> login(@RequestBody Account account) {
        //验证验证码是否正确,将用户输入的验证码和Redis里面的验证码 比较
        //所以首先从Redis里面把存的验证码取出来:
        String correct = (String) redisTemplate.opsForValue().get("captcha");
        if (!account.getCaptcha().equals(correct)) {
            return ResponseEntity.ok(JsonResult.fail("验证码输入不正确"));
        }

        //验证码验证完成后,接下来验证用户名(管理员的手机号)和密码:写业务类实现:
        Admin user = userService.findByPhone(account.getUsername());
        if (user == null) {
            ResponseEntity.ok(JsonResult.fail("用户不存在"));
        }

        //用户名(管理员电话)校验完成之后,校验密码:jasypt:加密解密库:首先引入依赖
        //然后定义强密码加密器:进行校验
        boolean pass = PE.checkPassword(account.getPassword(), user.getPassword());
        if(!pass){
            ResponseEntity.ok(JsonResult.fail("密码不正确"));
        }
        

        //验证 都通过之后:颁发jwt:

        return null;//下面继续完善

    }
}

5.校验通过后颁发JWT完成响应

(1)引入依赖:

<!--jwt-->
        <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.5.0</version>
        </dependency>

(2)创建工具类 :JWT不止一次会用到,作用:创建JWT、验证JWT

构建者模式:Builder(一步一步指定参数最终创建出对象)

package com.study.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Map;

public class JwtUtils {
    //创建jwt
    public static String createJwt(String id, String username, Map<String, Object> payloads,
                                   LocalDateTime expireTime, String secret) {
        JWTCreator.Builder builder = JWT.create();
        //指定载荷:多个参数:
        String jwt = builder.withPayload(payloads)
                .withExpiresAt(expireTime.toInstant(ZoneOffset.of("+8")))
                .withIssuer("Q")//颁发者
                .withIssuedAt(Instant.now())//颁发时间(当前时间)
                .withSubject("身份认证")//令牌主题
                .withAudience(username)//用户名:颁发给谁
                .withJWTId(id)//唯一编号
                .sign(Algorithm.HMAC256(secret));//签名加密算法
        return jwt;
    }
}

(3)使用工具类创建JWT

其中密钥最好放在配置文件里面,越复杂越好


# jwt密钥:123456加密之后的:
jwt:
   secret: sJU9CgKWaLlIR6/qbO/VvFd3GNVurqVRFo+8Wc64wP2XLAqQV8VcehNzLGppi5PB

在这里插入图片描述

   //JWT:
    @Value("${jwt.secret}")
    private String jwtSecret;
package com.study.controller;

import com.study.model.Account;
import com.study.model.Admin;
import com.study.service.UserService;
import com.study.util.JsonResult;
import com.study.util.JwtUtils;
import com.wf.captcha.SpecCaptcha;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jasypt.util.password.StrongPasswordEncryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;


@RestController
@RequestMapping(value = "/api/v1/users", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
    //redis依赖注入 :
    private RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    //service层对象依赖注入 :
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    //强加密加密器:
    private static final StrongPasswordEncryptor PE = new StrongPasswordEncryptor();

    //JWT:
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    //登录页面的:验证码
    @GetMapping("/captcha")
    public void captcha(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        //生成验证码:首先引入依赖

        //创建验证码并设置验证码尺寸:
        SpecCaptcha captcha = new SpecCaptcha(140, 40, 4);
        resp.setContentType("image/gif");
        resp.setHeader("Pragma", "No-cache");
        resp.setHeader("Cache-Control", "no-cache");
        resp.setDateHeader("Expires", 0);
//        req.getSession().setAttribute("captcha", captcha.text().toLowerCase());
        //使用Redis存储数据:Duration.ofMinutes(3是失效时间三分钟
        redisTemplate.opsForValue().set("captcha", captcha.text().toLowerCase(), Duration.ofMinutes(3));
        captcha.out(resp.getOutputStream());
    }


    //登录 :
    @PostMapping("/login")
    //前端传递三个参数:用户名,密码 ,验证码 ,写个登录模型:Account,这样 接收登录模型即可:
    public ResponseEntity<JsonResult<?>> login(@RequestBody Account account) {
        //验证验证码是否正确,将用户输入的验证码和Redis里面的验证码 比较
        //所以首先从Redis里面把存的验证码取出来:
        String correct = (String) redisTemplate.opsForValue().get("captcha");
        if (!account.getCaptcha().equals(correct)) {
            return ResponseEntity.ok(JsonResult.fail("验证码输入不正确"));
        }

        //验证码验证完成后,接下来验证用户名(管理员的手机号)和密码:写业务类实现:
        Admin user = userService.findByPhone(account.getUsername());
        if (user == null) {
            ResponseEntity.ok(JsonResult.fail("用户不存在"));
        }

        //用户名(管理员电话)校验完成之后,校验密码:jasypt:加密解密库:首先引入依赖
        //然后定义强密码加密器:进行校验
        boolean pass = PE.checkPassword(account.getPassword(), user.getPassword());
        if(!pass){
            ResponseEntity.ok(JsonResult.fail("密码不正确"));
        }


        //验证 都通过之后:颁发jwt:
        String jwt = JwtUtils.createJwt(UUID.randomUUID().toString(), user.getPhone(),
                Map.of("username", user.getPhone(), "userId", user.getId()),
                LocalDateTime.now().plusMinutes(30), jwtSecret);
        System.out.println(jwt);

        return ResponseEntity.ok(JsonResult.success(jwt));

    }
}

6.前端接收响应

后端传过来了响应的 JWT,前端要保存JWT

前端接收:

jwt.js:

const jwtKey = "@#jwt_key";

function saveJwt(jwt) {
    sessionStorage.setItem(jwtKey, jwt);
}

function getJwt() {
    return sessionStorage.getItem(jwtKey);
}

function removeJwt() {
    sessionStorage.removeItem(jwtKey);
}

export {saveJwt, getJwt, removeJwt}

在login.vue里面引入:jwt.js:

并响应

<template>
  <div class="main">
    <div class="login-body">
      <div class="pic"></div>
      <div class="form">
        <h1>健身会馆</h1>
        <el-form style="padding: 10px" :model="loginFormModel" :rules="rules" ref="loginFormRef">
          <el-row>
            <el-col :span="24">
              <el-form-item label="用户名:" prop="username" :label-width="80">
                <el-input v-model="loginFormModel.username" placeholder="请输入用户名" style="height: 40px;"
                          autocomplete="off"/>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="密码:" prop="password" :label-width="80">
                <el-input type="password" v-model="loginFormModel.password" show-password placeholder="请输入密码"
                          style="height: 40px" autocomplete="off"/>
              </el-form-item>
            </el-col>
          </el-row>

          <el-row>
            <el-col :span="12">
              <el-form-item label="验证码:" prop="captcha" :label-width="80">
                <el-input placeholder="请输入验证码" v-model="loginFormModel.captcha" maxlength="4" minLength="4"
                          style="height: 40px;" autocomplete="off"/>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <img class="captcha" :src="captchaUrl" style="height: 40px;" @click="refresh" alt="验证码">
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-button type="primary" class="login-btn" @click="submitLogin">&nbsp;&nbsp;</el-button>
            </el-col>
          </el-row>
        </el-form>
      </div>
    </div>
    <div class="mask"></div>
    <div class="copyright">
      <h2>&copy;版权所有 Q-健身会馆</h2>
    </div>
  </div>
</template>

<style scoped>
.main {
  height: 100%;
  background: url("@/assets/login_bg.jpg") no-repeat center center/cover;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
}

.main > .mask {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(255, 255, 255, 0.4);
}
.login-body {
  width: 600px;
  height: 450px;
  background-color: rgba(255, 255, 255, 0.6); /* 半透明白色背景 */
  display: flex;
  flex-direction: row;
  z-index: 1000;
  border-radius: 12px; /* 圆角 */
  box-shadow:
      0 6px 16px rgba(0, 0, 0, 0.1), /* 柔和阴影 */
      0 2px 8px rgba(0, 0, 0, 0.08); /* 双层阴影增强层次感 */
  backdrop-filter: blur(4px); /* 背景模糊效果(可选) */
  border: 1px solid rgba(255, 255, 255, 0.3); /* 浅色边框 */
}


.login-body > .form {
  flex-grow: 1;
}

.login-body > .form > h1 {
  font-size: 24px;
  color: #333;
  text-align: center
}

.copyright {
  position: fixed;
  bottom: 100px;
  color: #333;
  font-size: 12px;
}

.login-btn {
  width: 100%;
  height: 50px;
  background-color: #16b777;
  color: #fff;
  font-size: 18px;
}

.captcha {
  cursor: pointer;
}
</style>
<script setup>
import {ref, reactive, toRaw} from "vue";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";
import {login} from "@/api/user.js";

//验证码地址
const captchaUrl = ref("/api/users/captcha");

//刷新验证码
function refresh() {
  //此地址会被反向代理成http://localhost:8080的地址
  captchaUrl.value = "/api/users/captcha?id=" + Math.random();
}

//由于此规则不会对数据进行更改,所以没有使用ref函数
const rules = {
  username: [
    {required: true, message: "用户名不可为空"}
  ],
  password: [{
    required: true, message: "密码不可为空"
  }],
  captcha: [{
    required: true, message: "验证码不可为空"
  }, {
    min: 4, max: 4, message: "验证码必须为4位字符"
  }]
};

//登录数据模型
const loginFormModel = reactive({
  username: "13312341234",
  password: "123456",
  captcha: ""
});

let loginFormRef = ref();//表单实例引用

import {saveJwt} from "@/api/jwt.js";
import router from "@/router/index.js";

//提交登录
function submitLogin() {
  loginFormRef.value.validate(async valid => {
    if (valid) {
      let model = toRaw(loginFormModel);
      let resp = await login(model);

      if (resp.success) {
        //保存jwt
        saveJwt(resp.data);
        //路由:登录成功后跳转
        router.push("/main");
      } else {
        ElMessage.error(resp.error || "登录失败");
        refresh();
      }
    } else {
      ElMessage.error("输入不合法");
      refresh();
    }
  });
}
</script>

至此颁发JWT结束,能够实现正常登录。

配置请求拦截器:请求出错之后的处理:

api.js

import axios from "axios";
import {getJwt, removeJwt} from "@/api/jwt.js";

//创建一个axios实例
let api = axios.create({
    baseURL: "/api",
    timeout: 3000
});

//配置响应拦截器
api.interceptors.response.use(resp => {
    return resp.data;
}, resp => {//非200状态码
    //console.log(resp);
    let data = resp.response.data;//后台响应的数据
    if (resp.status === 401) {//认证未通过
        removeJwt();
        location.href = "/login";
    }
    return data;
});

//配置请求拦截器
api.interceptors.request.use(config => {
    //在请求头中携带jwt
    config.headers["Authorization"] = getJwt();
    return config;
}, error => {
    return Promise.reject(error);
});

export default api;

7.后端添加拦截器:

在这里插入图片描述
导入JSON库:

        <!-- JSON:-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.57</version>
        </dependency>

拦截器代码:

package com.study.common;

import com.alibaba.fastjson2.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.study.util.JsonResult;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.PrintWriter;

@Component
public class JwtInterceptor implements HandlerInterceptor {
    //配置JWT:
    @Value("${jwt.secret}")
    private String jwtSecret;

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        //取出jwt
        String jwt = req.getHeader("Authorization");
        //核验器:对jwt解密:
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256(jwtSecret)).build();
        //获取解密后的jwt
        try {
            DecodedJWT dj = verifier.verify(jwt);
            //成功
            //String username = dj.getAudience().getFirst();
            return true;
        } catch (JWTVerificationException e) {
            //失败
            String msg = "jwt无效或过期";
            resp.setStatus(HttpStatus.UNAUTHORIZED.value());//401
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            out.write(JSON.toJSONString(JsonResult.fail(msg)));
            out.flush();
            return false;
        }
    }
}

然后添加拦截情况的代码:

package com.study.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.study.common.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CommonConfig implements WebMvcConfigurer {
    private JwtInterceptor jwtInterceptor;

    @Autowired
    public void setJwtInterceptor(JwtInterceptor jwtInterceptor) {
        this.jwtInterceptor = jwtInterceptor;
    }

    //mybatis-plus自动分页拦截器
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/*/users/login/**",
                        "/api/*/users/logout/**",
                        "/api/*/users/captcha/**");
    }
}

在这里插入图片描述

8. 前端拦截器:

api.js

import axios from "axios";
import {getJwt, removeJwt} from "@/api/jwt.js";

//创建一个axios实例
let api = axios.create({
    baseURL: "/api",
    timeout: 3000
});

//配置响应拦截器
api.interceptors.response.use(resp => {
    return resp.data;
}, resp => {//非200状态码
    //console.log(resp);
    let data = resp.response.data;//后台响应的数据
    if (resp.status === 401) {//认证未通过
        removeJwt();
        location.href = "/login";
    }
    return data;
});

//配置请求拦截器
api.interceptors.request.use(config => {
    //在请求头中携带jwt
    config.headers["Authorization"] = getJwt();
    return config;
}, error => {
    return Promise.reject(error);
});

export default api;

未登录的跳转到登录页面:路由守卫:

index.js

//定义路由转发器
import {createRouter, createWebHistory} from "vue-router"
import {getJwt} from "@/api/jwt.js";

//定义路由
const routes = [
    {
        name: "main",
        path: "/main",
        component: () => import("@/components/view/Main.vue"),
        children: [
            {
                name: "customer",
                path: "/main/customer",
                component: () => import("@/components/view/Customer.vue")
            }, {
                name: "pool",
                path: "/main/pool",
                component: () => import("@/components/view/Pool.vue")
            }
        ]
    }, {
        name: "index",
        path: "/",
        redirect: "/main/customer"
    }, {
        name: "login",
        path: "/login",
        component: () => import("@/components/view/Login.vue")
    }
];

//定义路由转发器
const router = createRouter({
    routes,
    history: createWebHistory()
});

//配置路由守卫
router.beforeEach((to, from, next) => {
    let jwt = getJwt();
    if (jwt) {
        if (to.name === "login") {
            next("/main");
        } else {
            next();
        }
    } else {
        if (to.name !== "login") {
            next("/login");
        } else {
            next();
        }
    }
});

export default router;

四、注销功能

(1)添加页面显示:

在这里插入图片描述

<div>
   <a class="logout-btn" href="#" @click="logout">注销</a>
</div>

总体的代码 :

<template>
  <!--  页面布局-->
  <div class="common-layout h100">
    <el-container class="h100">
      <!--头部-->
      <el-header>
        <div class="logo"></div>
        <h1 class="system-title">健身会馆客户预约管理系统</h1>
         <div>
           <a class="logout-btn" href="#" @click="logout">注销</a>
         </div>
      </el-header>
      <el-container style="height: 100vh;">
        <el-aside width="200px" >
          <!-- 导航菜单,加上路由是实现跳转  -->
          <el-menu class="nav h100" router text-color="#fff" active-text-color="#ffd04b"
                   background-color="#545c64" default-active="/dashboard">
            <!-- /dashboard是数据看板页/欢迎页-->
            <!--遍历循环:mi.children(children是名字,跟下面是对应的)-->
            <template v-for="mi in menuItems">
              <el-sub-menu v-if="Array.isArray(mi.children)" :index="mi.url || mi.name">
                <template #title>
                  <span>{{ mi.name }}</span>
                </template>
                <el-menu-item
                    v-for="smi in mi.children"
                    :index="smi.url"
                    :key="smi.url"
                >
                  <span>{{ smi.name }}</span>
                </el-menu-item>
              </el-sub-menu>
              <el-menu-item v-else :index="mi.url" :key="mi.url">
                <span>{{ mi.name }}</span>
              </el-menu-item>
            </template>
          </el-menu>
        </el-aside>
        <!-- 二级导航 :router -->
        <el-main>
          <router-view></router-view>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<style scoped>

.h100 {
  height: 100%;
}

header {
  height: 135px;
  background-color: aliceblue;
  display: flex;
}

header > .logo {
  height: 135px;
  width: 170px;
  background: url("@/assets/logo.png") no-repeat center center/cover;
}

aside {
  width: 200px;
  background-color: #545c64;
}

.nav {
  border-right: none;
}

.logout-btn {
  display: inline-block;
  position: absolute;
  right: 10px;
  top: 25px;
}
aside {
  width: 200px;
  background-color: #545c64;
}

.nav {
  border-right: none;
  height: 100%;
}

.el-header {
  display: flex;
  align-items: center; /* 垂直居中 */
  justify-content: center; /* 水平居中 */
  height: 75px;
  background-color: aliceblue;
  position: relative; /* 为logo定位做准备 */
}

.system-title {
  font-size: 24px; /* 调整字体大小 */
  font-weight: bold; /* 加粗 */
  margin: 0; /* 去除默认边距 */
  text-align: center; /* 文字居中 */
  flex-grow: 1; /* 占据剩余空间 */
}

</style>

<script setup>
import {reactive} from "vue";
import {removeJwt} from "@/api/jwt.js";
import router from "@/router/index.js";
//所有导航菜单
const menuItems = reactive([
  {
    name: "数据看板",
    url: "/main/dashboard"
  },
  { name: "客户管理",
    url: "/main/members", // 添加父级url
    children: [
      {
        name: "客户列表",
        url: "/main/members" // 修改为/main/members
      }
    ]
  },
  {
    name: "课程管理",
    children: [
      {
        name: "课程列表",
        url: "/main/course"
      },
      {
        name: "课程日历",
        url: "/main/role"
      }
    ]
  },
  {
    name: "教练管理",
    children: [
      {
        name: "教练列表",
        url: "/main/coach"
      }
    ]
  },
  {
    name: "管理员管理",
    children: [
      {
        name: "管理员列表",
        url: "/main/admin"
      }
    ]
  }
]);

//注销
function logout() {
  removeJwt();
  router.push("/login");
}
</script>

(2)添加注销的对应的方法实现:

//注销
function logout() {
  removeJwt();
  router.push("/login");
}

在这里插入图片描述

五、总结

首先用户名认证,认证成功后通过库颁发JWT

写拦截器,拦截请求,取出JWT进行校验,验证 JWT是否有效,有效就放行,无效就给用户一个提示信息。可以对客户端进行响应或者操作。

前端每次往后端发送请求都要携带JWT,前端用什么名字发,后端就要用什么名字取。路由守卫。

至此,登录、退出、每个版块的 增删改查已经实现,相关源码已经上传 。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到