鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

这篇具有很好参考价值的文章主要介绍了鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。



初衷:
一直不太理解整个前后端的鉴权,跨域等问题,抽空两个晚上整理出万字文章,也是对于自己的一个交代,现在共享出来,希望大家也能受益,将使用过程在这里一一详述,还是多说一句,本来是不做限制的,但是为了让更多的朋友看见,有违初心,在这里向大家道歉,希望理解,设置为粉丝可见!我不会打扰大家的生活,如果大家需要任何帮助,私信我,我一定全力以赴,我们一起努力,一起为了梦想加油!!!

注意:如果代码有报错或者疑问,请看源码
源码地址(免费)

1.创建后端项目

项目初始化

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

为了方便快捷开发,我们直接使用Spring Initializr快速创建项目,注意一下,Java版本,软件包名称啥的,咋们一次性写好,虽然后面可以改正,但是没必要吃回头草,咋们尽量做到一镜到底!

解释:Spring Initializr是一个用于创建Spring Boot项目的在线工具。在创建项目时,您可以选择所需的依赖项和配置选项。

如果您想在Spring Initializr中添加Web依赖项,可以在"Dependencies"选项卡下找到"Web"子选项卡。在该子选项卡下,您可以勾选所需的Web依赖项,例如Spring Web、Spring MVC等。

勾选所需的Web依赖项后,Spring Initializr会自动为您生成一个包含这些依赖项的Maven POM文件。您可以使用该文件来构建您的Spring Boot项目。

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

注意哈,有的同学这里可能是默认的服务器URL,这样有一个问题,国外的服务器地址,有时候加载难免超时,速度不够,给力,大家可以换成国内的阿里地址

接下来,我们把相关的包直接引入,在这里的操作,相当于在pom文件中添加依赖

我们主要添加如下几个:

Developer Tools -> Lombok

Lombok是一个Java编程框架,它可以简化Java代码的编写,提高开发效率。它的主要作用是减少Java代码中的样板代码(boilerplate code),即重复性、冗长的代码,使开发人员能够更加专注于业务逻辑的实现。

Lombok提供了一些注解和库,可以帮助开发人员快速地生成getter、setter、构造函数、equals、hashCode等常用方法,从而减少代码量。此外,Lombok还提供了一些工具类,如@Data、@ToString、@AllArgsConstructor等,可以帮助开发人员更好地管理Java对象的属性和状态。

使用Lombok需要在项目中引入相应的依赖包,然后在Java代码中使用相应的注解或库即可。Lombok的使用可以提高代码的可读性和可维护性,减少开发人员的学习成本和工作量。

下面是一个使用Lombok的例子:

import lombok.Data; // 自动生成getter/setter方法
import lombok.NoArgsConstructor; // 自动生成无参构造方法
@Data
public class User {
   private int id;
   private String name;
   
   public User(int id, String name) {
       this.id = id;
       this.name = name;
   }
}


在这个例子中,我们使用了Lombok的@Data注解来自动生成getter和setter方法,以及@NoArgsConstructor注解来自动生成无参构造方法。这样,我们就可以省去手动编写getter和setter方法以及构造方法的过程,从而减少了代码量和开发时间。

Web ->Spring Web

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

其实就是pom里面那些web依赖库

sql -> sql driver,mybatis

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

这里就是默认添加一下MySQL驱动和mybatis依赖,用作数据查询

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

这样,一个简单的项目我们就完美构建出来了,接下来,配置文件分为三种,做一个介绍:

Spring Boot的配置文件有三种格式:properties、yml和yaml,它们的区别如下:

  1. 语法不同:properties文件使用键值对的方式来配置,每个属性以“=”结尾;yml文件使用缩进来表示层级关系,属性之间用冒号分隔;yaml文件也是使用缩进来表示层级关系,但不同之处在于属性值可以包含引号和换行符。
  2. 读写方式不同:properties文件可以直接读取,也可以使用Java代码来读取;yml文件需要使用特定的库(如YAML库)来解析;yaml文件也需要使用特定的库(如PyYAML库)来解析。
  3. 功能特性不同:在Spring Boot中,properties文件已经被标记为过时,建议使用yml或yaml文件来进行配置;yml文件支持更多的数据类型,比如数组、映射等;yaml文件还支持注释,可以在注释中说明配置的意义。

总之,选择哪种配置文件格式取决于个人喜好和项目需求。如果只是简单的配置,可以使用properties文件;如果需要更复杂的配置结构,可以使用yml或yaml文件。

然后大家看一下自动导入的依赖(大概瞅一眼)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.xiaohui</groupId>
    <artifactId>xiaohui-student-system</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>xiaohui-student-system</name>
    <description>xiaohui-student-system</description>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.xiaohui.system.XiaohuiStudentSystemApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

Springboot配置文件修改

在这里,不修改也能用,但是为了后期愉快的开发,比如MySQL日志输出啥的,我们配置一下,避免出问题

server:
    port: 8080  #端口
spring:
    datasource: 
   	    #数据库配置
        url: jdbc:mysql://localhost:3306/students?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
        username: root  #用户名
        password: root  #密码
        driver-class-name: com.mysql.cj.jdbc.Driver  #驱动
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
            minimum-idle: 0
            maximum-pool-size: 20
            idle-timeout: 10000
            auto-commit: true
            connection-test-query: select 1
