目录

  • 后端
  • 一、SpringBoot项目搭建
  • 二、添加基本工具类
  • 三、SpringBoot项目日志优化
  • 四、整合 MyBatis
  • 五、整合 Mybatis-generator
  • 六、整合 PageHelper 分页插件
  • 七、封装请求参数和返回参数
  • 八、集成 Validation 做参数校验
  • 九、密码加密
  • 十、整合 JWT 实现登录权限校验
  • 十一、解决前后端交互相关问题
  • 前端
  • 一、Vue CLI 项目搭建
  • 二、多环境配置
  • 三、添加封装的工具类 tool.js(util 目录下)
  • 四、使用 Axios 拦截器打印前端日志
  • 五、密码加密传输(与后端密码加密配套)
  • 六、登录权限校验(JWT)


后端

一、SpringBoot项目搭建

1、新建SpringBoot项目,仅需选择如下依赖,后续开发过程中再按需添加。

基于vue和springboot系统架构图 springboot vue 框架_vue.js


2、构建项目目录,如下:

基于vue和springboot系统架构图 springboot vue 框架_vue.js_02

说明

  • java 目录下:
  • aspect 包:AOP相关类,如 LogAspect.java ;
  • config 包:配置类相关;
  • controller 包:控制(controller)层,SpringMVC 三层架构之一,对前端的请求进行响应和控制,调用 service 层;
  • domain 包:实体类;
  • exception 包:自定义异常相关类;
  • interceptor 包:拦截器相关类;
  • mapper 包:持久(dao)层,负责访问、操作数据库;
  • req 包:requestVo(封装的请求参数)相关类;
  • resp 包:responseVo(封装的返回参数)相关类;
  • service 包:业务(service)层,SpringMVC 三层架构之一,实现具体的业务逻辑,调用 dao 层,被 controller 层调用;
  • util 包:封装的工具类;
  • filter 包:过滤器相关类,按需添加;
  • job 包:定时任务相关类,调用 service 层实现具体逻辑,按需添加;
  • resources 目录下:
  • generator 目录:mybatis-generator相关配置;
  • mapper 目录:java 目录下的 mapper 包对应接口的具体实现;
  • static、template 目录:存放静态资源相关,在前后端分离项目中无用;
  • application.properties 文件:项目的配置文件,也可为 application.yml 文件;
  • logback-spring.xml 文件:logback 日志相关;
  • test 目录:
  • 单元测试相关

3、启动类添加包扫描注解

@ComponentScan("com.yangj0020") //默认只会扫描启动类所在包下的子包,当不移动启动类的位置时,可不添加该注解(但是建议添加)

二、添加基本工具类

1、CopyUtil 工具类

package com.yangj0020.template.util;

import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Yang Jie
 * @Date 2021/06/2021/6/8
 */
public class CopyUtil {

    /**
     * 单体复制
     */
    public static <T> T copy(Object source, Class<T> clazz) {
        if (source == null) {
            return null;
        }
        T obj = null;
        try {
            obj = clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        BeanUtils.copyProperties(source, obj);
        return obj;
    }

    /**
     * 列表复制
     */
    public static <T> List<T> copyList(List source, Class<T> clazz) {
        List<T> target = new ArrayList<>();
        if (!CollectionUtils.isEmpty(source)){
            for (Object c: source) {
                T obj = copy(c, clazz);
                target.add(obj);
            }
        }
        return target;
    }

}

2、SnowFlake(雪花算法) 工具类

package com.yangj0020.template.util;

/**
 * @Author Yang Jie
 * @Date 2021/6/9 20:58
 */
import org.springframework.stereotype.Component;

import java.text.ParseException;

/**
 * Twitter的分布式自增ID雪花算法
 **/
@Component
public class SnowFlake {

    /**
     * 起始的时间戳
     */
    private final static long START_STMP = 1609459200000L; // 2021-01-01 00:00:00

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId = 1;  //数据中心
    private long machineId = 1;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳

    public SnowFlake() {
    }

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) throws ParseException {
        // 时间戳
        // System.out.println(System.currentTimeMillis());
        // System.out.println(new Date().getTime());
        //
        // String dateTime = "2021-01-01 08:00:00";
        // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        // System.out.println(sdf.parse(dateTime).getTime());

        SnowFlake snowFlake = new SnowFlake(1, 1);

        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            System.out.println(snowFlake.nextId());
            System.out.println(System.currentTimeMillis() - start);
        }
    }
}

