开发SpringBoot+Jwt+Vue的前后端分离后台管理系统VueAdmin(一)

为了让更多同学学习到前后端分离管理系统的搭建过程,这里我写了详细的开发过程的文档,使用的是springsecurity + jwt + vue的技术栈组合,如果有帮助,别忘了点个赞和关注我的公众号哈!


首发公众号:MarkerHub

作者:吕一明

项目源码:关注公众号 MarkerHub 回复【 234 】获取

项目视频:www.bilibili.com/video/BV1af…

转载请保留此声明,感谢!

1. 前言

从零开始搭建一个项目骨架,最好选择合适熟悉的技术,并且在未来易拓展,适合微服务化体系等。所以一般以Springboot作为我们的框架基础,这是离不开的了。

然后数据层,我们常用的是Mybatis,易上手,方便维护。但是单表操作比较困难,特别是添加字段或减少字段的时候,比较繁琐,所以这里我推荐使用Mybatis Plus(mp.baomidou.com/),为简化开发而生,只… CRUD 操作,从而节省大量时间。

作为一个项目骨架,权限也是我们不能忽略的,上一个项目vueblog我们使用了shiro,但是有些同学想学学SpringSecurity,所以这一期我们使用security作为我们的权限控制和会话控制的框架。

考虑到项目可能需要部署多台,一些需要共享的信息就保存在中间件中,Redis是现在主流的缓存中间件,也适合我们的项目。

然后因为前后端分离,所以我们使用jwt作为我们用户身份凭证,并且session我们会禁用,这样以前传统项目使用的方式我们可能就不再适合使用,这点需要注意了。

ok,我们现在就开始搭建我们的项目脚手架!

技术栈:

  • SpringBoot
  • mybatis plus
  • spring security
  • lombok
  • redis
  • hibernate validatior
  • jwt

2. 新建springboot项目,注意版本

这里,我们使用IDEA来开发我们项目,新建步骤比较简单,我们就不截图了。

开发工具与环境:

  • idea
  • mysql
  • jdk 8
  • maven3.3.9

新建好的项目结构如下,SpringBoot版本使用的目前最新的2.4.0版本

图片

图片

pom的jar包导入如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.0</version>
    <relativePath/>
</parent>
<groupId>com.markerhub</groupId>
<artifactId>vueadmin-java</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>vueadmin-java</name>
<description>公众号:MarkerHub</description>
<properties>
    <java.version>1.8</java.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
复制代码
  • devtools:项目的热加载重启插件
  • lombok:简化代码的工具

3. 整合mybatis plus,生成代码

接下来,我们来整合mybatis plus,让项目能完成基本的增删改查操作。步骤很简单:可以去官网看看:mp.baomidou.com/guide/

第一步:导入jar包

pom中导入mybatis plus的jar包,因为后面会涉及到代码生成,所以我们还需要导入页面模板引擎,这里我们用的是freemarker。

<!--整合mybatis plus https://baomidou.com/-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>
<!--mp代码生成器-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.1</version>
</dependency>
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.30</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
复制代码

第二步:然后去写配置文件

server:
  port: 8081
# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: admin
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml
复制代码

上面除了配置数据库的信息,还配置了myabtis plus的mapper的xml文件的扫描路径,这一步不要忘记了。然后因为前段默认是8080端口了,所以后端我们设置为8081端口,防止端口冲突。

第三步:开启mapper接口扫描,添加分页、防全表更新插件

新建一个包:通过@mapperScan注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。

  • com.markerhub.config.MybatisPlusConfig
@Configuration
@MapperScan("com.markerhub.mapper")
public class MybatisPlusConfig {
   /**
    * 新的分页插件,一缓和二缓遵循mybatis的规则,
    * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
    * 避免缓存出现问题(该属性会在旧插件移除后一同移除)
    */
   @Bean
   public MybatisPlusInterceptor mybatisPlusInterceptor() {
      MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
      interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
      // 防止全表更新和删除
      interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
      return interceptor;
   }
   @Bean
   public ConfigurationCustomizer configurationCustomizer() {
      return configuration -> configuration.setUseDeprecatedExecutor(false);
   }
}
复制代码

上面代码中,我们给Mybatis plus添加了2个拦截器,这是根据mp官网配置的:

  • PaginationInnerInterceptor:新的分页插件
  • BlockAttackInnerInterceptor:防止全表更新和删除

第四步:创建数据库和表

因为是后台管理系统的权限模块,所以我们需要考虑的表主要就几个:用户表、角色表、菜单权限表、以及关联的用户角色中间表、菜单角色中间表。就5个表,至于什么字段其实都听随意的,用户表里面除了用户名、密码字段必要,其他其实都听随意,然后角色和菜单我们可以参考一下其他的系统、或者自己在做项目的过程中需要的时候在添加也行,反正重新生成代码也是非常简便的事情,综合考虑,数据库名称为vueadmin,我们建表语句如下:

  • vueadmin.sql
