目录
- 后端
- 一、SpringBoot项目搭建
- 二、添加基本工具类
- 三、SpringBoot项目日志优化
- 四、整合 MyBatis
- 五、整合 Mybatis-generator
- 六、整合 PageHelper 分页插件
- 七、封装请求参数和返回参数
- 八、集成 Validation 做参数校验
- 九、密码加密
- 十、整合 JWT 实现登录权限校验
- 十一、解决前后端交互相关问题
- 前端
- 一、Vue CLI 项目搭建
- 二、多环境配置
- 三、添加封装的工具类 tool.js(util 目录下)
- 四、使用 Axios 拦截器打印前端日志
- 五、密码加密传输(与后端密码加密配套)
- 六、登录权限校验(JWT)
后端
一、SpringBoot项目搭建
1、新建SpringBoot项目,仅需选择如下依赖,后续开发过程中再按需添加。
2、构建项目目录,如下:
说明
- 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、配置启动项
注意: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