三、SpringBoot项目日志优化

1、编写 logback-spring.xml(2.3之前版本为logback.xml)文件 + 启动类修改:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 修改一下路径-->
    <property name="PATH" value="./log"></property>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>
<!--            <Pattern>%d{ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>-->
        </encoder>
    </appender>

    <appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${PATH}/trace.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <layout>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
        </layout>
    </appender>

    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${PATH}/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <layout>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
        </layout>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <root level="ERROR">
        <appender-ref ref="ERROR_FILE" />
    </root>

    <root level="TRACE">
        <appender-ref ref="TRACE_FILE" />
    </root>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>
package com.yangj0020.template;

import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.env.Environment;

@ComponentScan("com.yangj0020") //默认只会扫描启动类所在包下的子包,当不移动启动类的位置时,可不添加该注解(但是建议添加)
@SpringBootApplication
@MapperScan("com.yangj0020.template.mapper") //配置mapper类所在位置
public class TemplateApplication {

    private static final Logger LOG = LoggerFactory.getLogger(TemplateApplication.class);

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(TemplateApplication.class);
        Environment env = app.run(args).getEnvironment();
        LOG.info("启动成功!!!");
        LOG.info("地址:\thttp://127.0.0.1:{}", env.getProperty("server.port"));
    }

}

2、打印 sql 日志,在application.properties文件中添加如下内容:

# 打印所有的sql日志:sql,参数,结果
logging.level.com.yangj0020.template.mapper=trace

3、通过 AOP 打印请求参数和返回参数,导入依赖 + 编写 LogAspect 类:

<!-- 集成AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- JSON格式转换 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.70</version>
</dependency>
package com.yangj0020.template.aspect;

/**
 * @Author Yang Jie
 * @Date 2021/6/18 16:30
 */
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.yangj0020.template.util.SnowFlake;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
public class LogAspect {

    private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    @Resource
    private SnowFlake snowFlake;

    /** 定义一个切点 */
    @Pointcut("execution(public * com.yangj0020.*.controller..*Controller.*(..))")
    public void controllerPointcut() {}

    @Before("controllerPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {

        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();

        // 增加日志流水号
        MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));

        // 打印请求信息
        LOG.info("------------- 开始 -------------");
        LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
        LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
        LOG.info("远程地址: {}", request.getRemoteAddr());

        // 打印请求参数
        Object[] args = joinPoint.getArgs();
        // LOG.info("请求参数: {}", JSONObject.toJSONString(args));

        Object[] arguments  = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof ServletRequest
                    || args[i] instanceof ServletResponse
                    || args[i] instanceof MultipartFile) {
                continue;
            }
            arguments[i] = args[i];
        }
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
    }

    @Around("controllerPointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 排除字段,敏感字段或太长的字段不显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
        excludefilter.addExcludes(excludeProperties);
        LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
        LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
        return result;
    }

}

4、添加日志流水号(跟踪号),LogAspect 类中增加日志流水号 + logback-spring.xml 文件中添加 LOG_ID 字段(上文代码中均已实现)

四、整合 MyBatis

1、添加依赖 (pom.xml)

<!-- 集成mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!-- 集成mysql连接 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

2、 配置Datasource (application.properties)

# 增加数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/数据库名?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=用户名
spring.datasource.password=密码
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

五、整合 Mybatis-generator

1、集成插件 (pom.xml)

<!-- mybatis generator 自动生成代码插件 -->
<plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.4.0</version>
    <configuration>
        <configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
        <overwrite>true</overwrite>
        <verbose>true</verbose>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
    </dependencies>
</plugin>

2、编写 generator-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">

        <!-- 自动检查关键字,为关键字增加反引号 -->
        <property name="autoDelimitKeywords" value="true"/>
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!--覆盖生成XML文件-->
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
        <!-- 生成的实体类添加toString()方法 -->
        <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>

        <!-- 不生成注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/数据库名?serverTimezone=Asia/Shanghai"
                        userId="用户名"
                        password="密码">
        </jdbcConnection>

        <!-- domain类的位置 -->
        <javaModelGenerator targetProject="src\main\java"
                            targetPackage="com.yangj0020.wiki.domain"/>

        <!-- mapper xml的位置 -->
        <sqlMapGenerator targetProject="src\main\resources"
                         targetPackage="mapper"/>

        <!-- mapper类的位置 -->
        <javaClientGenerator targetProject="src\main\java"
                             targetPackage="com.yangj0020.wiki.mapper"
                             type="XMLMAPPER"/>
                             
        <!-- 数据库表和实体类对应关系 -->
        <!-- <table tableName="demo" domainObjectName="Demo"/> -->
        <!-- <table tableName="demo"> --> <!-- 效果同上句 -->

    </context>