# 显示SQL语句
mybatis:
    mapper-locations: classpath:mappers/*xml  #注意,创建mapper.xml的时候,需要遵循这个规则,再resource下面建立mappers
    type-aliases-package: com.xiaohui.system.entity  #注意自己的实体类位置
    configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  #sql日志输出

运行项目

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

可见项目成功运行了

2.创建前端项目

项目初始化

在这里,node的安装教程我就不再分享,网络上有许多小伙伴的文章很棒哒,找一个安装就可以,这里就直接演示如何创建vue项目

  1. 打开后端项目所在目录
  2. 使用cmd窗口打开

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

3.执行命令进行创建项目

vue create  xiaohui-student-system-ui

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

这里大家就先选vue2的版本,vue3的版本自己可以尝试,由于自身能力欠缺,考虑到需要兼容elemui组件,暂不使用

完成后大家使用vscode或者webstorm打开,我这里使用webstorm进行展示

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

配置文件修改

这里,项目其实可以直接启动的,但是我们先不启动,先把语法严格检查关掉,不然一个逗号就可能报错

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  lintOnSave:false, //关闭eslint检查
  devServer: {
    client: {
      overlay: {
        warnings: false, //不显示警告
        errors: false	//不显示错误
      }
    }
  }
})


运行项目

1.第一种:终端命令运行

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

2.第二种:配置webstorm启动器

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

这样,我们前后端项目都就创建好了,接下来我们就仔细编写一下后端的数据查询代码

3.创建数据表

大家可以自行创建,这里作为演示,就先创建一个用户信息表,大家理解!

CREATE TABLE `user_table` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `user_name` varchar(255) DEFAULT NULL COMMENT '用户姓名',
  `user_pwd` varchar(255) DEFAULT NULL COMMENT '用户密码',
  `user_status` varchar(255) DEFAULT NULL COMMENT '用户状态',
  `user_role` varchar(255) DEFAULT NULL COMMENT '用户权限',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

4.业务层次划分

在这里,再多说几句

在 MyBatis 中,通常会将实体类、DAO 接口和对应的值对象(VO)分开定义,以实现更好的代码复用和可维护性。下面分别介绍它们的用途和区别:

  1. 实体类(Entity)

实体类用于表示数据库中的表与字段的映射关系,通常包含属性和 getter/setter 方法。它的主要作用是作为数据传输的对象,将数据库中的数据映射到 Java 对象中。

实体类的属性通常是直接从数据库表中获取的,因此它的属性名和类型应该与数据库表中的列名和数据类型一一对应。同时,实体类也可以添加一些额外的属性和方法,例如计算属性、校验属性等。

  1. DAO 接口(Dao)

DAO 接口用于定义对数据库的操作方法,包括增删改查等基本操作,通常需要传入实体类作为参数,并返回对应的结果对象。它的主要作用是提供一个标准的接口,供业务层调用。

DAO 接口中的每个方法都对应着一个具体的 SQL 语句,MyBatis 会根据该方法的名称和参数自动生成对应的 SQL 语句。因此,使用 DAO 接口可以避免手写 SQL 语句的繁琐和错误。

  1. VO(Value Object)

VO 用于封装一些常量或辅助数据,通常包含一些静态方法和属性。VO 可以被多个 DAO 接口共享,以避免重复的代码。它的主要作用是提供一些公共的数据结构和方法,方便业务层进行数据处理。

VO 通常包含以下内容:

  • 常量:用于存储一些固定不变的数据,例如日期、时间、枚举值等。
  • 静态方法:用于执行一些简单的计算或校验逻辑,例如字符串长度、数字比较等。
  • 实例方法:用于返回一些查询结果或缓存数据,例如将查询结果封装成一个对象返回给客户端。

总之,实体类用于映射数据库表,DAO 接口用于定义对数据库的操作方法,VO 则用于提供一些公共的数据结构和方法。它们三者之间相互独立,但又密切相关,共同构成了 MyBatis 的基本架构。

对于每个层做一个赘述

  1. DAO(Data Access Object):数据访问对象,用于与数据库进行交互操作,提供对数据的增删改查等操作。
  2. Entity:实体类,用于描述数据表中的每一行记录,包含属性和getter/setter方法。
  3. Mapper.xml:MyBatis中的映射文件,用于将SQL语句映射到Java接口中的方法上,实现对数据库的操作。
  4. Mapper:Mapper接口,定义了与数据库进行交互的方法,通过注解或XML配置文件来实现。
  5. Service:服务层,用于处理业务逻辑,包括对DAO的操作、事务管理等。
  6. ServiceImpl:服务实现类,实现了Service接口中的方法,通常会使用@Autowired注解来自动注入DAO对象。
  7. Controller:控制器,用于接收前端请求并将其转发给Service层进行处理,同时返回结果给前端。

它们之间的关系如下:

  1. DAO与Entity和Mapper的关系:DAO通过Entity和Mapper来访问数据库,实现数据的增删改查等操作。
  2. Service与ServiceImpl和Mapper的关系:Service层通过ServiceImpl来实现具体的方法,调用Mapper进行数据库操作。
  3. Controller与Service和ServiceImpl的关系:Controller层通过Service层来处理业务逻辑和事务控制,调用ServiceImpl中的相关方法来实现具体的业务逻辑。

创建entity层的实体类

package com.xiaohui.system.entity;

import lombok.Data;

/**
 * @Description 用户信息实体类
 * @Author IT小辉同学
 * @Date 2023/05/17
 */
@Data

public class User {
    /**
     * 用户id
     */
    private Long userId;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 用户密码
     */
    private String userPwd;
    /**
     * 用户状态
     */
    private String userStatus;
    /**
     * 用户角色
     */
    private String userRole;
    /**
     * 创建时间
     */
    private Date createTime;
}

创建mapper.xml映射

在这个里面,我们写了一条查询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.xiaohui.system.mapper.UserMapper">
    <resultMap id="UserResultVO" type="com.xiaohui.system.entity.User">
        <result property="userId" column="user_id"/>
        <result property="userName" column="user_name"/>
        <result property="userPwd" column="user_pwd"/>
        <result property="userStatus" column="user_status"/>
        <result property="userRole" column="user_role"/>
    </resultMap>
    <!--获取所有用户信息-->
    <select id="selectAllUserInfo" resultMap="UserResultVO" parameterType="com.xiaohui.system.entity.User">
        SELECT *
        FROM user_table
    </select>
</mapper>

创建mapper接口

注意啊,不要少了注释,这个很重要

package com.xiaohui.system.mapper;
import com.xiaohui.system.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

/**
 * @Description 用户映射器
 * @Author IT小辉同学
 * @Date 2023/05/17
 */
@Mapper
public interface UserMapper {
    /**
     * @Param user 用户
     * @Return {@link List }<{@link User }>
     * @Description 获取所有用户信息
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    List<User> selectAllUserInfo(User user);
}

创建service服务层

package com.xiaohui.system.service;
import com.xiaohui.system.entity.User;
import java.util.List;

public interface UserService {
    /**
     * @param user 用户
     * @return {@link List }<{@link User }>
     * @Description 获取所有用户信息
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    List<User> selectAllUserInfo(User user);
}

创建serviceImpl服务实现类

package com.xiaohui.system.service.Impl;
import com.xiaohui.system.entity.User;
import com.xiaohui.system.mapper.UserMapper;
import com.xiaohui.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @Description 用户服务实现类
 * @Author IT小辉同学
 * @Date 2023/05/17
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    /**
     * @param user 用户
     * @return {@link List }<{@link User }>
     * @Description 获取所有用户信息
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    @Override
    public List<User> selectAllUserInfo(User user) {
        return userMapper.selectAllUserInfo(user);
    }
}

创建Controller控制器

在这里,我们对应返回体需要处理一下,这里搬过来若依的一套封装,很好用,大家放到Utils里面就行

package com.xiaohui.system.Utils;

/**
 * @Description 返回状态码
 * @Author IT小辉同学
 * @Date 2023/05/17
 */
public class HttpStatus {
    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    /**
     * 对象创建成功
     */
    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    /**
     * 操作已经执行成功,但是没有返回数据
     */
    public static final int NO_CONTENT = 204;

    /**
     * 资源已被移除
     */
    public static final int MOVED_PERM = 301;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 资源没有被修改
     */
    public static final int NOT_MODIFIED = 304;

    /**
     * 参数列表错误(缺少,格式不匹配)
     */
    public static final int BAD_REQUEST = 400;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限,授权过期
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    /**
     * 不允许的http方法
     */
    public static final int BAD_METHOD = 405;

    /**
     * 资源冲突,或者资源被锁
     */
    public static final int CONFLICT = 409;

    /**
     * 不支持的数据,媒体类型
     */
    public static final int UNSUPPORTED_TYPE = 415;

    /**
     * 系统内部错误
     */
    public static final int ERROR = 500;

    /**
     * 接口未实现
     */
    public static final int NOT_IMPLEMENTED = 501;

    /**
     * 系统警告消息
     */
    public static final int WARN = 601;
}

package com.xiaohui.system.Utils;

import java.util.HashMap;
/**
 * @Description 请求返回体
 * @Author IT小辉同学
 * @Date 2023/05/17
 */
public class AjaxResult extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;

    /** 状态码 */
    public static final String CODE_TAG = "code";

    /** 返回内容 */
    public static final String MSG_TAG = "msg";