/*
Navicat MySQL Data Transfer
Source Server         : localhost
Source Server Version : 50717
Source Host           : localhost:3306
Source Database       : vueadmin
Target Server Type    : MYSQL
Target Server Version : 50717
File Encoding         : 65001
Date: 2021-01-23 09:41:50
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
  `name` varchar(64) NOT NULL,
  `path` varchar(255) DEFAULT NULL COMMENT '菜单URL',
  `perms` varchar(255) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
  `component` varchar(255) DEFAULT NULL,
  `type` int(5) NOT NULL COMMENT '类型     0:目录   1:菜单   2:按钮',
  `icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',
  `orderNum` int(11) DEFAULT NULL COMMENT '排序',
  `created` datetime NOT NULL,
  `updated` datetime DEFAULT NULL,
  `statu` int(5) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) NOT NULL,
  `code` varchar(64) NOT NULL,
  `remark` varchar(64) DEFAULT NULL COMMENT '备注',
  `created` datetime DEFAULT NULL,
  `updated` datetime DEFAULT NULL,
  `statu` int(5) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`) USING BTREE,
  UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL,
  `menu_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `city` varchar(64) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `updated` datetime DEFAULT NULL,
  `last_login` datetime DEFAULT NULL,
  `statu` int(5) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4;
复制代码

第五步:代码生成

  1. 获取项目数据库所对应表和字段的信息
  2. 新建一个freemarker的页面模板 - SysUser.java.ftl - ${baseEntity}
  3. 提供相关需要进行渲染的动态数据 - BaseEntity、表字段、注释、baseEntity=SuperEntity
  4. 使用freemarker模板引擎进行渲染! - SysUser.java
# 获取表
SELECT
	*
FROM
	information_schema. TABLES
WHERE
	TABLE_SCHEMA = (SELECT DATABASE());
# 获取字段
SELECT
	*
FROM
	information_schema. COLUMNS
WHERE
	TABLE_SCHEMA = (SELECT DATABASE())
AND TABLE_NAME = "sys_user";
复制代码

有了数据库之后,那么现在就已经可以使用mybatis plus了,官方给我们提供了一个代码生成器,然后我写上自己的参数之后,就可以直接根据数据库表信息生成entity、service、mapper等接口和实现类。 因为代码比较长,就不贴出来了,说明一下重点:

  • com.markerhub.CodeGenerator

图片

上面代码生成的过程中,我默认所有的实体类都继承BaseEntity,控制器都继承BaseController,所以在代码生成之前,最好先编写这两个基类:

  • com.markerhub.entity.BaseEntity
@Data
public class BaseEntity implements Serializable {
   @TableId(value = "id", type = IdType.AUTO)
   private Long id;
   private LocalDateTime created;
   private LocalDateTime updated;
   private Integer statu;
}
复制代码
  • com.markerhub.controller.BaseController
public class BaseController {
   @Autowired
   HttpServletRequest req;
}
复制代码

然后我们单独运行CodeGenerator的main方法,注意调整CodeGenerator的数据库连接、账号密码啥的,然后我们输入表名称,通过逗号隔开:sys_menu,sys_role,sys_role_menu,sys_user,sys_user_role 执行结果成功:

图片

然后我们生成了一些代码如下:

图片

这里有点需要注意,因为关联的用户角色中间表、菜单角色中间表我们是没有created等几个公共字段的,所以我们把这两个实体继承BaseEntity去掉:

图片

最后这样子的:

@Data
public class SysRoleMenu {
   ...
}
复制代码

简洁!方便!经过上面的步骤,基本上我们已经把mybatis plus框架集成到项目中了,并且也生成了基本的代码,省了好多功夫。然后我们做个简单测试:

  • com.markerhub.controller.TestController
@RestController
public class TestController {
   @Autowired
   SysUserService userService;
   @GetMapping("/test")
   public Object test() {
      return userService.list();
   }
}
复制代码

然后sys_user随意添加几条数据,结果如下: 图片

ok,毛什么问题,大家不用在意密码是怎么生成的,后面我们会说到,你现在随意填写就好了。对了,好多人问我的浏览器的json数据怎么显示这么好看,这是因为我用了JSONView这个插件:

图片

4. 结果封装

因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。

这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的

  • 是否成功,可用code表示(如200表示成功,400表示异常)
  • 结果消息
  • 结果数据

所以可得到封装如下:

  • com.markerhub.common.lang.Result
@Data
public class Result implements Serializable {
    private int code; // 200是正常,非200表示异常
    private String msg;
    private Object data;
    
    public static Result succ(Object data) {
        return succ(200, "操作成功", data);
    }
    public static Result succ(int code, String msg, Object data) {
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }
    public static Result fail(String msg) {
        return fail(400, msg, null);
    }
    public static Result fail(String msg, Object data) {
        return fail(400, msg, data);
    }
    public static Result fail(int code, String msg, Object data) {
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }
}
复制代码

另外出了在结果封装类上的code可以提现数据是否正常,我们还可以通过http的状态码来提现访问是否遇到了异常,比如401表示五权限拒绝访问等,注意灵活使用。

5. 全局异常处理

有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。

处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。

步骤二、定义全局异常处理,@ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。

  • com.markerhub.common.exception.GlobalExceptionHandler
/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(value = AccessDeniedException.class)
    public Result handler(AccessDeniedException e) {
        log.info("security权限不足:----------------{}", e.getMessage());
        return Result.fail("权限不足");
    }
    
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) {
        log.info("实体校验异常:----------------{}", e.getMessage());
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.fail(objectError.getDefaultMessage());
    }
    
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) {
        log.error("Assert异常:----------------{}", e.getMessage());
        return Result.fail(e.getMessage());
    }
    
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e) {
        log.error("运行时异常:----------------{}", e);
        return Result.fail(e.getMessage());
    }
}
复制代码

上面我们捕捉了几个异常:

  • ShiroException:shiro抛出的异常,比如没有权限,用户登录异常
  • IllegalArgumentException:处理Assert的异常
  • MethodArgumentNotValidException:处理实体校验的异常
  • RuntimeException:捕捉其他异常

6. 整合Spring Security

很多人不懂spring security,觉得这个框架比shiro要难,的确,security更加复杂一点,同时功能也更加强大,我们首先来看一下security的原理....

后续:请进入主页查看下篇

也可以关注公众号MarkerHub,回复234获取更详细的开发笔记和源码、视频等。

分类:
后端