</generatorConfiguration>

3、配置启动项

基于vue和springboot系统架构图 springboot vue 框架_jwt_03


基于vue和springboot系统架构图 springboot vue 框架_spring_04


基于vue和springboot系统架构图 springboot vue 框架_jwt_05

基于vue和springboot系统架构图 springboot vue 框架_mybatis_06


基于vue和springboot系统架构图 springboot vue 框架_jwt_07

注意:mybatis-generator 生成的四个文件不宜修改,后续若要自己编写复杂 sql,可增加对应的 XXXMapperCust 接口和 XXXMapperCust.xml 文件

4、配置mapper类所在位置,在启动类上添加注解

@MapperScan("com.yangj0020.template.mapper") //配置mapper类所在位置

5、配置mybatis所有Mapper.xml所在的路径,在 application.xml 添加:

mybatis.mapper-locations=classpath:/mapper/**/*.xml

六、整合 PageHelper 分页插件

1、导入依赖

<!-- pagehelper 插件-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.13</version>
</dependency>

2、使用分页插件,在需要分页查询的代码前加上:

//分页
 PageHelper.startPage(req.getPage(), req.getSize());

 //需要分页查询的代码
 List<User> userList = userMapper.selectByExample(userExample);

 //获取相应的分页信息,如查询到的总记录条数等
 PageInfo<User> pageInfo = new PageInfo<>(userList);

七、封装请求参数和返回参数

1、编写 PageReq 类

package com.yangj0020.template.req;

public class PageReq {
  
    private int page; //页号
    
    private int size; //页面大小

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    @Override
    public String toString() {
        return "PageReq{" +
                "page=" + page +
                ", size=" + size +
                '}';
    }
}

2、需要分页查询的请求类都应该继承 PageReq 类,如:

package com.yangj0020.template.req;

/**
 * @Author Yang Jie
 * @Date 2021/6/17 16:18
 * @Descrition 示例代码
 */
public class UserQueryReq extends PageReq {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "UserQueryReq{" +
                "name='" + name + '\'' +
                "} " + super.toString();
    }
}

3、编写通用返回类 CommonResp,后端统一向前端返回该类对象

package com.yangj0020.template.resp;

/**
 * 通用返回类
 * @param <T>
 */
public class CommonResp<T> {

    /**
     * 业务上的成功或失败
     */
    private boolean success = true;

    /**
     * 返回信息
     */
    private String message;

    /**
     * 返回泛型数据,自定义类型
     */
    private T content;

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "CommonResp{" +
                "success=" + success +
                ", message='" + message + '\'' +
                ", content=" + content +
                '}';
    }
}

4、编写 PageResp 类,分页查询返回的通用类中,content 属性为该类对象

package com.yangj0020.template.resp;

import java.util.List;

public class PageResp<T> {
    private long total;

    private List<T> list;

    public long getTotal() {
        return total;
    }

    public void setTotal(long total) {
        this.total = total;
    }

    public List<T> getList() {
        return list;
    }

    public void setList(List<T> list) {
        this.list = list;
    }

    @Override
    public String toString() {
        return "PageResp{" +
                "total=" + total +
                ", list=" + list +
                '}';
    }
}

5、按需编写相应的 XXXReq 类和 XXXResp 类

八、集成 Validation 做参数校验

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2、为需要校验的属性加上相应注解,如:

@NotNull(message = "【每页条数】不能为空")
@Max(value = 1000, message = "【每页条数】不能超过1000")
private int size;

3、在controller对应接口上开启校验,具体为在参数前添加 @Valid 注解,如:

@GetMapping("/list")
    public CommonResp list(@Valid UserQueryReq req) {
        CommonResp<PageResp<UserQueryResp>> resp = new CommonResp<>();
        PageResp<UserQueryResp> list = userService.list(req);
        resp.setContent(list);
        return resp;
    }

4、在 controller 包下添加 ControllerExceptionHandler 类,统一处理校验异常

import com.yangj0020.wiki.resp.CommonResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;