    /** 数据对象 */
    public static final String DATA_TAG = "data";

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult()
    {
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     */
    public AjaxResult(int code, String msg)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     * @param data 数据对象
     */
    public AjaxResult(int code, String msg, Object data)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (StringUtils.isNotNull(data))
        {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success()
    {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data)
    {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
    }

    /**
     * 返回警告消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult warn(String msg)
    {
        return AjaxResult.warn(msg, null);
    }

    /**
     * 返回警告消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult warn(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.WARN, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return 错误消息
     */
    public static AjaxResult error()
    {
        return AjaxResult.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 错误消息
     */
    public static AjaxResult error(String msg)
    {
        return AjaxResult.error(msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 错误消息
     */
    public static AjaxResult error(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.ERROR, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @param code 状态码
     * @param msg 返回内容
     * @return 错误消息
     */
    public static AjaxResult error(int code, String msg)
    {
        return new AjaxResult(code, msg, null);
    }

    /**
     * 方便链式调用
     *
     * @param key 键
     * @param value 值
     * @return 数据对象
     */
    @Override
    public AjaxResult put(String key, Object value)
    {
        super.put(key, value);
        return this;
    }
}

package com.xiaohui.system.controller;

import com.xiaohui.system.Utils.AjaxResult;
import com.xiaohui.system.entity.User;
import com.xiaohui.system.service.Impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

/**
 * @Description 用户控制器
 * @Author IT小辉同学
 * @Date 2023/05/17
 */
@CrossOrigin
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserServiceImpl userService;

    /**
     * @param user 用户
     * @return {@link AjaxResult }
     * @Description 获取所有用户信息
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    @GetMapping("/list")
    private AjaxResult selectAllUserInfo() {
        User user=new User();
        return AjaxResult.success(userService.selectAllUserInfo(user));
    }
}


做一个小小的拓展,加深大家的印象,关于各种请求和接参形式再啰嗦几句,一定要把知识盲区补上,不能留下漏洞,不然后面真实开发遇到问题很麻烦!!!
Spring Boot 是一个基于 Spring 框架的开发框架,它提供了非常方便和高效的方式来创建和部署独立运行的、生产级别的 Web 应用程序。在 Spring Boot 的 Controller 层中,我们可以使用如下注解来定义请求方法、请求参数等:

  1. @RequestMapping

@RequestMapping 注解用于映射 HTTP 请求 URL 到相应的处理方法,可以定义 class 级别或 method 级别的映射。

@RestController
@RequestMapping("/api/user")
public class UserController {
    // ...
}
  1. @PostMapping, @GetMapping, @PutMapping, @DeleteMapping

这些是相应注解的缩写形式,用于简化 @RequestMapping 的使用方式,并且能够根据不同的请求方法来限定映射范围。

@RestController
@RequestMapping("/api/user")
public class UserController {
    @PostMapping("/")
    public User createUser(@RequestBody User user) {
        // 处理新增用户请求
        return userService.createUser(user);
    }

    @GetMapping("/{userId}")
    public User getUserById(@PathVariable Long userId) {
        // 查询指定 ID 的用户
        return userService.getUserById(userId);
    }

    @PutMapping("/{userId}")
    public User updateUser(@PathVariable Long userId, @RequestBody User newUser) {
        // 更新指定 ID 的用户信息
        return userService.updateUser(userId, newUser);
    }

    @DeleteMapping("/{userId}")
    public void deleteUser(@PathVariable Long userId) {
        // 删除指定 ID 的用户
        userService.deleteUser(userId);
    }
}
  1. @RequestBody

@RequestBody 注解主要用于处理请求体中传递的数据,将请求体中的 JSON 或 XML 数据绑定到 Java 对象上。

@RestController
@RequestMapping("/api/user")
public class UserController {
    @PostMapping("/")
    public User createUser(@RequestBody User user) {
        // 处理新增用户请求
        return userService.createUser(user);
    }
}
  1. @PathVariable

@PathVariable 注解主要用于获取 URL 中的参数值,例如:

@RestController
@RequestMapping("/api/user")
public class UserController {
    @GetMapping("/{userId}")
    public User getUserById(@PathVariable Long userId) {
        // 查询指定 ID 的用户
        return userService.getUserById(userId);
    }
}
  1. @RequestParam

@RequestParam 注解主要用于获取 URL 中的查询参数值,例如:

@RestController
@RequestMapping("/api/user")
public class UserController {
    @GetMapping("/")
    public List<User> listUsers(
            @RequestParam(value = "name", required = false) String name,
            @RequestParam(value = "age", required = false) Integer age) {
        // 查询满足条件的用户列表
        return userService.listUsers(name, age);
    }
}
  1. @RequestHeader

@RequestHeader 注解主要用于获取 HTTP 请求头参数。例如:

@RestController
@RequestMapping("/api/user")
public class UserController {
    @GetMapping("/")
    public List<User> listUsers(
            @RequestHeader(value = "User-Agent", required = false) String userAgent) {
        // 查询满足条件的用户列表
        logger.info("User-Agent: {}", userAgent);
        return userService.listUsers();
    }
}

以上是 Controller 层中常用的注解和演示代码,这些注解都提供了非常便利的方式来接收和处理 HTTP 请求,并且能够很好地与 Spring Boot 的其他功能(例如异常处理、数据校验等)融合使用。

这样,我们就彻底完成了一整套的流程,先启动试试,看能不能跑起来

5.ApiPost测试

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

这里给大家一点小建议,接口写好之后不要直接去套前端,有的朋友喜欢全栈开发,就直接上手套,最好使用api工具进行测试,有时候跨域乱七八糟的问题会导致失败,而我们却不知道,这是一个小建议!在controller层中,我们就已经使用@CrossOrigin注释解决了跨域问题,所以前端不需要处理!!!

6.编写后端登录逻辑

后端代码

mapper.xml

<!--    根据用户名和密码进行登录校验-->
    <select id="userLoginJuage" resultMap="UserResultVO" parameterType="com.xiaohui.system.entity.User" >
        select *
        from user_table
        where user_name = #{userName}
          and user_pwd = #{userPwd}
    </select>

mapper


    /**
     * @param user 用户
     * @return {@link User }
     * @Description 用户登录鉴权
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    User userLoginJuage(User user);

service

    /**
     * @param user 用户
     * @return {@link User }
     * @Description 用户登录鉴权
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    User userLoginJuage(User user);

serviceImpl

   /**
     * @param user 用户
     * @return {@link User }
     * @Description 用户登录鉴权
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    @Override
    public User userLoginJuage(User user) {
        return userMapper.userLoginJuage(user);
    }

controller

   /**
     * @param user 用户
     * @return {@link AjaxResult }
     * @Description 用户登录鉴权
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    @GetMapping("/login")
    private AjaxResult userLoginJuage(@RequestBody User user) {
        //获取登录信息
        User userLoginInfo = userService.userLoginJuage(user);
        //判断用户是否存在
        if (userLoginInfo != null) {
            return AjaxResult.success("登录成功", userLoginInfo);
        }
        return AjaxResult.error("用户不存在");
    }

接口测试

成功示例

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

失败示例

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

7.编写前端登录逻辑

前端代码

前端我会使用到vue里面的一些组件,在这里给大家提前给出,就是简单下载配置,不费事

安装element-ui

npm install element-ui

安装router

您可以使用以下命令安装 Vue Router:

npm install vue-router -S

其中,-S--save 表示将 Vue Router 作为项目的依赖保存在项目中。

安装axios

您可以使用以下命令安装 Axios:

npm install axios -S

其中,-S--save 表示将 Axios 作为项目的依赖保存在项目中。

上面这些安装好了,咋们就开始干活,首先在src下面新建一个文件夹,名为router,里面新建一个index.js,内容如下

import VueRouter from "vue-router";
import loginPage from "@/view/login/loginPage.vue"
import helloWorld from "@/components/HelloWorld.vue";

export default new VueRouter({
    mode: 'history',
    routes: [
        {
            path: '/',
            redirect: '/login' // 将默认路由重定向到登录页
        },
        {
            name: 'login',
            path: '/login',
            component: loginPage

        },
        {
            name: 'helloWorld',
            path: '/helloWorld',
            component: helloWorld

        },

    ]
})

以上代码是一个基本的 Vue Router 配置文件,通过导出实例化出来的 VueRouter 对象,在应用中启用路由功能。以下是对每个部分的解释:

  • import VueRouter from "vue-router";:导入 Vue Router 模块。
  • import loginPage from "@/view/login/loginPage.vue":导入登录页组件,使用了 vue-cli 的 alias配置,这种写法是为了方便通用路径导入。(如果没有使用别名配置,可以写成 import loginPage from "../view/login/loginPage.vue"
  • export default new VueRouter({ ... }):使用 ES6 的模块化语法,导出实例化后的 VueRouter 对象。
  • mode:'history':设置路由模式为 HTML5 history 模式,URL 中不再包含 # 号。
  • routes:[ { ... }]:定义路由信息,数组中每一项表示一个路由,包括 name、path 和 component 属性。例如以上代码中定义了一个路由对象,当 URL 访问 /login 路径时,会渲染 Login 页面组件。
  • name:'loginPage':给路由规则起一个名称,方便在程序中进行编程式跳转。
  • path:'/login':对应的访问路径。
  • component:loginPage:访问该路由时要加载的组件。

这是一个最简单的路由示例,当然您可以根据项目需求配置更多路由规则。

然后我们在src下面的main.js文件一次性引入如下组件

import Vue from 'vue';
import App from './App.vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import VueRouter from 'vue-router';
import router from './router'

Vue.use(ElementUI);
Vue.use(VueRouter);
new Vue({
    render: h => h(App),
    router: router
}).$mount('#app')

以上代码是一个 Vue 应用的入口文件 main.js,其中做了以下事情:

  • import Vue from 'vue';:导入 Vue 模块。
  • import App from './App.vue';:导入根组件 App.vue。
  • import ElementUI from 'element-ui';:按需导入 Element UI 库。
  • import 'element-ui/lib/theme-chalk/index.css';:引入 Element UI 的默认样式文件。
  • import VueRouter from 'vue-router';:导入 Vue Router 模块。
  • import router from './router':导入路由配置对象。
  • Vue.use(ElementUI);:全局注册 Element UI 组件。
  • Vue.use(VueRouter);:全局注册路由功能。
  • new Vue({ ... }).$mount('#app'):创建一个 Vue 实例并挂载到 DOM 元素上。

创建的 Vue 实例挂载的选项包括以下内容:

  • render: h => h(App),:使用根组件 App.vue 渲染 Vue 应用。
  • router: router:启用导入的 Vue Router 配置。这里可以看到内部启用了传统的 JavaScript 对象命名简写方式,即 router 等效于 router: router
  • .$mount('#app'):将实例挂载到指定的 DOM 元素上,这里指的是 id 为 app 的 div 容器,与 HTML 页面中定义的 <div id="app"></div> 对应。

通过以上配置,我们完成了对 Vue、Element UI 和 Vue Router 库的引入,并配置了 Vue 应用的基本结构。

下面,需要改动的代码比较多,大家先就不要急着运行代码,跟着我稍微整理一下代码,代码目录结构如下

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

首先,我们需要把登录界面的代码完善

<template>
    <div class="container">
        <form class="form" @submit.prevent="login">
            <div class="form-group">

                <span class="label"> 账号</span>
                <el-input placeholder="请输入账号" v-model="form.userName" class="input"></el-input>
            </div>
            <div class="form-group">
                <span class="label">密码</span>
                <el-input placeholder="请输入密码" v-model="form.userPwd" class="input" show-password></el-input>
            </div>


            <el-button type="primary" @click="toLogin" class="loginBt" plain>登录</el-button>
            <el-button type="danger" @click="toRestData" class="loginBt">重置</el-button>
        </form>
    </div>
</template>

<script>
import {userLogin} from "@/api/login/userLogin";

export default {
    data() {
        return {
            form: {
                userName: '',
                userPwd: ''
            },
            queryParams: {
                pageNum: 1,
                pageSize: 10,
                userName: "",
                userPwd: ""
            },
        }
    },
    methods: {
        toLogin() {
            userLogin(this.form).then(response => {
                    console.log(response)
                    if (response.code !== 200) {
                        this.form = ""
                        this.$message.error(response.msg);
                    } else {
                        this.$message({message:response.msg , type: 'success'});
                        this.$router.push({path: "/helloWorld"});
                    }

                }
            );
        },
        toRestData() {
            this.form = ""

        }
    }
}
</script>

<style>

.container {
    background: url("@/assets/images/background.jpg") no-repeat center center fixed;
    -webkit-background-size: cover;
    -moz-background-size: cover;
    -o-background-size: cover;
    background-size: cover;
    display: flex;
    flex-direction: column;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background-color: #f2f2f2;
    padding-top: 0px;
    top: 0;
    position: fixed;
    height: 100vh;
    width: 100%;
}

.form {
    /*background-color: #fff;*/
    padding: 60px;
    border-radius: 10px;
    background-color: rgb(222 241 255 / 50%);
    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
    max-width: 400px;
    width: 50%;
    height: 28%;
    margin-left: 50%;
}

.form label,
.form input[type="text"],
.form input[type="password"],
.form button[type="submit"] {
    display: block;
    width: 100%;
    height: 45px;
    box-shadow: none;
    margin-bottom: 0px;
}

.form button[type="submit"] {
    /*background-color: #0070f3;*/
    color: #fff;
    padding: 10px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

.form-group {
    display: flex;
    align-items: center; /* 让内容在行中垂直居中 */
    margin: 20px;
}

.label {
    font-size: large;
    margin-right: 10px;
}

.input {
    flex: 1; /* 设置flex-grow为1,让输入框占据多余空间 */
    padding: 5px;
    border-radius: 5px;
    align-items: center;
}

.loginBt {
    width: 80px;
    height: 40px;
  margin-left: 30%;
}
</style>

这里的代码不算复杂,就是做了一个和后端的互动,看密码是否正确,然后就只判断,正确就跳转欢迎页

登录页api接口

import { get, post, put, del } from '@/utils/request'

//用户登录鉴权
export function userLogin(data) {
    return post('user/login', data)
}

然后大家就会看到,这里使用了一个封装的request工具类,这里给出

import axios from 'axios'

const service = axios.create({
    baseURL: "http://localhost:8080",
    timeout: 5000 // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
    config => {
// 在请求发送之前可以做一些处理,如请求头携带token等
        const token = window.localStorage.getItem('Authorization')
        if (token) {
            // 将 Authorization 请求头信息组装为 'Bearer token' 的格式
            config.headers.common['Authorization'] = `Bearer ${token}`
        }

        return config
    },
    error => {
        console.log(error) // for debug
        Promise.reject(error)
    }
)

// response拦截器
service.interceptors.response.use(
    response => {
        const res = response.data
        return res
    },
    error => {
        console.log('err' + error) // for debug
        return Promise.reject(error)
    }
)

export function get(url, params) {
    return service({
        url: url,
        method: 'get',
        params
    })
}

export function post(url, data) {
    return service({
        url: url,
        method: 'post',
        data
    })
}

export function put(url, data) {
    return service({
        url: url,
        method: 'put',
        data
    })
}

export function del(url) {
    return service({
        url: url,
        method: 'delete'
    })
}

这段代码是一个基于 axios 库封装的网络请求工具,可以实现发送 GET、POST、PUT、DELETE 等常见 HTTP 请求,并带有请求拦截器、响应拦截器等功能。

详细解读如下:

  • 首先,通过 axios.create() 创建了一个名为 service 的 Axios 实例,并配置了一些默认选项,如请求的 baseURL 和 timeout 等。

  • 接下来定义了两个拦截器:

-在请求发起之前会进行请求拦截,可以进行一些处理,如添加请求头等。如果需要进行错误处理,也可以在该处抛出异常中止请求。

-在获得响应之后会进行响应拦截,用于统一处理返回数据。在该拦截器中将响应数据解构到 res 对象中,并将其返回给请求处。

  • 最后通过导出四个函数:get、post、put 和 del,分别对应 GET、POST、PUT、DELETE 四种请求方法。这些函数通过调用 service() 方法发起请求,函数的参数即为 Axios 请求配置中的 urlmethoddata/params 等选项。

总的来说,这是一个可复用的网络请求工具,可在 Vue、React 或 Node.js 等项目中使用。

欢迎首页代码HelloWorld.vue

<template>
    <div id="home"></div>
</template>

<script>
export default {
    name: 'homePage'
}
</script>

<style scoped>

#home {
    width: 100%;
    min-height: 100vh;
    background: url("~@/assets/images/index.png") center center no-repeat;
    background-size: 100% 100%;

}
</style>

主入口App.vue

<template>
    <div id="home"></div>
</template>

<script>
export default {
    name: 'homePage'
}
</script>

<style scoped>

#home {
    width: 100%;
    min-height: 100vh;
    background: url("~@/assets/images/index.png") center center no-repeat;
    background-size: 100% 100%;

}
</style>

运行代码

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

输入账户和密码正确后将会跳转到helloworld

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

在这里,我们再把http状态码给大家列出来,大家注意,遇到问题及时速查

HTTP 状态码用于指示客户端向服务器提交的 HTTP 请求是否成功。常见的状态码包括以下 5 类:

  1. 1xx:信息性状态码,表示服务器已接受请求,正在等待更多数据。

  2. 2xx:成功状态码,表示服务器已成功处理请求并发送响应。

  3. 3xx:重定向状态码,表示需要客户端进一步操作才能完成请求。

  4. 4xx:客户端错误状态码,表示请求有错误或者不可用。

  5. 5xx:服务器错误状态码,表示服务器处理请求时发生错误。

以下是经常遇到的几种 HTTP 状态码及其解决方法:

  1. 200 OK:请求成功,服务器正常返回数据。

  2. 201 Created:请求成功,并创建了新资源。

  3. 204 No Content:请求成功,但无响应内容。

  4. 400 Bad Request:客户端发送的请求有异常,如参数格式错误、无权限访问等。解决方法:检查请求参数是否正确。

  5. 401 Unauthorized:未授权或登录失效。解决方法:用户重新登录或确认 token 是否过期。

  6. 403 Forbidden:禁止访问。解决方法:检查用户的权限是否满足访问 API 的要求。

  7. 404 Not Found:资源未找到。解决方法:检查请求路径是否正确。

  8. 405 Method Not Allowed:请求方式不支持。解决方法:检查请求方式是否与 API 兼容。

  9. 500 Internal Server Error:服务器内部出错。解决方法:查看服务器日志或联系管理员。

  10. 503 Service Unavailable:服务不可用。解决方法:检查服务是否正常启动,或者与虚拟化环境相关的负载是否太高。

对于其他的 HTTP 状态码,也需要根据具体情况进行相应的处理和解决,以保证应用程序的正常运行。

8.最复杂的token校验

解决思路

实现 SpringBoot+Vue+Jwt 的 token 认证需要以下几个步骤:

  1. 首先需要在后端(Spring Boot)中编写接口,用于用户登录、注册等操作。在处理用户登录时,服务器需要验证请求体中传递的用户账号密码信息,并根据其是否匹配从数据库中获取该用户信息。此外,在明确用户有效身份,且通过验证后,需要生成 JWT,并将其返回给前端。

  2. 在前端(Vue)中使用 axios(或其他 Http 请求库)对后端服务器进行 HTTP 调用,并将用户账号和密码以 POST 方法发送到服务器上。

  3. 服务器端成功验证用户身份后,将会颁发一个由 Jwt 签名的 Token,并将 Token 挂载在响应 Body 中。前端在接收到响应后,可以将 Token 存储在 localStorage 中。

  4. 在后续的 Http 请求中,前端将携带 JWT 发送至服务端,后端接收 JWT 后验证签名和有效期,并根据 JWT 中所携带的用户信息判断用户是否有权限进行请求操作。如果校验失败,则返回 401 状态码,否则正常响应请求。

  5. 鉴于存在 CSRF 攻击的可能,进一步加强机制 withCredentials 设置为 true,使得 Vue 对 fetch/webSocket 均携带 cookie/session,以便于 SpringBoot 进行身份认证(只设置头 Authorization 不足以防止 csrf 攻击)

在 Spring Boot 中可以使用 Jwt 和 Spring Security 集成,实现统一认证和鉴权,同时可以配置登录拦截器只放行匿名访问(如注册、登录)API,并对其他 API 进行身份认证和访问控制。而在 Vue 中可以使用 axios 拦截器,在请求发送之前对 token 进行添加等处理。如果要进一步优化,可以结合 Vuex 和 localStorage 等方式来管理 JWT,以增加安全性和代码的可读性。

后端代码

引入jwt依赖

Java-JWT 是一个在 Java 平台上生成和验证 JSON Web Token(JWT)的库,符合 RFC 7519 标准。JWT 是一种用于跨域认证的安全协议,能够对信息进行编码、加密和验证签名等操作。使用 JWT 能够避免传统的 cookie 机制存在的跨域访问问题,同时也能够保护敏感数据的安全性。

Java-JWT 几乎支持所有主流算法的加密和解密,并通过底层的 Java Cryptography API 对密钥进行管理,同时提供了易于使用的构建器模式,可以轻松地创建和解析 JWT。

下面是一些 Java-JWT 中常用的类和方法:

  1. JWT

这是核心类,提供了创建和解析 JWT 的主要功能,包括设置 Header 和 Payload 等基本信息。

  1. JwtBuilder

JwtBuilder 类可以帮助我们构建一个标准的 JSON Web Token,其中包含 Claim 的各项内容,例如过期时间、签发者、主题 ID 等等。一般情况下,我们将 Combine 这些不同的 Claims 填充到一个 Builder 对象中,最后调用 build() 方法获得一个 Json Web Token。

  1. Jwts

Jwts 类提供了针对 JSON Web Tokens 格式和实现的默认规则,包括 SignatureAlgorithm 枚举类中提供的算法以及 DefaultJwtParser 的解析规则(例如,RSA、HMAC、SHA-256 等)。

  1. Claims

Claims 类表示 JWT 中包含的声明,可以通过字符串形式的 key-value 对来体现。它们可以是 JSON 格式的 Key,也可以是自定义格式。

  1. JwtParser

JwtParser 类提供了解析一个给定 JWT 的方法,并获取其内部所包含的一些 Claims,例如过期时间和签发者等。

  1. SignatureAlgorithm

SignatureAlgorithm 枚举列出了所有支持进行 JSON Web Token 签名/验证的标准算法,包括 HMAC 和 RSA 签名等常见的加密方式。

因此,Java-JWT 是一款使用简单而强大的 Java 库,适用于跨越认证领域。它可以帮助开发者轻松生成和验证 JSON Web Token,并可通过多种算法和编码保护数据的安全性和完整性。

   <!--        java-jwt依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <!--        阿里巴巴fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>

再pom.xml中加入此依赖,记得刷新Maven

生成token的工具类
package com.xiaohui.system.Utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.xiaohui.system.entity.User;

import java.util.Date;

/**
 * @Description JWT工具类
 * @Author IT小辉同学
 * @Date 2023/05/18
 */
public class MyTokenUtil {
    private static final long EXPIRE_TIME = 10 * 60 * 60 * 1000;
    /**
     * 密钥盐
     */
    private static final String TOKEN_SECRET = "mytoken";

    /**
     * @param user
     * @return {@link String }
     * @Description 签名生成
     * @Author IT小辉同学
     * @Date 2023/05/18
     */
    public static String sign(User user) {
        String token = null;
        try {
            Date expiresAt = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            token = JWT.create().withIssuer("auth0").withClaim("userName", user.getUserName()).withExpiresAt(expiresAt)
                    // 使用了HMAC256加密算法。
                    .sign(Algorithm.HMAC256(TOKEN_SECRET));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return token;
    }

    /**
     * @param token
     * @return boolean
     * @Description 签名验证
     * @Author IT小辉同学
     * @Date 2023/05/18
     */
    public static boolean verify(String token) {
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

这段代码是一个 JWT 工具类,实现了生成 JWT 和验证 JWT 的功能。下面是逐行解读:

  1. import com.auth0.jwt.JWT;

引入 JWT 类,可以通过该类创建一个 JWT 实例。

  1. import com.auth0.jwt.JWTVerifier;

引入 JWTVerifier 类,用于验证 JWT 是否合法。

  1. import com.auth0.jwt.algorithms.Algorithm;

引入 Algorithm 类,用于指定生成或验证 JWT 时使用的算法。

  1. import com.auth0.jwt.interfaces.DecodedJWT;

引入 DecodedJWT 类,可用于获取 JWT 中包含的声明和 payload。

  1. import com.xiaohui.system.entity.User;

引入 User 类,通常情况下,我们会将一些敏感信息打包成 User 对象放入 JWT 中,以保证数据的安全性。

  1. private static final long EXPIRE_TIME = 10 * 60 * 60 * 1000;

指定 JWT 的过期时间,单位为毫秒。

  1. private static final String TOKEN_SECRET = "mytoken";

指定用于生成 JWT 签名的秘钥盐,建议将其存储在服务器中。

  1. public static String sign(User user) {...}

生成 JWT 签名的方法。首先设置 JWT 过期时间、声明和 payload 等必要信息,并使用算法 HMAC256 对其进行签名并返回 token 字符串。

  1. public static boolean verify(String token) {...}

验证 JWT 签名是否合法的方法。通过传入 token 字符串,调用 JWT 相关类(如 JWTrequire 和 Algorithm)及其方法,返回一个Boolean值,表示该 token 是否合法。

  1. DecodedJWT jwt = verifier.verify(token);

如果传入的 token 字符串合法,则将其解码成 DecodedJWT 对象,并从该对象中获取 JWT 中包含的声明和 payload。

token认证
package com.xiaohui.system.config;

import com.alibaba.fastjson.JSONObject;
import com.xiaohui.system.Utils.MyTokenUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Description token认证拦截器
 * @Author IT小辉同学
 * @Date 2023/05/18
 */
@Component
public class MyTokenInterceptor implements HandlerInterceptor {
    /**
     * @param request  请求
     * @param response 响应
     * @param handler  处理程序
     * @return boolean
     * @Description HandlerInterceptor 中的方法,重写它来自定义拦截逻辑
     * @Author IT小辉同学
     * @Date 2023/05/18
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /*
         *针对跨域请求种类 OPTIONS,设置成 "SC_OK" 以允许请求方法
         * 下一步操作应当检查token是否存在。
         */
        if (request.getMethod().equals("OPTIONS")) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }
        response.setCharacterEncoding("utf-8");
        //从请求头中获取到前端 Vue 发起的 token
        String token = request.getHeader("Authorization");
        if (token != null) {
            boolean result = MyTokenUtil.verify(token);
            if (result) {
                System.out.println("通过拦截器");
                return true;
            }
        }
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            //使用 fastjson 创建 JSON 对象
            JSONObject json = new JSONObject();
            json.put("msg", "令牌为空");
            json.put("code", "401");
            //返回易于 Vue 接受的信息,并说明发生的错误(如果有)
            response.getWriter().append(json.toJSONString());
            System.out.println("认证失败,未通过拦截器!!!");

        } catch (Exception e) {
            e.printStackTrace();
            response.sendError(500);
            return false;
        }
        return false;
    }
}

这段代码是一个拦截器,用于过滤请求并处理 JWT 鉴权问题。下面是逐行解读:

  1. import com.alibaba.fastjson.JSONObject;

引入 fastjson 库中的 JSONObject 类,用于构造返回结果。

  1. import com.xiaohui.system.Utils.MyTokenUtil;

引入自定义的 JWT 工具类。

  1. import org.springframework.stereotype.Component;

将该类声明为 Spring 组件,以便在代码其他位置使用。

  1. import org.springframework.web.servlet.HandlerInterceptor;

实现 HandlerInterceptor 接口,成为 Spring MVC 的拦截器。

  1. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ... }

HandlerInterceptor 中的方法,重写它来自定义拦截逻辑。

  1. if (request.getMethod().equals("OPTIONS")) {...}

针对跨域请求种类 OPTIONS,设置成 “SC_OK” 以允许请求方法。下一步操作应当检查token是否存在。

  1. String token = request.getHeader("token");

从请求头中获取到前端 Vue 发起的 token。

  1. boolean result = MyTokenUtil.verify(token);

使用前面定义的 JWT 工具类进行鉴权,得到验证结果。

  1. JSONObject json = new JSONObject();

使用 fastjson 创建 JSON 对象。

  1. response.getWriter().append(json.toJSONString());

返回易于 Vue 接受的信息,并说明发生的错误(如果有)。

  1. response.setContentType("application/json; charset=utf-8");

指定返回的内容类型,并设置编码格式为 utf-8。

最终说明,该方法用于 JWT 的校验操作,通过此拦截器在request将token提取出来,然后使用JWT工具类进行校验。如果校验失败,则响应401的状态,然后自定义错误信息返回给前端Vue。

token认证过滤器
package com.xiaohui.system.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * @Description 跨域
 * @Author IT小辉同学
 * @Date 2023/05/18
 */
@Configuration
public class CrosConfig implements WebMvcConfigurer {
    private ExecutorService executorService = null;


    /**
     * @param registry 注册表
     * @Description 通过 registry 来设置允许跨域的路由、
     * 请求方式、允许顺序等消息头,并在 Spring 容器中创建注册器
     * @Author IT小辉同学
     * @Date 2023/05/18
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置允许跨域的路由
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }

    private MyTokenInterceptor tokenInterceptor;

    //构造方法
    public CrosConfig(MyTokenInterceptor tokenInterceptor) {
        this.tokenInterceptor = tokenInterceptor;
    }

    /**
     * @param configurer 配置
     * @Description 配置异步支持
     * @Author IT小辉同学
     * @Date 2023/05/18
     */
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        executorService = new ThreadPoolExecutor(2,
                2,
                100,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        configurer.setTaskExecutor(new ConcurrentTaskExecutor(executorService));
        configurer.setDefaultTimeout(30000);
    }

    /**
     * @param registry 注册表
     * @Description 添加拦截器
     * @Author IT小辉同学
     * @Date 2023/05/18
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        List<String> excludePath = new ArrayList<>();
        //排除拦截,除了注册登录(此时还没token),其他都拦截
        excludePath.add("/user/register");
        excludePath.add("/user/login");

        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludePath);
        WebMvcConfigurer.super.addInterceptors(registry);
    }

}

这段代码是一个 Spring MVC 的配置类,主要用于设置异步支持、跨域访问和拦截器的操作。下面是逐行解读:

  1. import org.springframework.context.annotation.Configuration;

将该类声明为 Spring 配置类。

  1. import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;

引入对线程池的支持。

  1. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

WebMvcConfigurer 是 Spring MVC 的自定义配置接口,需要实现该接口才能自定义 Spring MVC 相关组件。

  1. @Configuration public class CrosConfig implements WebMvcConfigurer { ... }

声明一个 CrosConfig 类,使用 WebMvcConfigurer 接口提供的方法来自定义 Spring MVC 相关组件。

  1. private ExecutorService executorService = null;

使用线程池来处理异步支持所需的线程。

  1. @Override public void addCorsMappings(CorsRegistry registry) { ... }

通过 registry 来设置允许跨域的路由、请求方式、允许顺序等消息头,并在 Spring 容器中创建注册器。

  1. private MyTokenInterceptor tokenInterceptor;

注入 MyTokenInterceptor 实例。

  1. public CrosConfig(MyTokenInterceptor tokenInterceptor) { this.tokenInterceptor = tokenInterceptor; }

构造函数,参数为 MyTokenInterceptor 对象,以供引入。

  1. @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { ... }

异步支持配置,使用线程池来创建定制异步任务。

  1. registry.addInterceptor(tokenInterceptor) .addPathPatterns("/**") .excludePathPatterns(excludePath);

添加拦截器,除注册和登录外,其他请求都需要验证 Token 来实现安全过滤。

最终说明,该代码主要用于配置 Spring MVC 的一些属性,包括跨域访问、拦截器、线程池等。具体来说:允许所有的路由进行跨域访问;使用线程池来支持异步请求;拦截所有除了注册和登录以外的 HTTP 请求,并通过 tokenInterceptor 对其进行进一步处理。

做到这里,我们把原来写的controller修改一下

package com.xiaohui.system.controller;

import com.xiaohui.system.Utils.AjaxResult;
import com.xiaohui.system.Utils.MyTokenUtil;
import com.xiaohui.system.entity.User;
import com.xiaohui.system.service.Impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;

/**
 * @Description 用户控制器
 * @Author IT小辉同学
 * @Date 2023/05/17
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserServiceImpl userService;

    /**
     * @param user 用户
     * @return {@link AjaxResult }
     * @Description 获取所有用户信息
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    @GetMapping("/list")
    private AjaxResult selectAllUserInfo() {
        User user = new User();
        return AjaxResult.success(userService.selectAllUserInfo(user));
    }

    /**
     * @param user 用户
     * @return {@link AjaxResult }
     * @Description 用户登录鉴权
     * @Author IT小辉同学
     * @Date 2023/05/17
     */
    @PostMapping("/login")
    private AjaxResult userLoginJuage(@RequestBody User user) {
        //获取登录信息
        User userLoginInfo = userService.userLoginJuage(user);
        boolean flag = false;
        //判断用户是否存在
        if (userLoginInfo != null) {
            //构造token
            String token = MyTokenUtil.sign(user);
            HashMap tokenMap=new HashMap();
            tokenMap.put("token",token);
            return AjaxResult.success("登录成功", tokenMap);
        }
        return AjaxResult.error("用户不存在或者密码错误");
    }
}

前端代码

重写router/index.js 路由守卫
import Vue from 'vue'
import VueRouter from "vue-router";
import loginPage from "@/view/login/loginPage.vue"
import helloWorld from "@/components/HelloWorld.vue";
Vue.use(VueRouter)
const router = new VueRouter({
    mode: 'history',
    routes: [
        {
            path: '/',
            redirect: '/login' // 将默认路由重定向到登录页
        },
        {
            name: 'login',
            path: '/login',
            component: loginPage

        },
        {
            name: 'helloWorld',
            path: '/helloWorld',
            component: helloWorld

        },

    ]
})
//全局前置守卫
router.beforeEach((to, from, next) => {
    // to and from are both route objects. must call `next`.
    if (to.path === '/register' || to.path === '/login' || to.path === '/') {
        next();//直接放行
    } else {
        const token =   window.localStorage.getItem('Authorization');
        if (token === null || token === '') {
            next('/login')
        } else {

            next()
        }
    }
})

export default router 
配置Vuex

老规矩,先安装

npm install vuex 
编写/router/store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const actions = {
    // 定义异步操作等不改变 state 的逻辑 ...
}

const mutations = {
    // 修改 token,将 token 放入 localStorage 中
    changeLogin(state, user) {
        state.Authorization = user.Authorization
          window.localStorage.setItem('Authorization', user.Authorization)
    },

    // 注销
    logout(state) {
        state.Authorization = null
        window.localStorage.removeItem('Authorization')
    }
}

const state = {
    // 存储 token
    Authorization: window.localStorage.getItem('Authorization') ? window.localStorage.getItem('Authorization') : '',
}

// 向外暴露
const store = new Vuex.Store({
    actions,
    mutations,
    state,
})

export default store

最后,对于登录界面的登录逻辑重写一下
 toLogin() {
            userLogin(this.form).then(response => {
                    console.log(response)
                    if (response.code !== 200) {
                        this.form = ""
                        this.$message.error(response.msg);
                    } else {
                      //获取服务器响应的token值。保存LocalStorage
                      window.localStorage.setItem("Authorization",response.data.token);
                      this.$message({message:response.msg , type: 'success'});
                        this.$router.push({path: "/helloWorld"});
                    }

                }
            );
        },

9.验证token鉴权

有token

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

没有token

鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)

10.已经写了两个晚上了,最后,再给大家补充一个密码加密

加密算法我们使用MD5加密

MD5(Message Digest Algorithm 5)是一种常见的哈希函数,用于将任意长度的消息数据计算得出一个128位的哈希值。这个哈希值通常表示为32位的16进制数字,经常被用于对密码、消息等敏感信息进行加密存储或传输。

先引入依赖

 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>

MD5工具类

package com.xiaohui.system.Utils;

import org.springframework.stereotype.Component;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import static com.alibaba.druid.util.Utils.md5;

@Component
public class MD5Utils {
    /**
     * 使用md5的算法进行加密
     */
    public static String toMD5(String plainText) {
        byte[] secretBytes = null;
        try {
            secretBytes = MessageDigest.getInstance("md5").digest(
                    plainText.getBytes());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("没有md5这个算法!");
        }
        String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字
        // 如果生成数字未满32位,需要前面补0
        for (int i = 0; i < 32 - md5code.length(); i++) {
            md5code = "0" + md5code;
        }
        return md5code;
    }

    /**
     * 可逆的的加密解密方法;两次是解密,一次是加密
     *
     * @param inStr
     * @return
     */
    public static String convertMD5(String inStr) {

        char[] a = inStr.toCharArray();
        for (int i = 0; i < a.length; i++) {
            a[i] = (char) (a[i] ^ 't');
        }
        String s = new String(a);
        return s;

    }

/*
    public static void main(String[] args) {
        String s = md5("1234");
        System.out.println("MD5后:"+s);
        System.out.println("MD5后:"+s);
        System.out.println("MD5后再加密:"+convertMD5(s));
        System.out.println("MD5加密后解密:"+convertMD5(convertMD5(s)));
        String s2 = convertMD5("12345");
        System.out.println("可逆的加密解密方法之加密:"+s2);
        System.out.println("可逆的加密解密方法之解密:"+convertMD5(s2));
    }
*/
}

新增用户的时候,调用toMD5进行密码加密存储,对比的时候将输入的密码convertMD5进行加密与数据库进行对比即可

没想到,用了两天时间终于写完了,可以说是集众家之长,汇集了众多大佬的资源,得以完成!很感谢自己,也感谢大家,我们一起努力,相信梦想不被辜负,付出终将温柔回报!如有不足,大家见谅,私信讨论,我们一起进步!!!

代码在个人资源,可以主页免费下载!文章来源地址https://www.toymoban.com/news/detail-450827.html

128位的哈希值。这个哈希值通常表示为32位的16进制数字,经常被用于对密码、消息等敏感信息进行加密存储或传输。

先引入依赖

 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>

MD5工具类

package com.xiaohui.system.Utils;

import org.springframework.stereotype.Component;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import static com.alibaba.druid.util.Utils.md5;

@Component
public class MD5Utils {
    /**
     * 使用md5的算法进行加密
     */
    public static String toMD5(String plainText) {
        byte[] secretBytes = null;
        try {
            secretBytes = MessageDigest.getInstance("md5").digest(
                    plainText.getBytes());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("没有md5这个算法!");
        }
        String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字
        // 如果生成数字未满32位,需要前面补0
        for (int i = 0; i < 32 - md5code.length(); i++) {
            md5code = "0" + md5code;
        }
        return md5code;
    }

    /**
     * 可逆的的加密解密方法;两次是解密,一次是加密
     *
     * @param inStr
     * @return
     */
    public static String convertMD5(String inStr) {

        char[] a = inStr.toCharArray();
        for (int i = 0; i < a.length; i++) {
            a[i] = (char) (a[i] ^ 't');
        }
        String s = new String(a);
        return s;

    }

/*
    public static void main(String[] args) {
        String s = md5("1234");
        System.out.println("MD5后:"+s);
        System.out.println("MD5后:"+s);
        System.out.println("MD5后再加密:"+convertMD5(s));
        System.out.println("MD5加密后解密:"+convertMD5(convertMD5(s)));
        String s2 = convertMD5("12345");
        System.out.println("可逆的加密解密方法之加密:"+s2);
        System.out.println("可逆的加密解密方法之解密:"+convertMD5(s2));
    }
*/
}

新增用户的时候,调用toMD5进行密码加密存储,对比的时候将输入的密码convertMD5进行加密与数据库进行对比即可

没想到,用了两天时间终于写完了,可以说是集众家之长,汇集了众多大佬的资源,得以完成!很感谢自己,也感谢大家,我们一起努力,愿梦想不被辜负,付出终将温柔回报!如有不足,大家见谅,私信讨论,我们一起进步!!!

代码在个人资源,可以主页免费下载!

到了这里,关于鉴权管理系统(JWT技术架构)——SpringBoot2+Vue2(一定惊喜满满,万字长文)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringBoot小项目——简单的小区物业后台管理系统 & 认证鉴权 用户-角色模型 & AOP切面日志 & 全局异常【源码】

    基于SpringBoot的简单的小区物业后台管理系统,主要功能有报修的处理,楼宇信息和房屋信息的管理,业主信息的管理【核心】,以及数据统计分析模块Echarts绘图;此外采用用户-角色权限模型,结合自定义注解实现简单的权限管理功能,采用aop切面实现日志的存储,全局异常

    2024年02月06日
    浏览(52)
  • 【大学生体质】图书管理系统(Vue+SpringBoot2)-完整部署教程【课设OR毕设提供API接口文档、数据库文件、README.MD、部署视频】

    本项目拥有完整的API后台接口文档(文尾) 项目部署视频正在录制 如果项目对您有所帮助,可以Star⭐一下,受到鼓励的我会继续加油。 项目在线演示地址 项目前端地址 项目后端地址 项目部署视频 ☃️前端主要技术栈 技术 作用 版本 Vue 提供前端交互 2.6.14 Vue-Router 路由式编

    2024年01月18日
    浏览(54)
  • 新零售SaaS架构:客户管理系统的应用架构设计

    应用层定义了软件系统的应用功能,负责接收用户的请求,协调领域层能力来执行任务,并将结果返回给用户,功能模块包括: 客户管理:核心功能模块,负责收集和更新客户信息,包括个人资料、联系方式、消费习惯、会员卡、归属信息(比如销售或顾问)和备注。这个模

    2024年04月08日
    浏览(47)
  • 智慧图书管理系统架构设计与实现

    随着数字化时代的到来,智慧图书管理系统在图书馆和机构中扮演着重要的角色。一个优秀的图书管理系统不仅需要满足基本的借阅管理需求,还需要具备高效的性能、良好的扩展性和稳定的安全性。本文将讨论智慧图书管理系统的架构设计与实现,以满足现代图书管理的多

    2024年02月20日
    浏览(50)
  • 架构设计参考项目系列主题:新零售SaaS架构:客户管理系统架构设计

    什么是客户管理系统? 客户管理系统,也称为CRM(Customer Relationship Management),主要目标是建立、发展和维护好客户关系。 CRM系统围绕客户全生命周期的管理,吸引和留存客户,实现缩短销售周期、降低销售成本、增加销售收入的目的,从而提高企业的盈利能力和竞争力。

    2024年04月14日
    浏览(51)
  • 系统架构设计师-项目管理

    目录         一、盈亏平衡分析         二、进度管理                 1、WBS工作分解结构                  2、进度管理流程                  (1)活动定义                 (2)活动排序                 (3)活动资源估算:                 (4)活

    2024年02月16日
    浏览(43)
  • 基于SSM架构实现学生信息管理系统

    本项目是一个基于SSM(Spring+SpringMVC+MyBatis)框架搭建的学生信息管理系统,实现了对学生、用户等信息的增删改查功能,以及登录、分页等功能。本项目采用了三层架构,分为entity层、service层、dao层和controller层,使用了Maven进行项目管理,使用了MySQL作为数据库。 本项目主要

    2024年02月03日
    浏览(52)
  • 尚融宝13-后台管理系统前端架构梳理

    目录 一、程序入口 (一)入口页面 index.html (二) 入口js脚本:src/main.js (三)顶层组件:src/App.vue (四)路由:src/router/index.js  查看源代码 这正是srb-admin/public/index.html    我们进入积分等级列表,查看源代码会发现仍然是index.html中的代码  那么它是怎么实现页面的不同

    2023年04月11日
    浏览(40)
  • 学校图书馆管理系统的架构设计与实现

    随着大学生越来越多,学校图书馆的管理变得愈发复杂。为了更好地管理和服务于学生和教职工,学校需要建立一个高效的图书馆管理系统。本文将介绍学校图书馆管理系统的架构设计与实现。 1. 架构设计 学校图书馆管理系统的架构设计主要分为三个部分: 展示层:展示层

    2024年02月22日
    浏览(54)
  • 使用eclipse创建一个图书管理系统(1)-----搭建架构

    目录 思维导图: 图书管理系统的创建: 第一步:搭建框架-------使用者 第二步:搭建框架------被使用者 第三步:操作方法 第四步:main函数  前言: 昨天学了一下使用Java语言来写一个图书管理系统,于是今天写一篇博客作为一个小笔记巩固一下自己学到的知识!博主也是刚

    2024年02月02日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包