@ControllerAdvice
public class ControllerExceptionHandler {

    private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);

    /**
     * 校验异常统一处理
     * @param e
     * @return
     */
    @ExceptionHandler(value = BindException.class)
    @ResponseBody
    public CommonResp validExceptionHandler(BindException e) {
        CommonResp commonResp = new CommonResp();
        LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        commonResp.setSuccess(false);
        commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return commonResp;
    }
}

九、密码加密

在实现更新、登录等接口时进行 md5 加密,如下:

@PostMapping("/save")
public CommonResp save(@Valid @RequestBody UserSaveReq req) {
    req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes())); // md5加密
    CommonResp resp = new CommonResp<>();
    userService.save(req);
    return resp;
}

注意:前端发送对应的请求前,也必须对密码进行 md5 加密!!!

十、整合 JWT 实现登录权限校验

1、编写自定义异常类

package com.yangj0020.template.exception;

public enum BusinessExceptionCode {

    USER_LOGIN_NAME_EXIST("登录名已存在"),
    LOGIN_USER_ERROR("用户名不存在或密码错误"),
    //按需添加
    ;

    private String desc;

    BusinessExceptionCode(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}
package com.yangj0020.template.exception;

public class BusinessException extends RuntimeException{

    private BusinessExceptionCode code;

    public BusinessException (BusinessExceptionCode code) {
        super(code.getDesc());
        this.code = code;
    }

    public BusinessExceptionCode getCode() {
        return code;
    }

    public void setCode(BusinessExceptionCode code) {
        this.code = code;
    }

    /**
     * 不写入堆栈信息,提高性能
     */
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}

2、对自定义异常进行统一处理,在 ControllerExceptionHandler 添加如下内容:

/**
     * 自定义异常统一处理
     * @param e
     * @return
     */
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public CommonResp validExceptionHandler(BusinessException e) {
        CommonResp commonResp = new CommonResp();
        LOG.warn("业务异常:{}", e.getCode().getDesc());
        commonResp.setSuccess(false);
        commonResp.setMessage(e.getCode().getDesc());
        return commonResp;
    }

    /**
     * 校验异常统一处理
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public CommonResp validExceptionHandler(Exception e) {
        CommonResp commonResp = new CommonResp();
        LOG.error("系统异常:", e);
        commonResp.setSuccess(false);
        commonResp.setMessage("系统出现异常,请联系管理员");
        return commonResp;
    }

3、编写 LoginReq 类和 LoginResp 类,

前者一般只需要“用户名”和“密码”字段;
后者一般设置“id”、“用户名”和“token”字段,“token” 必须返回给前端

4、编写 JwtUtil 工具类

package com.yangj0020.template.util;

import com.yangj0020.template.req.UserLoginReq;
import io.jsonwebtoken.*;

import java.util.Date;
import java.util.UUID;

/**
 * @Author Yang Jie
 * @Date 2021/6/18 23:24
 * @Descrition JWT工具类
 */
public class JwtUtil {

    private static long time = 1000 * 60 * 60;

    private static String signature = "o38hx0!90fn!%jf";

    public static String createToken(UserLoginReq req) {
        JwtBuilder jwtBuilder = Jwts.builder();
        String jwtToken = jwtBuilder
                .setAudience(req.getName())
                //header
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                //payload
                .claim("password", req.getPassword())
                .setExpiration(new Date(System.currentTimeMillis() + time))
                .setId(UUID.randomUUID().toString())
                //signature
                .signWith(SignatureAlgorithm.HS256, signature)
                .compact();
        return jwtToken;
    }

    public static boolean verifyToken(String token) {
        if (token == null) {
            return false;
        }
        try {
            Jwts.parser().setSigningKey(signature).parseClaimsJws(token);
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
     * 通过载荷名字获取载荷的值
     */
    public static String getAudience(String token) {
        JwtParser jwtParser = Jwts.parser();
        return jwtParser.setSigningKey(signature).parseClaimsJws(token).getBody().getAudience();
    }
}

5、实现登录接口,用户名和密码校验成功后设置 token 并返回给前端

/**
	 * 登录
	 */
	@PostMapping("/login")
    public CommonResp login(@Valid @RequestBody UserLoginReq req) {
        req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
        CommonResp<UserLoginResp> resp = new CommonResp<>();
        UserLoginResp userLoginResp = userService.login(req);

        LOG.info("开始放置token");
        userLoginResp.setToken(JwtUtil.createToken(req));
        
        resp.setContent(userLoginResp);
        return resp;
    }

6、编写登录拦截器 LoginInterceptor 校验 token

package com.yangj0020.template.interceptor;

import com.yangj0020.template.util.JwtUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

/**
 * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 打印请求信息
        LOG.info("------------- LoginInterceptor 开始 -------------");
        long startTime = System.currentTimeMillis();
        request.setAttribute("requestStartTime", startTime);

        // OPTIONS请求不做校验,
        // 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
        if(request.getMethod().toUpperCase().equals("OPTIONS")){
            return true;
        }

        String path = request.getRequestURL().toString();
        LOG.info("接口登录拦截:,path:{}", path);

        //获取header的token参数,前端需要在请求的header里面添加token
        String token = request.getHeader("token");
        LOG.info("登录校验开始,token:{}", token);
        if (token == null || token.isEmpty()) {
            LOG.info( "token为空,请求被拦截" );
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        if (JwtUtil.verifyToken(token)) {
            LOG.info("已登录:{}", JwtUtil.getAudience(token));
            return true;
        } else {
            LOG.warn( "token无效,请求被拦截" );
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        long startTime = (Long) request.getAttribute("requestStartTime");
        LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LOG.info("LogInterceptor 结束");
    }
}

7、编写 SpringMVCConfig 类

package com.yangj0020.template.config;

import com.yangj0020.template.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    @Resource
    LoginInterceptor loginInterceptor;

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/user/login" //可按需添加其它请求
                );
    }
}

十一、解决前后端交互相关问题

1、编写 JacksonConfig 配置类,解决前后端交互 Long 类型精度丢失问题

package com.yangj0020.template.config;

/**
 * @Author Yang Jie
 * @Date 2021/6/9 21:27
 *
 * 解决前后端交互Long类型精度丢失的问题
 */
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        return objectMapper;
    }
}

2、编写 CorsConfig 配置类解决跨域问题

package com.yangj0020.template.config;

/**
 * @Author Yang Jie
 * @Date 2021/06/2021/6/9
 *
 * 解決跨域问题
 */
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowCredentials(true)
                .maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
    }

}

前端

一、Vue CLI 项目搭建

vue create 项目名

选择 Babel、Router、Vuex 即可

二、多环境配置

1、开发环境
步骤 1:新建文件 .env.dev,与src目录同级
步骤 2:文件中写入相关配置,如:

NODE_ENV=development
VUE_APP_SERVER=http://127.0.0.1:8880  # 自定义属性,前缀VUE_APP_是必须的

2、生产环境
步骤 1:新建文件 .env.prod,与 src 目录同级
步骤 2:文件中写入相关配置,如:

NODE_ENV=production
VUE_APP_SERVER=http://xxx.xxx.xxx.xxx

3、在 package.json 文件中修改 npm 启动项
修改前:

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },

修改后:

"scripts": {
    "serve-dev": "vue-cli-service serve --mode dev --port 8080",
    "serve-prod": "vue-cli-service serve --mode prod --port 8080",
    "build-dev": "vue-cli-service build --mode dev",
    "build-prod": "vue-cli-service build --mode prod",
    "lint": "vue-cli-service lint"
  },

4、使用 Axios 的 baseUrl
在 main.js 中加入如下代码,则请求路径可忽略“http://127.0.0.1:8080”(或服务器IP地址:端口号):

import axios from 'axios'

axios.defaults.baseURL = process.env.VUE_APP_SERVER

三、添加封装的工具类 tool.js(util 目录下)

"use strict";
exports.__esModule = true;
exports.Tool = void 0;
var Tool = /** @class */ (function () {
    function Tool() {
    }
    /**
     * 空校验 null或""都返回true
     */
    Tool.isEmpty = function (obj) {
        if ((typeof obj === 'string')) {
            return !obj || obj.replace(/\s+/g, "") === "";
        }
        else {
            return (!obj || JSON.stringify(obj) === "{}" || obj.length === 0);
        }
    };
    /**
     * 非空校验
     */
    Tool.isNotEmpty = function (obj) {
        return !this.isEmpty(obj);
    };
    /**
     * 对象复制
     * @param obj
     */
    Tool.copy = function (obj) {
        if (Tool.isNotEmpty(obj)) {
            return JSON.parse(JSON.stringify(obj));
        }
    };
    /**
     * 使用递归将数组转为树形结构
     * 父ID属性为parent
     */
    Tool.array2Tree = function (array, parentId) {
        if (Tool.isEmpty(array)) {
            return [];
        }
        var result = [];
        for (var i = 0; i < array.length; i++) {
            var c = array[i];
            // console.log(Number(c.parent), Number(parentId));
            if (Number(c.parent) === Number(parentId)) {
                result.push(c);
                // 递归查看当前节点对应的子节点
                var children = Tool.array2Tree(array, c.id);
                if (Tool.isNotEmpty(children)) {
                    c.children = children;
                }
            }
        }
        return result;
    };
    /**
     * 随机生成[len]长度的[radix]进制数
     * @param len
     * @param radix 默认62
     * @returns {string}
     */
    Tool.uuid = function (len, radix) {
        if (radix === void 0) { radix = 62; }
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
        var uuid = [];
        radix = radix || chars.length;
        for (var i = 0; i < len; i++) {
            uuid[i] = chars[0 | Math.random() * radix];
        }
        return uuid.join('');
    };
    return Tool;
}());
exports.Tool = Tool;

四、使用 Axios 拦截器打印前端日志

在 main.js 文件中加入如下代码:

/**
 * axios拦截器
 */
axios.interceptors.request.use(function (config) {
    console.log('请求参数:', config);
    return config;
}, error => {
    return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
    console.log('返回结果:', response);
    return response;
}, error => {
    console.log('返回错误:', error);
    return Promise.reject(error);
});

五、密码加密传输(与后端密码加密配套)

1、在 public 目录下加入 js/md5.js 文件,内容如下:

var KEY = "!@#QWERT"; // 盐值
/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hexMd5(s) {
    return hex_md5(s);
}
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
    return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
function core_md5(x, len)
{
    /* append padding */
    x[len >> 5] |= 0x80 << ((len) % 32);
    x[(((len + 64) >>> 9) << 4) + 14] = len;

    var a =  1732584193;
    var b = -271733879;
    var c = -1732584194;
    var d =  271733878;

    for(var i = 0; i < x.length; i += 16)
    {
        var olda = a;
        var oldb = b;
        var oldc = c;
        var oldd = d;

        a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
        d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
        c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
        b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
        a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
        d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
        c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
        b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
        a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
        d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
        c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
        b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
        a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
        d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
        c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
        b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

        a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
        d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
        c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
        b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
        a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
        d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
        c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
        b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
        a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
        d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
        c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
        b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
        a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
        d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
        c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
        b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

        a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
        d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
        c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
        b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
        a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
        d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
        c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
        b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
        a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
        d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
        c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
        b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
        a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
        d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
        c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
        b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

        a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
        d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
        c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
        b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
        a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
        d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
        c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
        b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
        a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
        d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
        c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
        b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
        a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
        d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
        c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
        b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

        a = safe_add(a, olda);
        b = safe_add(b, oldb);
        c = safe_add(c, oldc);
        d = safe_add(d, oldd);
    }
    return Array(a, b, c, d);

}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
    return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
    return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
    return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
    return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
    return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Calculate the HMAC-MD5, of a key and some data
 */
function core_hmac_md5(key, data)
{
    var bkey = str2binl(key);
    if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);

    var ipad = Array(16), opad = Array(16);
    for(var i = 0; i < 16; i++)
    {
        ipad[i] = bkey[i] ^ 0x36363636;
        opad[i] = bkey[i] ^ 0x5C5C5C5C;
    }

    var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
    return core_md5(opad.concat(hash), 512 + 128);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
    var lsw = (x & 0xFFFF) + (y & 0xFFFF);
    var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
    return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
    return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert a string to an array of little-endian words
 * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
function str2binl(str)
{
    var bin = Array();
    var mask = (1 << chrsz) - 1;
    for(var i = 0; i < str.length * chrsz; i += chrsz)
        bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
    return bin;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2str(bin)
{
    var str = "";
    var mask = (1 << chrsz) - 1;
    for(var i = 0; i < bin.length * 32; i += chrsz)
        str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
    return str;
}

/*
 * Convert an array of little-endian words to a hex string.
 */
function binl2hex(binarray)
{
    var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
    var str = "";
    for(var i = 0; i < binarray.length * 4; i++)
    {
        str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
            hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
    }
    return str;
}

/*
 * Convert an array of little-endian words to a base-64 string
 */
function binl2b64(binarray)
{
    var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    var str = "";
    for(var i = 0; i < binarray.length * 4; i += 3)
    {
        var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
            | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
            |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
        for(var j = 0; j < 4; j++)
        {
            if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
            else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
        }
    }
    return str;
}

2、在 public/index.html 中引用

<script src="<%= BASE_URL %>js/md5.js"></script>

3、在相应的请求(与后端接口配套,一一对应)前添加一行代码:

//密码加密
this.resetDialogForm.password = hexMd5(this.resetDialogForm.password + KEY);

axios.post("/user/reset-password", this.resetDialogForm).then((response) => {
  const data = response.data;
  if (data.success) {
    this.resetDialogFormVisible = false;

    //重新加载列表
    this.handleQuery({
      page: this.pagination.current,
      size: this.pagination.pageSize
    });
  } else {
    this.$message.error(data.message);
  }
});

六、登录权限校验(JWT)

1、在 store/index.js 中定义全局变量 user,记录登录的用户信息

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const USER = 'USER'

const store = new Vuex.Store({
  state: {
    user: SessionStorage.get(USER) || {}
  },
  mutations: {
    setUser (state, user) {
      state.user = user;
      SessionStorage.set(USER, user);
    }
  },
  actions: {
  },
  modules: {
  }
});

export default store;

2、在其它地方要用到全局变量 user 时,可以通过计算属性获得 user 值,如:

<script>
    import store from '@/store'
    export default {
        name: "the-header",
        computed: {
            user() { 
                return store.state.user;
            }
        },
    }
</script>

3、登录

onSubmit(formName) {
  // 为表单绑定验证功能
  this.$refs[formName].validate((valid) => {
    if (valid) {
      this.form.password = hexMd5(this.form.password + KEY);
      axios.post("/user/login", this.form).then((response) => {
        const data = response.data;
        if (data.success) {
        
          // 登录成功后,保存 user 信息
          store.commit('setUser', data.content); 
          
          this.$router.push("/page");
        } else {
          this.$message.error(data.message);
        }
      });
    }
  });
}

4、退出登录,清空 user 即可

logout() {
    store.commit('setUser', {});
    this.$router.push('/login');
}

5、往请求的 header 中加入 token,在 axios 拦截器中新增如下标记的代码

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from "axios";
import {Tool} from '@/util/tool'

Vue.config.productionTip = false;

axios.defaults.baseURL = process.env.VUE_APP_SERVER;

/**
 * axios拦截器
 */
axios.interceptors.request.use(function (config) {
  console.log('请求参数:', config);
  
  /**
   * 该部分新增的代码
   */
  const token = store.state.user.token;
  if(Tool.isNotEmpty(token)) {
    config.headers.token = token;
    console.log("请求headers增加token", token);
  }
  
  return config;
}, error => {
  return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
  console.log('返回结果:', response);
  return response;
}, error => {
  console.log('返回错误:', error);
  return Promise.reject(error);
});

Vue.use(ElementUI);

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

6、增加登录校验(router/index.js 添加 loginRequire 属性并校验)

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Test from '../views/Test.vue'
import Login from '../views/Login.vue'
import Page from '../views/Page.vue'
import TestRouter from '../views/TestRouter.vue'
import axios from "axios";
import {Tool} from "@/util/tool";
import store from "@/store";

Vue.use(VueRouter)

const routes = [
  // {
  //   path: '/',
  //   name: 'Home',
  //   component: Home
  // },
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/page',
    name: 'Page',
    component: Page,
    redirect: '/page/test',
    meta: { //需要登录校验的路径下,在meta中添加自定义属性 loginRequire
      loginRequire: true
    },
    children: [
      {
          path: 'test',
          name: 'Test',
          component: Test,
          meta: {
            loginRequire: true
          },
      },
      {
        path: 'testRouter',
        name: 'TestRouter',
        component: TestRouter,
        meta: {
          loginRequire: true
        },
      }
    ]
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

// 路由登录拦截
router.beforeEach((to, from, next) => {
  // 要不要对meta.loginRequire属性做监控拦截
  if (to.matched.some(function (item) {
    console.log(item, "是否需要登录校验:", item.meta.loginRequire);
    return item.meta.loginRequire
  })) {
    const user = store.state.user;
    if (Tool.isEmpty(user)) {
      console.log("用户未登录!");
      next('/');
    } else {
      next();
    }
  } else {
    next();
  }
});

export default router