转载

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

(1) 相关博文地址:

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y-h/p/12930895.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(二):引入 element-ui 定义基本页面显示:https://www.cnblogs.com/l-y-h/p/12935300.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(三):引入 js-cookie、axios、mock 封装请求处理以及返回结果:https://www.cnblogs.com/l-y-h/p/12955001.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(四):引入 vuex 进行状态管理、引入 vue-i18n 进行国际化管理:https://www.cnblogs.com/l-y-h/p/12963576.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(五):引入 vue-router 进行路由管理、模块化封装 axios 请求、使用 iframe 标签嵌套页面:https://www.cnblogs.com/l-y-h/p/12973364.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(六):使用 vue-router 进行动态加载菜单:https://www.cnblogs.com/l-y-h/p/13052196.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(一): 搭建基本环境、整合 Swagger、MyBatisPlus、JSR303 以及国际化操作:https://www.cnblogs.com/l-y-h/p/13083375.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能:https://www.cnblogs.com/l-y-h/p/13163653.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(三): 整合阿里云 OSS 服务 -- 上传、下载文件、图片:https://www.cnblogs.com/l-y-h/p/13202746.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(四): 整合阿里云 短信服务、整合 JWT 单点登录:https://www.cnblogs.com/l-y-h/p/13214493.html

(2)代码地址:

https://github.com/lyh-man/admin-vue-template.git

一、数据表设计

1、需求分析

(1)目的:

由于此项目作为一个后台管理系统模板,不同用户登录后应该有不同的操作权限,所以此处实现一个简单的菜单权限控制。即不同用户登录系统后,会展示不同的菜单,并对菜单具有操作(增删改查)的权限。

(2)数据表设计(自己瞎捣鼓的,有不对的地方还望 DBA 大神不吝赐教(=_=)):

需求:

一个用户登录系统后,根据其所代表的的角色,去查询其对应的菜单权限,并返回相应的菜单数据。

整个设计核心可以分为:用户、用户角色(下面简称角色)、菜单权限(下面简称菜单)。

思考一:

一个用户只拥有一个角色,一个角色可以被多个用户拥有。

一个角色可以有多个菜单,一个菜单可以被多个角色拥有。

即 角色 与 用户间为 1 对 多关系,角色 与 菜单 间为 多对多关系。

所以可以在用户表中定义一个字段作为外键 关联到 角色表。

而角色表 与 菜单表 采用 中间表去维护。

思考二:

一个用户可以有多个角色,一个角色可以被多个用户拥有。

一个角色可以有多个菜单,一个菜单可以被多个角色拥有。

即 菜单 与 角色 间属于 多对多关系,用户 与 角色间 也属于 多对多关系。

所以 用户表 与 角色表间、角色表 与 菜单表间均可以采用 中间表维护。

为了避免使用外键,此处我均采用中间表对三张表进行数据关联。

最终设计(三个主表,两个中间表):

用户表 sys_user

用户角色表 sys_user_role

角色表 sys_role

角色菜单表 sys_role_menu

菜单表 sys_menu

2、用户表(sys_user)设计

(1)必须字段:

用户 ID、用户名、用户手机号、用户密码。

其中:

用户手机号 作为用户注册、登录的依据(用户名也可以登录)。

用户名为 用户登录后显示的 昵称。

用户密码 需要密文存储(此项目中 前端、后端均对密码进行 MD5 加密处理)。

(2)数据表结构如下:

-- DROP DATABASE IF EXISTS admin_template;
--
-- CREATE DATABASE admin_template;

-- --------------------------sys_user 用户表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_user;
-- 用户表
CREATE TABLE sys_user (
    id bigint NOT NULL COMMENT '用户 ID',
    name varchar(20) NOT NULL COMMENT '用户名',
    mobile varchar(20) NOT NULL COMMENT '用户手机号',
    password varchar(64) NOT NULL COMMENT '用户密码',
   sex tinyint DEFAULT NULL COMMENT '性别, 0 表示女, 1 表示男',
   age tinyint DEFAULT NULL COMMENT '年龄',
   avatar varchar(255) DEFAULT NULL COMMENT '头像',
   email varchar(100) DEFAULT NULL COMMENT '邮箱',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
   disabled_flag tinyint DEFAULT NULL COMMENT '禁用标志, 0 表示未禁用, 1 表示禁用',
   wx_id varchar(128) DEFAULT NULL COMMENT '微信 openid(拓展字段、用于第三方微信登录)',
   qq_id varchar(128) DEFAULT NULL COMMENT 'QQ openid(拓展字段、用于第三方 QQ 登录)',
    PRIMARY KEY(id),
    UNIQUE INDEX(name),
    UNIQUE INDEX(mobile)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统用户表';


-- 插入数据
INSERT INTO `sys_user`(`id`, `name`, `mobile`, `password`, `sex`, `age`, `avatar`, `email`, `create_time`, `update_time`, `delete_flag`, `disabled_flag`, `wx_id`, `qq_id`)
VALUES (1278601251755454466, 'superAdmin', '17730125031', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL),
    (1278601251755451232, 'admin', '17730125032', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL),
    (1278601251755456778, 'jack', '17730125033', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL);

-- --------------------------sys_user 用户表---------------------------------------

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

3、角色表(sys_role)设计

(1)必须字段:

角色 ID,角色名称。

其中:

角色名称用于定位用户角色。

(2)数据表结构如下:

-- DROP DATABASE IF EXISTS admin_template;
--
-- CREATE DATABASE admin_template;

-- --------------------------sys_role 角色表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_role;
-- 系统用户角色表
CREATE TABLE sys_role (
    id bigint NOT NULL COMMENT '角色 ID',
    role_name varchar(20) NOT NULL COMMENT '角色名称',
   role_code varchar(20) DEFAULT NULL COMMENT '角色码',
   remark varchar(255) DEFAULT NULL COMMENT '角色备注',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统用户角色表';


-- 插入数据
INSERT INTO `sys_role`(`id`, `role_name`, `role_code`, `remark`, `create_time`, `update_time`, `delete_flag`)
VALUES (1278601251755451245, 'superAdmin', '1001', '超级管理员','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755452551, 'admin', '2001', '普通管理员','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755458779, 'user', '3001', '普通用户','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

-- --------------------------sys_role 角色表---------------------------------------

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

4、菜单权限表(sys_menu)设计

(1)必须字段:

当前菜单 ID,父菜单 ID,菜单名,菜单类型,菜单路径

其中:

当前菜单 ID 与 父菜单 ID 用于确定菜单的层级顺序。

菜单类型 用于确定是否显示在菜单目录中(按钮不显示在菜单目录中)。

菜单路径 用于确定最终指向的 组件路径(使用 vue-route 进行路由跳转)。

注:

最外层 父菜单 ID 此处设置为 0,但不创建 ID 为 0 的数据。

(2)数据表结构如下:

-- DROP DATABASE IF EXISTS admin_template;
--
-- CREATE DATABASE admin_template;

-- --------------------------sys_menu 菜单权限表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_menu;
-- 系统菜单权限表
CREATE TABLE sys_menu (
    menu_id bigint NOT NULL COMMENT '当前菜单 ID',
    parent_id bigint NOT NULL COMMENT '当前菜单父菜单 ID',
   name_zh varchar(20) NOT NULL COMMENT '中文菜单名称',
   name_en varchar(40) NOT NULL COMMENT '英文菜单名称',
   type tinyint NOT NULL COMMENT '菜单类型,0 表示目录,1 表示菜单项,2 表示按钮',
   url varchar(100) NOT NULL COMMENT '访问路径',
   icon varchar(100) DEFAULT NULL COMMENT '菜单图标',
   order_num int DEFAULT NULL COMMENT '菜单项顺序',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
    PRIMARY KEY(menu_id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统菜单权限表';

-- 插入数据
INSERT INTO `sys_menu`(`menu_id`, `parent_id`, `name_zh`, `name_en`, `type`, `url`, `icon`, `order_num`, `create_time`, `update_time`, `delete_flag`)
VALUES (127860125171111, 0, '系统管理', 'System Control', 0, '', 'el-icon-setting', 0,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172211, 127860125171111, '用户管理', 'User Control', 1, 'sys/UserList', 'el-icon-user', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173311, 127860125171111, '角色管理', 'Role Control', 1, 'sys/RoleControl', 'el-icon-price-tag', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174411, 127860125171111, '菜单管理', 'Menu Control', 1, 'sys/MenuControl', 'el-icon-menu', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125172221, 127860125172211, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172231, 127860125172211, '删除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172241, 127860125172211, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172251, 127860125172211, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125173321, 127860125173311, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173331, 127860125173311, '删除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173341, 127860125173311, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173351, 127860125173311, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125174421, 127860125174411, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174431, 127860125174411, '删除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174441, 127860125174411, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174451, 127860125174411, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125175511, 0, '帮助', 'help', 0, '', 'el-icon-info', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125175521, 127860125175511, '百度', 'Baidu', 1, 'https://www.baidu.com/', 'el-icon-menu', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125175531, 127860125175511, '博客', 'Blog', 1, 'https://www.cnblogs.com/l-y-h/', 'el-icon-menu', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

-- --------------------------sys_menu 菜单权限表---------------------------------------

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

5、中间表设计(sys_user_role、sys_role_menu)

(1)设计原则:

中间表存储的是相关联两表的主键。

(2)用户角色表如下:

-- DROP DATABASE IF EXISTS admin_template;
--
-- CREATE DATABASE admin_template;

-- --------------------------sys_user_role 用户角色表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_user_role;
-- 系统用户角色表
CREATE TABLE sys_user_role (
    id bigint NOT NULL COMMENT '用户角色表 ID',
    role_id bigint NOT NULL COMMENT '角色 ID',
   user_id bigint NOT NULL COMMENT '用户 ID',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统用户角色表';


-- 插入数据
INSERT INTO `sys_user_role`(`id`, `role_id`, `user_id`, `create_time`, `update_time`, `delete_flag`)
VALUES (1278601251755452234, '1278601251755451245', '1278601251755454466', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755453544, '1278601251755452551', '1278601251755451232', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755454664, '1278601251755458779', '1278601251755456778', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

-- --------------------------sys_user_role 用户角色表---------------------------------------

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

(3)角色菜单表如下:

-- DROP DATABASE IF EXISTS admin_template;
--
-- CREATE DATABASE admin_template;

-- --------------------------sys_role_menu 系统角色菜单表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_role_menu;
-- 系统角色菜单表
CREATE TABLE sys_role_menu (
    id bigint NOT NULL COMMENT '角色菜单表 ID',
    role_id bigint NOT NULL COMMENT '角色 ID',
   menu_id varchar(20) NOT NULL COMMENT '菜单 ID',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统角色菜单表';


-- 插入数据
INSERT INTO `sys_role_menu`(`id`, `role_id`, `menu_id`, `create_time`, `update_time`, `delete_flag`)
VALUES (1278601251755461111, '1278601251755451245', '1278601251755451111', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461112, '1278601251755451245', '1278601251755452211', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461113, '1278601251755451245', '1278601251755453311', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461114, '1278601251755451245', '1278601251755454411', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461115, '1278601251755451245', '1278601251755452221', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461116, '1278601251755451245', '1278601251755452231', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461117, '1278601251755451245', '1278601251755452241', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461118, '1278601251755451245', '1278601251755452251', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461119, '1278601251755451245', '1278601251755453321', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461120, '1278601251755451245', '1278601251755453331', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461121, '1278601251755451245', '1278601251755453341', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461122, '1278601251755451245', '1278601251755453351', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461123, '1278601251755451245', '1278601251755454421', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461124, '1278601251755451245', '1278601251755454431', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461125, '1278601251755451245', '1278601251755454441', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461126, '1278601251755451245', '1278601251755454451', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461127, '1278601251755451245', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461128, '1278601251755451245', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461129, '1278601251755451245', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (1278601251755462111, '1278601251755452551', '1278601251755451111', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462112, '1278601251755452551', '1278601251755452211', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462113, '1278601251755452551', '1278601251755453311', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462114, '1278601251755452551', '1278601251755454411', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462115, '1278601251755452551', '1278601251755452251', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462116, '1278601251755452551', '1278601251755453351', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462117, '1278601251755452551', '1278601251755454451', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462118, '1278601251755452551', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462119, '1278601251755452551', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462120, '1278601251755452551', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (1278601251755463111, '1278601251755458779', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755463112, '1278601251755458779', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755463113, '1278601251755458779', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

-- --------------------------sys_role_menu 系统角色菜单表---------------------------------------

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

6、完整表结构以及相关数据插入

-- DROP DATABASE IF EXISTS admin_template;
--
-- CREATE DATABASE admin_template;

-- --------------------------sys_user 用户表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_user;
-- 用户表
CREATE TABLE sys_user (
    id bigint NOT NULL COMMENT '用户 ID',
    name varchar(20) NOT NULL COMMENT '用户名',
    mobile varchar(20) NOT NULL COMMENT '用户手机号',
    password varchar(64) NOT NULL COMMENT '用户密码',
   sex tinyint DEFAULT NULL COMMENT '性别, 0 表示女, 1 表示男',
   age tinyint DEFAULT NULL COMMENT '年龄',
   avatar varchar(255) DEFAULT NULL COMMENT '头像',
   email varchar(100) DEFAULT NULL COMMENT '邮箱',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
   disabled_flag tinyint DEFAULT NULL COMMENT '禁用标志, 0 表示未禁用, 1 表示禁用',
   wx_id varchar(128) DEFAULT NULL COMMENT '微信 openid(拓展字段、用于第三方微信登录)',
   qq_id varchar(128) DEFAULT NULL COMMENT 'QQ openid(拓展字段、用于第三方 QQ 登录)',
    PRIMARY KEY(id),
    UNIQUE INDEX(name, mobile)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统用户表';


-- 插入数据
INSERT INTO `sys_user`(`id`, `name`, `mobile`, `password`, `sex`, `age`, `avatar`, `email`, `create_time`, `update_time`, `delete_flag`, `disabled_flag`, `wx_id`, `qq_id`)
VALUES (1278601251755454466, 'superAdmin', '17730125031', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL),
    (1278601251755451232, 'admin', '17730125032', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL),
    (1278601251755456778, 'jack', '17730125033', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL);

-- --------------------------sys_user 用户表---------------------------------------

-- --------------------------sys_role 角色表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_role;
-- 系统用户角色表
CREATE TABLE sys_role (
    id bigint NOT NULL COMMENT '角色 ID',
    role_name varchar(20) NOT NULL COMMENT '角色名称',
   role_code varchar(20) DEFAULT NULL COMMENT '角色码',
   remark varchar(255) DEFAULT NULL COMMENT '角色备注',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统用户角色表';


-- 插入数据
INSERT INTO `sys_role`(`id`, `role_name`, `role_code`, `remark`, `create_time`, `update_time`, `delete_flag`)
VALUES (1278601251755451245, 'superAdmin', '1001', '超级管理员','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755452551, 'admin', '2001', '普通管理员','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755458779, 'user', '3001', '普通用户','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

-- --------------------------sys_role 角色表---------------------------------------


-- --------------------------sys_user_role 用户角色表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_user_role;
-- 系统用户角色表
CREATE TABLE sys_user_role (
    id bigint NOT NULL COMMENT '用户角色表 ID',
    role_id bigint NOT NULL COMMENT '角色 ID',
   user_id bigint NOT NULL COMMENT '用户 ID',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统用户角色表';


-- 插入数据
INSERT INTO `sys_user_role`(`id`, `role_id`, `user_id`, `create_time`, `update_time`, `delete_flag`)
VALUES (1278601251755452234, '1278601251755451245', '1278601251755454466', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755453544, '1278601251755452551', '1278601251755451232', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755454664, '1278601251755458779', '1278601251755456778', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

-- --------------------------sys_user_role 用户角色表---------------------------------------

-- --------------------------sys_menu 菜单权限表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_menu;
-- 系统菜单权限表
CREATE TABLE sys_menu (
    menu_id bigint NOT NULL COMMENT '当前菜单 ID',
    parent_id bigint NOT NULL COMMENT '当前菜单父菜单 ID',
   name_zh varchar(20) NOT NULL COMMENT '中文菜单名称',
   name_en varchar(40) NOT NULL COMMENT '英文菜单名称',
   type tinyint NOT NULL COMMENT '菜单类型,0 表示目录,1 表示菜单项,2 表示按钮',
   url varchar(100) NOT NULL COMMENT '访问路径',
   icon varchar(100) DEFAULT NULL COMMENT '菜单图标',
   order_num int DEFAULT NULL COMMENT '菜单项顺序',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
    PRIMARY KEY(menu_id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统菜单权限表';

-- 插入数据
INSERT INTO `sys_menu`(`menu_id`, `parent_id`, `name_zh`, `name_en`, `type`, `url`, `icon`, `order_num`, `create_time`, `update_time`, `delete_flag`)
VALUES (127860125171111, 0, '系统管理', 'System Control', 0, '', 'el-icon-setting', 0,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172211, 127860125171111, '用户管理', 'User Control', 1, 'sys/UserList', 'el-icon-user', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173311, 127860125171111, '角色管理', 'Role Control', 1, 'sys/RoleControl', 'el-icon-price-tag', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174411, 127860125171111, '菜单管理', 'Menu Control', 1, 'sys/MenuControl', 'el-icon-menu', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125172221, 127860125172211, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172231, 127860125172211, '删除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172241, 127860125172211, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172251, 127860125172211, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125173321, 127860125173311, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173331, 127860125173311, '删除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173341, 127860125173311, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173351, 127860125173311, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125174421, 127860125174411, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174431, 127860125174411, '删除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174441, 127860125174411, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174451, 127860125174411, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125175511, 0, '帮助', 'help', 0, '', 'el-icon-info', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125175521, 127860125175511, '百度', 'Baidu', 1, 'https://www.baidu.com/', 'el-icon-menu', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125175531, 127860125175511, '博客', 'Blog', 1, 'https://www.cnblogs.com/l-y-h/', 'el-icon-menu', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

-- --------------------------sys_menu 菜单权限表---------------------------------------

-- --------------------------sys_role_menu 系统角色菜单表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_role_menu;
-- 系统角色菜单表
CREATE TABLE sys_role_menu (
    id bigint NOT NULL COMMENT '角色菜单表 ID',
    role_id bigint NOT NULL COMMENT '角色 ID',
   menu_id varchar(20) NOT NULL COMMENT '菜单 ID',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间',
    delete_flag tinyint DEFAULT NULL COMMENT '逻辑删除标志,0 表示未删除, 1 表示删除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统角色菜单表';


-- 插入数据
INSERT INTO `sys_role_menu`(`id`, `role_id`, `menu_id`, `create_time`, `update_time`, `delete_flag`)
VALUES (1278601251755461111, '1278601251755451245', '1278601251755451111', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461112, '1278601251755451245', '1278601251755452211', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461113, '1278601251755451245', '1278601251755453311', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461114, '1278601251755451245', '1278601251755454411', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461115, '1278601251755451245', '1278601251755452221', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461116, '1278601251755451245', '1278601251755452231', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461117, '1278601251755451245', '1278601251755452241', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461118, '1278601251755451245', '1278601251755452251', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461119, '1278601251755451245', '1278601251755453321', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461120, '1278601251755451245', '1278601251755453331', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461121, '1278601251755451245', '1278601251755453341', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461122, '1278601251755451245', '1278601251755453351', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461123, '1278601251755451245', '1278601251755454421', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461124, '1278601251755451245', '1278601251755454431', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461125, '1278601251755451245', '1278601251755454441', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461126, '1278601251755451245', '1278601251755454451', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461127, '1278601251755451245', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461128, '1278601251755451245', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755461129, '1278601251755451245', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (1278601251755462111, '1278601251755452551', '1278601251755451111', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462112, '1278601251755452551', '1278601251755452211', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462113, '1278601251755452551', '1278601251755453311', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462114, '1278601251755452551', '1278601251755454411', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462115, '1278601251755452551', '1278601251755452251', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462116, '1278601251755452551', '1278601251755453351', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462117, '1278601251755452551', '1278601251755454451', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462118, '1278601251755452551', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462119, '1278601251755452551', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755462120, '1278601251755452551', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (1278601251755463111, '1278601251755458779', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755463112, '1278601251755458779', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755463113, '1278601251755458779', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

-- --------------------------sys_role_menu 系统角色菜单表---------------------------------------

二、完善注册登录逻辑

1、注册、登录需求分析:

(1)用户种类:

超级管理员、普通管理员、普通用户。

其中:

通过注册方式创建的用户均为 普通用户。

普通管理员由超级管理员创建。

超级管理员使用 系统默认的数据(不可创建、修改)。

默认:

普通用户 -- 账号:jack 密码:123456

普通管理员 -- 账号:admin 密码:123456

超级管理员 -- 账号:superAdmin 密码:123456

(2)注册需求:

输入用户名、密码,并根据 手机号 发送验证码进行注册。

其中:

用户名 不能为 纯数字 组成 或者 包含 @ 符号(为了与手机号、邮箱进行区分)。

密码前后端均采用 MD5 加密,两次加密。

验证码时效性为 5 分钟(此项目中借用 redis 进行过期时间控制)。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

(3)登录需求:

登录方式:密码登录、短信登录。

其中:

短信登录 是根据 手机号以及验证码 进行登录(跳过密码输入操作)。

密码登录 是根据 手机号 或者 用户名 加密码 的方式进行登录。

登录时提供忘记密码功能,根据手机号重置密码。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

登录时限制同一账号登陆人数。

注:

此项目中限制同一账号登陆人数为 1 人,即同时只允许一个 账号登陆系统。

实现限制同一账号登陆人数思路:

并发执行时,存在同一个用户在多处同时登陆,此处为了限制只能允许一个人登陆系统,使用 redis 进行辅助。其中 key 为 用户名(或者 ID 值)、 value 为 token 值(JWT 值)。

用户第一次访问系统时,首先判定是否为第一次登录系统(检查 redis 中是否存在 token),不存在则为第一次登录,需要将 token 存入 redis 中,并将该 token 返回给用户。存在则继续判定是否为重复登录系统(检查 token 是否一致)。token 一致,则为同一用户再次访问系统。token 不一致,则用户为重复登录系统,此时需要剔除前一个登录用户(比较当前 token 与 redis 中 token 的时间戳),如果当前 token 时间戳 大于等于 redis 中 token 时间戳,则当前时间戳为最新登录者,此时剔除 redis 中的 token 数据(即将 当前 token 数据存入 redis),如果 小于 redis 中 token 时间戳,则 redis 中 token 为最新登录者,需剔除当前 token(不返回 token 给用户,即登录失败,引导用户重新登录)。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

注意:

此处为了实现效果,还需要修改 单点登录 逻辑,之前单点登录逻辑中,根据 token 可以直接解析出 用户信息。

但是在此处 token 并不一定有效,因为存在同一用户在多处登录,每一次登录均会产生一个 token(定义拦截器,拦截除了登录请求外的所有请求,这样使每次登录请求均能产生 token,非登录请求验证是否存在 token),此时为了限制只允许一人登录,即只有一个 token 生效。

需要与 redis 中存储的 token 比较后才可确认。若 两者 token 不同,需引导用户重新进行登录操作,并将最新的 token 存入 redis(感觉代码好像变得有点冗余了(=_=),毕竟每次还得与 redis 进行交互,有更方便的方法还望不吝赐教)。

2、生成基本代码

(1)使用 mybatis-plus 代码生成器根据 sys_user 表生成基本代码。

此处不再重复截图,详细使用过程参考:

https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_1

此处只截细节部分:

Step1:

修改实体类,添加 @TableField(用于自动填充)、@TableLogic(用于逻辑删除) 注解。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

Step2:

由于新增了填充字段 disabledFlag,所以需给其添加填充规则。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

Step3:

修改 mapper 扫描路径,此处可以使用通配符 **(只用一个 * 不生效时使用两个 **)。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

3、编写一个工具类( Md5Util.java) 用于加密密码

(1)目的

此项目中使用 MD5 进行密码加密,使用其他方式亦可。

此加密方式网上随便搜搜就可以搜的到,代码实现也不尽相同,此处代码来源于网络。

(2)代码实现如下:

package com.lyh.admin_template.back.common.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {
    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

4、调整 JWT 工具类、SMS 工具类

(1)目的:

之前考虑的有点欠缺,这两个工具类使用起来有点问题,稍作修改。

(2)修改 JWT 工具类 JwtUtil.java

主要修改 自定义数据 的方式,以及自定义 过期时间。

package com.lyh.admin_template.back.common.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * JWT 操作工具类
 */
public class JwtUtil {

    // 设置默认过期时间(15 分钟)
    private static final long DEFAULT_EXPIRE = 1000L * 60 * 15;
    // 设置 jwt 生成 secret(随意指定)
    private static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    /**
     * 生成 jwt token,并指定默认过期时间 15 分钟
     */
    public static String getJwtToken(Object data) {
        return getJwtToken(data, DEFAULT_EXPIRE);
    }

    /**
     * 生成 jwt token,根据指定的 过期时间
     */
    public static String getJwtToken(Object data, Long expire) {
        String JwtToken = Jwts.builder()
                // 设置 jwt 类型
                .setHeaderParam("typ", "JWT")
                // 设置 jwt 加密方法
                .setHeaderParam("alg", "HS256")
                // 设置 jwt 主题
                .setSubject("admin-user")
                // 设置 jwt 发布时间
                .setIssuedAt(new Date())
                // 设置 jwt 过期时间
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                // 设置自定义数据
                .claim("data", data)
                // 设置密钥与算法
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                // 生成 token
                .compact();
        return JwtToken;
    }

    /**
     * 判断token是否存在与有效,true 表示未过期,false 表示过期或不存在
     */
    public static boolean checkToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) {
            return false;
        }
        try {
            // 获取 token 数据
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            // 判断是否过期
            return claimsJws.getBody().getExpiration().after(new Date());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 判断token是否存在与有效
     */
    public static boolean checkToken(HttpServletRequest request) {
        return checkToken(request.getHeader("token"));
    }

    /**
     * 根据 token 获取数据
     */
    public static Claims getTokenBody(HttpServletRequest request) {
        return getTokenBody(request.getHeader("token"));
    }

    /**
     * 根据 token 获取数据
     */
    public static Claims getTokenBody(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) {
            return null;
        }
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        return claimsJws.getBody();
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

(3)修改 短信发送工具类 SmsUtil.java

主要修改 其返回数据的方式,返回 code,而非 boolean 数据。

package com.lyh.admin_template.back.common.utils;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.lyh.admin_template.back.modules.sms.entity.SmsResponse;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * sms 短信发送工具类
 */
@Data
@Component
public class SmsUtil {
    @Value("${aliyun.accessKeyId}")
    private String accessKeyId;
    @Value("${aliyun.accessKeySecret}")
    private String accessKeySecret;
    @Value("${aliyun.signName}")
    private String signName;
    @Value("${aliyun.templateCode}")
    private String templateCode;
    @Value("${aliyun.regionId}")
    private String regionId;
    private final static String OK = "OK";

    /**
     * 发送短信
     */
    public String sendSms(String phoneNumbers) {
        if (StringUtils.isEmpty(phoneNumbers)) {
            return null;
        }
        DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        // 固定参数,无需修改
        request.setSysMethod(MethodType.POST);
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        request.putQueryParameter("RegionId", regionId);

        // 设置手机号
        request.putQueryParameter("PhoneNumbers", phoneNumbers);
        // 设置签名模板
        request.putQueryParameter("SignName", signName);
        // 设置短信模板
        request.putQueryParameter("TemplateCode", templateCode);
        // 设置短信验证码
        String code = getCode();
        request.putQueryParameter("TemplateParam", "{/"code/":" + code +"}");
        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            // 转换返回的数据(需引入 Gson 依赖)
            SmsResponse smsResponse = GsonUtil.fromJson(response.getData(), SmsResponse.class);
            // 当 message 与 code 均为 ok 时,短信发送成功、否则失败
            if (SmsUtil.OK.equals(smsResponse.getMessage()) && SmsUtil.OK.equals(smsResponse.getCode())) {
                return code;
            }
            return null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取 6 位验证码
     */
    public String getCode() {
        return String.valueOf((int)((Math.random()*9+1)*100000));
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

5、完善三种登录方式

(1)三种登录方式:

密码登录:

用户名 + 密码。

手机号 + 密码。

验证码登录:

手机号 + 验证码。

(2)定义相关 vo 类 以及 进行 国际化、JSR303 处理

定义 vo(viewObject)实体类去接收数据,并对其进行 JSR303 校验,当然国际化也得一起处理。

国际化数据如下:

详细使用请参考: https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_4

【en】
sys.user.name.notEmpty=Sys user name cannot be null
sys.user.phone.notEmpty=Sys user mobile cannot be null
sys.user.password.notEmpty=Sys user password cannot be null
sys.user.code.notEmpty=Sys user code cannot be null
sys.user.phone.format.error=Sys user mobile format error
sys.user.name.format.error=Sys user name format error

【zh】
sys.user.name.notEmpty=用户名不能为空
sys.user.phone.notEmpty=用户手机号不能为空
sys.user.password.notEmpty=用户密码不能为空
sys.user.code.notEmpty=验证码不能为空
sys.user.phone.format.error=用户手机号格式错误
sys.user.name.format.error=用户名格式错误

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

vo 以及 JSR303 数据校验如下:

定义分组,用于不同场景的数据校验(不定义也行)。

详细使用可参考: https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_2

【LoginGroup】
package com.lyh.admin_template.back.common.validator.group.sys;

/**
 * 新增登录的 Group 校验规则
 */
public interface LoginGroup {
}

【RegisterGroup】
package com.lyh.admin_template.back.common.validator.group.sys;

/**
 * 新增注册的 Group 校验规则
 */
public interface RegisterGroup {
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

为了逻辑看起来简单,此处使用了三种 vo 分别接受不同场景下的登录数据。

三种 vo 如下:

【用户名 + 密码】
package com.lyh.admin_template.back.modules.sys.vo;

import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
import lombok.Data;

import javax.validation.constraints.NotEmpty;

/**
 * 登录时的视图数据类(view object),
 * 用于接收使用 用户名 + 密码 登陆的数据与操作。
 */
@Data
public class NamePwdLoginVo {
    @NotEmpty(message = "{sys.user.name.notEmpty}", groups = {LoginGroup.class})
    private String userName;
    @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {LoginGroup.class})
    private String password;
}

【手机号 + 密码】
package com.lyh.admin_template.back.modules.sys.vo;

import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

/**
 * 登录时的视图数据类(view object),
 * 用于接收使用 手机号 + 密码 登陆的数据与操作。
 */
@Data
public class PhonePwdLoginVo {
    @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {LoginGroup.class})
    @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {LoginGroup.class})
    private String phone;
    @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {LoginGroup.class})
    private String password;
}


【手机号 + 验证码】
package com.lyh.admin_template.back.modules.sys.vo;

import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

/**
 * 登录时的视图数据类(view object),
 * 用于接收使用 手机号 + 验证码 登陆的数据与操作。
 */
@Data
public class PhoneCodeLoginVo {
    @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {LoginGroup.class})
    @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {LoginGroup.class})
    private String phone;
    @NotEmpty(message = "{sys.user.code.notEmpty}", groups = {LoginGroup.class})
    private String code;
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

定义一个 vo,用于存储 jwt 自定义数据。

package com.lyh.admin_template.back.modules.sys.vo;

import lombok.Data;

/**
 * 保存 JWT 对应存储的数据
 */
@Data
public class JwtVo {
    // 保存用户 ID
    private Long id;
    // 保存用户名
    private String name;
    // 保存用户手机号
    private String phone;
    // 保存 JWT 创建时间戳
    private Long time;
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

(3)密码登录

主要流程:

接收数据,并对数据校验,对通过校验的数据进行操作。

根据数据去数据库查找数据,若查找失败,则返回相关异常数据。若存在数据,进行下面操作。

使用 JWT 工具类将相关数据封装,并存放在 redis 中,其中以数据 ID 为 key,jwt 为 value。

最后将 jwt 数据返回,命名为 token(前台接收数据并保存,一般存放于 cookie 的 header )。

jwt 与 redis 逻辑需要注意一下:

由于此项目中只允许某用户同时登陆系统的人数为 1,即某用户多次登录时,后一次登录的 jwt 需要替换掉 redis 中的 jwt,并发操作执行可能导致 后一次 jwt 的生成时机 在 redis 中 jwt 之前,直接替换会使最新的登录者被剔除,所以每次登录操作不能直接替换掉 redis 中的 jwt。

每次登录前,生成 jwt 后,应该去查询 redis 中是否存在对应的 jwt,如果不存在,则直接将当前 jwt 存入 redis 中,如果存在,则比较两个 jwt 的时间戳,若 redis 中 jwt 大于当前 jwt,则当前登录失败,否则将当前 jwt 存入 redis 中。

后台代码实现如下:(前台代码后续再整合)

package com.lyh.admin_template.back.modules.sys.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lyh.admin_template.back.common.utils.*;
import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
import com.lyh.admin_template.back.modules.sys.entity.SysUser;
import com.lyh.admin_template.back.modules.sys.service.SysUserService;
import com.lyh.admin_template.back.modules.sys.vo.JwtVo;
import com.lyh.admin_template.back.modules.sys.vo.NamePwdLoginVo;
import com.lyh.admin_template.back.modules.sys.vo.PhonePwdLoginVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * <p>
 * 系统用户表 前端控制器
 * </p>
 *
 * @author lyh
 * @since 2020-07-02
 */
@RestController
@RequestMapping("/sys/sys-user")
@Api(tags = "用户登录、注册操作")
public class SysUserController {

    /**
     * 用于操作 sys_user 表
     */
    @Autowired
    private SysUserService sysUserService;
    /**
     * 用于操作 redis
     */
    @Autowired
    private RedisUtil redisUtil;
    /**
     * 常量,表示用户密码登录操作
     */
    private static final String USER_NAME_STATUS = "0";
    /**
     * 常量,表示手机号密码登录操作
     */
    private static final String PHONE_STATUS = "1";

    /**
     * 获取 jwt
     * @return jwt
     */
    private String getJwt(SysUser sysUser) {
        // 获取需要保存在 jwt 中的数据
        JwtVo jwtVo = new JwtVo();
        jwtVo.setId(sysUser.getId());
        jwtVo.setName(sysUser.getName());
        jwtVo.setPhone(sysUser.getMobile());
        jwtVo.setTime(new Date().getTime());
        // 获取 jwt 数据,设置过期时间为 30 分钟
        String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30);
        // 判断用户是否重复登录(code 有值则重复登录,需要保留最新的登录者,剔除前一个登录者)
        String code = redisUtil.get(String.valueOf(sysUser.getId()));
        // 获取当前时间戳
        Long currentTime = new Date().getTime();
        // 如果 redis 中存在 jwt 数据,则根据时间戳比较谁为最新的登陆者
        if (StringUtils.isNotEmpty(code)) {
            // 获取 redis 中存储的 jwt 数据
            JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class);
            // redis jwt 大于 当前时间戳,则 redis 中 jwt 为最新登录者,当前登录失败
            if (redisJwt.getTime() > currentTime) {
                return null;
            }
        }
        // 把数据存放在 redis 中,设置过期时间为 30 分钟
        redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30);
        return jwt;
    }

    /**
     * 使用密码进行真实登录操作
     * @param account 账号(用户名或手机号)
     * @param pwd 密码
     * @param status 是否使用用户名登录(0 表示用户名登录,1 表示手机号登录)
     * @return jwt
     */
    private String pwdLogin(String account, String pwd, String status) {
        // 新增查询条件
        QueryWrapper queryWrapper = new QueryWrapper();
        // 如果是用户名 + 密码登录,则根据 姓名 + 密码 查找数据
        if (USER_NAME_STATUS.equals(status)) {
            queryWrapper.eq("name", account);
        }
        // 如果是手机号 + 密码登录,则根据 手机号 + 密码 查找数据
        if (PHONE_STATUS.equals(status)) {
            queryWrapper.eq("mobile", account);
        }
        // 添加密码条件,密码进行 MD5 加密后再与数据库数据比较
        queryWrapper.eq("password", MD5Util.encrypt(pwd));
        // 获取用户数据
        SysUser sysUser = sysUserService.getOne(queryWrapper);
        // 如果存在用户数据
        if (sysUser != null) {
            return getJwt(sysUser);
        }
        return null;
    }

    @ApiOperation(value = "使用用户名、密码登录")
    @PostMapping("/login/namePwdLogin")
    public Result namePwdLogin(@Validated({LoginGroup.class}) @RequestBody NamePwdLoginVo namePwdLoginVo) {
        String jwt = pwdLogin(namePwdLoginVo.getUserName(), namePwdLoginVo.getPassword(), USER_NAME_STATUS);
        if (StringUtils.isNotEmpty(jwt)) {
            return Result.ok().message("登录成功").data("token", jwt);
        }
        return Result.error().message("登录失败");
    }

    @ApiOperation(value = "使用手机号、密码登录")
    @PostMapping("/login/phonePwdLogin")
    public Result phonePwdLogin(@Validated({LoginGroup.class}) @RequestBody PhonePwdLoginVo phonePwdLoginVo) {
        String jwt = pwdLogin(phonePwdLoginVo.getPhone(), phonePwdLoginVo.getPassword(), PHONE_STATUS);
        if (StringUtils.isNotEmpty(jwt)) {
            return Result.ok().message("登录成功").data("token", jwt);
        }
        return Result.error().message("登录失败");
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

使用 swagger 简单测试一下:

点击用户名 + 密码登录,生成 token,存入 redis 中并设置过期时间 30 分钟(1800 秒)。

点击手机号 + 密码登录,会重新生成 token,并存入 redis 中。

并发操作,可以使用 Jmeter 进行测试(此处省略)。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

(4)验证码登录

获取验证码流程:

首先获取验证码(此处不考虑并发情况,毕竟手机号只有一个用户能用,应该避免重复获取验证码的情况),并将其存放与 redis 中,设置过期时间为 5 分钟。

为了避免重复获取验证码,可以根据其已过期时间是否小于 1 分钟判断,即 1 分钟内不可以重复获取验证码。

验证码登录流程:

接收数据,并校验数据,通过检验的数据进行下面处理。

先检查 redis 中是否存在验证码,若不存在验证码(验证码不存在或失效),则登录失败。否则,根据手机号去查询用户数据,生成 jwt,存放与 redis 中并返回。

后台代码实现如下:(前台代码后续再整合)

package com.lyh.admin_template.back.modules.sys.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lyh.admin_template.back.common.utils.*;
import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
import com.lyh.admin_template.back.modules.sys.entity.SysUser;
import com.lyh.admin_template.back.modules.sys.service.SysUserService;
import com.lyh.admin_template.back.modules.sys.vo.JwtVo;
import com.lyh.admin_template.back.modules.sys.vo.PhoneCodeLoginVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
 * <p>
 * 系统用户表 前端控制器
 * </p>
 *
 * @author lyh
 * @since 2020-07-02
 */
@RestController
@RequestMapping("/sys/sys-user")
@Api(tags = "用户登录、注册操作")
public class SysUserController {

    /**
     * 用于操作 sys_user 表
     */
    @Autowired
    private SysUserService sysUserService;
    /**
     * 用于操作 redis
     */
    @Autowired
    private RedisUtil redisUtil;
    /**
     * 用于操作 短信验证码发送
     */
    @Autowired
    private SmsUtil smsUtil;

    /**
     * 获取 jwt
     * @return jwt
     */
    private String getJwt(SysUser sysUser) {
        // 获取需要保存在 jwt 中的数据
        JwtVo jwtVo = new JwtVo();
        jwtVo.setId(sysUser.getId());
        jwtVo.setName(sysUser.getName());
        jwtVo.setPhone(sysUser.getMobile());
        jwtVo.setTime(new Date().getTime());
        // 获取 jwt 数据,设置过期时间为 30 分钟
        String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30);
        // 判断用户是否重复登录(code 有值则重复登录,需要保留最新的登录者,剔除前一个登录者)
        String code = redisUtil.get(String.valueOf(sysUser.getId()));
        // 获取当前时间戳
        Long currentTime = new Date().getTime();
        // 如果 redis 中存在 jwt 数据,则根据时间戳比较谁为最新的登陆者
        if (StringUtils.isNotEmpty(code)) {
            // 获取 redis 中存储的 jwt 数据
            JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class);
            // redis jwt 大于 当前时间戳,则 redis 中 jwt 为最新登录者,当前登录失败
            if (redisJwt.getTime() > currentTime) {
                return null;
            }
        }
        // 把数据存放在 redis 中,设置过期时间为 30 分钟
        redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30);
        return jwt;
    }

    /**
     * 使用 验证码进行真实登录操作
     * @param phone 手机号
     * @param code 验证码
     * @return jwt
     */
    private String codeLogin(String phone, String code) {
        // 获取 redis 中存放的验证码
        String redisCode = redisUtil.get(phone);
        // 存在验证码,且输入的验证码与 redis 存放的验证码相同,则根据手机号去数据库查询数据
        if (StringUtils.isNotEmpty(redisCode) && code.equals(redisCode)) {
            // 新增查询条件
            QueryWrapper queryWrapper = new QueryWrapper();
            // 根据手机号去查询数据
            queryWrapper.eq("mobile", phone);
            SysUser sysUser = sysUserService.getOne(queryWrapper);
            // 如果存在用户数据
            if (sysUser != null) {
                return getJwt(sysUser);
            }
        }
        return null;
    }

    @ApiOperation(value = "使用手机号、验证码登录")
    @PostMapping("/login/phoneCodeLogin")
    public Result phoneCodeLogin(@Validated({LoginGroup.class}) @RequestBody PhoneCodeLoginVo phoneCodeLoginVo) {
        String jwt = codeLogin(phoneCodeLoginVo.getPhone(), phoneCodeLoginVo.getCode());
        if (StringUtils.isNotEmpty(jwt)) {
            return Result.ok().message("登录成功").data("token", jwt);
        }
        return Result.error().message("登录失败");
    }

    @ApiOperation(value = "获取短信验证码")
    @GetMapping("/login/getCode")
    public Result getCode(String phone) {
        // 设置默认过期时间
        Long defaultTime = 60L * 5;
        // 先判断 redis 中是否存储过验证码(设置期限为 1 分钟),防止重复获取验证码
        Long expire = redisUtil.getExpire(phone);
        if (expire != null && (defaultTime - expire < 60)) {
            return Result.error().message("验证码已发送,1 分钟后可再次获取验证码");
        } else {
            // 获取 短信验证码
            String code = smsUtil.sendSms(phone);
            if (StringUtils.isNotEmpty(code)) {
                // 把验证码存放在 redis 中,并设置 过期时间 为 5 分钟
                redisUtil.set(phone, code, defaultTime);
                return Result.ok().message("验证码获取成功").data("code", code);
            }
        }
        return Result.error().message("验证码获取失败");
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

使用 swagger 简单测试一下:

首先获取验证码,其会存放于 redis 中,过期时间为 5 分钟(300 秒)。若 1 分钟内重复点击验证码,会提示相关信息(验证码已发送,1 分钟后再次获取)。

然后根据 手机号和验证码进行登录操作。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

6、完善注册逻辑

(1)主要流程:

先获取验证码,验证码处理与验证码登录相同(此处不再重复)。

输入用户名、密码、手机号、以及得到的验证码,后端对数据进行校验,校验通过的数据进行下面操作。

先检查 redis 中是否存在验证码,若不存在验证码(验证码不存在或失效)或者验证码与当前验证码不同,则注册失败,如存在且相同,则进行下面操作。

根据用户名与手机号,对数据库数据进行查找,若存在数据则注册失败,若不存在,则向数据库添加数据。由于给用户名和手机号添加了唯一性约束,所以可以直接进行插入操作,存在数据会返回异常,不存在数据会直接插入。

(2)代码实现如下:

首先定义一个 vo 类,用于接收数据。

package com.lyh.admin_template.back.modules.sys.vo;

import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup;
import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

/**
 * 注册时对应的视图数据类(view object),
 * 用于接收并处理 注册时的数据。
 */
@Data
public class RegisterVo {
    @NotEmpty(message = "{sys.user.name.notEmpty}", groups = {RegisterGroup.class})
    @Pattern(message = "{sys.user.name.format.error}", regexp = "^.*[^//d].*$", groups = {RegisterGroup.class})
    private String userName;
    @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {RegisterGroup.class})
    private String password;
    @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {RegisterGroup.class})
    @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {RegisterGroup.class})
    private String phone;
    @NotEmpty(message = "{sys.user.code.notEmpty}", groups = {RegisterGroup.class})
    private String code;
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

接口如下:

由于 注册 用户均属于 普通用户,所以注册的同时需要给其绑定角色,即向 sys_user 插入数据后,还需要向 sys_user_role 插入数据(需要使用代码生成器生成相关代码,此处省略)。

由于出现多表插入操作,此处使用 @Transactional 对事务进行控制。

注:

@Transactional 需要写在 Service 层,写在 Controller 层不生效。

在 service 层定义一个 saveUser 方法。

package com.lyh.admin_template.back.modules.sys.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.lyh.admin_template.back.modules.sys.entity.SysUser;

/**
 * <p>
 * 系统用户表 服务类
 * </p>
 *
 * @author lyh
 * @since 2020-07-02
 */
public interface SysUserService extends IService<SysUser> {
    public boolean saveUser(SysUser sysUser);
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

在 service 实现类中,重写方法并完善注册逻辑。

package com.lyh.admin_template.back.modules.sys.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lyh.admin_template.back.modules.sys.entity.SysRole;
import com.lyh.admin_template.back.modules.sys.entity.SysUser;
import com.lyh.admin_template.back.modules.sys.entity.SysUserRole;
import com.lyh.admin_template.back.modules.sys.mapper.SysUserMapper;
import com.lyh.admin_template.back.modules.sys.service.SysRoleService;
import com.lyh.admin_template.back.modules.sys.service.SysUserRoleService;
import com.lyh.admin_template.back.modules.sys.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * 系统用户表 服务实现类
 * </p>
 *
 * @author lyh
 * @since 2020-07-02
 */
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysUserRoleService sysUserRoleService;

    /**
     * 先插入数据到 用户表 sys_user 中。
     * 再获取数据 ID 与 角色 ID 并插入到 用户角色表 sys_user_role 中。
     * @param sysUser 用户数据
     * @return true 表示插入成功, false 表示失败
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,timeout=36000,rollbackFor=Exception.class)
    public boolean saveUser(SysUser sysUser) {
        // 向 sys_user 表中插入数据
        if (this.save(sysUser)) {
            // 获取当前用户的 ID
            QueryWrapper queryWrapper = new QueryWrapper();
            queryWrapper.eq("name", sysUser.getName());
            SysUser sysUser2 = this.getOne(queryWrapper);

            // 获取普通用户角色 ID
            QueryWrapper queryWrapper2 = new QueryWrapper();
            queryWrapper2.eq("role_name", "user");
            SysRole sysRole = sysRoleService.getOne(queryWrapper2);

            // 插入到 用户-角色 表中(sys_user_role)
            SysUserRole sysUserRole = new SysUserRole();
            sysUserRole.setUserId(sysUser2.getId()).setRoleId(sysRole.getId());
            return sysUserRoleService.save(sysUserRole);
        }
        return false;
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

controller 层接口如下:

package com.lyh.admin_template.back.modules.sys.controller;


import com.lyh.admin_template.back.common.utils.MD5Util;
import com.lyh.admin_template.back.common.utils.RedisUtil;
import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.common.utils.SmsUtil;
import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup;
import com.lyh.admin_template.back.modules.sys.entity.SysUser;
import com.lyh.admin_template.back.modules.sys.service.SysUserService;
import com.lyh.admin_template.back.modules.sys.vo.RegisterVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * 系统用户表 前端控制器
 * </p>
 *
 * @author lyh
 * @since 2020-07-02
 */
@RestController
@RequestMapping("/sys/sys-user")
@Api(tags = "用户登录、注册操作")
public class SysUserController {

    /**
     * 用于操作 sys_user 表
     */
    @Autowired
    private SysUserService sysUserService;
    /**
     * 用于操作 redis
     */
    @Autowired
    private RedisUtil redisUtil;
    /**
     * 用于操作 短信验证码发送
     */
    @Autowired
    private SmsUtil smsUtil;

    @ApiOperation(value = "获取短信验证码")
    @GetMapping("/login/getCode")
    public Result getCode(String phone) {
        // 设置默认过期时间
        Long defaultTime = 60L * 5;
        // 先判断 redis 中是否存储过验证码(设置期限为 1 分钟),防止重复获取验证码
        Long expire = redisUtil.getExpire(phone);
        if (expire != null && (defaultTime - expire < 60)) {
            return Result.error().message("验证码已发送,1 分钟后可再次获取验证码");
        } else {
            // 获取 短信验证码
            String code = smsUtil.sendSms(phone);
            if (StringUtils.isNotEmpty(code)) {
                // 把验证码存放在 redis 中,并设置 过期时间 为 5 分钟
                redisUtil.set(phone, code, defaultTime);
                return Result.ok().message("验证码获取成功").data("code", code);
            }
        }
        return Result.error().message("验证码获取失败");
    }

    @ApiOperation(value = "用户注册")
    @PostMapping("/register")
    public Result register(@Validated({RegisterGroup.class}) @RequestBody RegisterVo registerVo) {
        if (save(registerVo)) {
            return Result.ok().message("用户注册成功");
        }
        return Result.error().message("用户注册失败");
    }

    /**
     * 真实注册操作
     * @param registerVo 注册数据
     * @return true 为插入成功, false 为失败
     */
    public boolean save(RegisterVo registerVo) {
        // 判断 redis 中是否存在 验证码
        String code = redisUtil.get(registerVo.getPhone());
        // redis 中存在验证码且与当前验证码相同
        if (StringUtils.isNotEmpty(code) && code.equals(registerVo.getCode())) {
            SysUser sysUser = new SysUser();
            sysUser.setName(registerVo.getUserName()).setPassword(MD5Util.encrypt(registerVo.getPassword()));
            sysUser.setMobile(registerVo.getPhone());
            return sysUserService.saveUser(sysUser);
        }
        return false;
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

使用 swagger 简单测试一下,添加数据。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

7、完善登出逻辑

(1)目的:

让客户端 保存的 token 失效,则用户再次访问系统后由于 token 失效而无法继续访问,需重新登录后才可访问。

后台操作(非必须操作):

返回一个 过期时间为 1 秒的 token(或返回一个无效 token),并删除 redis 中的 token。

前台操作:

前台保存无效的 token。

清除 token(简单粗暴)。

(2)代码如下:(仅后台代码,前台代码此处省略、后续整合)

package com.lyh.admin_template.back.modules.sys.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lyh.admin_template.back.common.utils.JwtUtil;
import com.lyh.admin_template.back.common.utils.RedisUtil;
import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.modules.sys.entity.SysUser;
import com.lyh.admin_template.back.modules.sys.service.SysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 * 系统用户表 前端控制器
 * </p>
 *
 * @author lyh
 * @since 2020-07-02
 */
@RestController
@RequestMapping("/sys/sys-user")
@Api(tags = "用户登录、注册操作")
public class SysUserController {

    /**
     * 用于操作 sys_user 表
     */
    @Autowired
    private SysUserService sysUserService;
    /**
     * 用于操作 redis
     */
    @Autowired
    private RedisUtil redisUtil;

    @ApiOperation(value = "用户登出")
    @GetMapping("/logout")
    public Result logout(@RequestParam String userName) {
        // 先获取用户数据
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("name", userName);
        SysUser sysUser = sysUserService.getOne(queryWrapper);
        // 用户存在时
        if (sysUser != null) {
            // 生成并返回一个无效的 token
            String jwt = JwtUtil.getJwtToken(null, 1000L);
            // 删除 redis 中的 token
            redisUtil.del(String.valueOf(sysUser.getId()));
            return Result.ok().message("登出成功").data("token", jwt);
        }
        return Result.error().message("登出失败");
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

使用 swagger 简单测试一下:

某用户登录后,会返回一个有效 token,并在 redis 中保存。

用户登出后,返回一个无效 token,并删除 redis 中数据。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

8、完整的登录、注册、登出接口代码

包括三种登录接口、注册接口、登出接口、获取验证码接口。

package com.lyh.admin_template.back.modules.sys.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lyh.admin_template.back.common.utils.*;
import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup;
import com.lyh.admin_template.back.modules.sys.entity.SysUser;
import com.lyh.admin_template.back.modules.sys.service.SysUserService;
import com.lyh.admin_template.back.modules.sys.vo.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
 * <p>
 * 系统用户表 前端控制器
 * </p>
 *
 * @author lyh
 * @since 2020-07-02
 */
@RestController
@RequestMapping("/sys/sys-user")
@Api(tags = "用户登录、注册操作")
public class SysUserController {

    /**
     * 用于操作 sys_user 表
     */
    @Autowired
    private SysUserService sysUserService;
    /**
     * 用于操作 redis
     */
    @Autowired
    private RedisUtil redisUtil;
    /**
     * 用于操作 短信验证码发送
     */
    @Autowired
    private SmsUtil smsUtil;
    /**
     * 常量,表示用户密码登录操作
     */
    private static final String USER_NAME_STATUS = "0";
    /**
     * 常量,表示手机号密码登录操作
     */
    private static final String PHONE_STATUS = "1";

    /**
     * 获取 jwt
     * @return jwt
     */
    private String getJwt(SysUser sysUser) {
        // 获取需要保存在 jwt 中的数据
        JwtVo jwtVo = new JwtVo();
        jwtVo.setId(sysUser.getId());
        jwtVo.setName(sysUser.getName());
        jwtVo.setPhone(sysUser.getMobile());
        jwtVo.setTime(new Date().getTime());
        // 获取 jwt 数据,设置过期时间为 30 分钟
        String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30);
        // 判断用户是否重复登录(code 有值则重复登录,需要保留最新的登录者,剔除前一个登录者)
        String code = redisUtil.get(String.valueOf(sysUser.getId()));
        // 获取当前时间戳
        Long currentTime = new Date().getTime();
        // 如果 redis 中存在 jwt 数据,则根据时间戳比较谁为最新的登陆者
        if (StringUtils.isNotEmpty(code)) {
            // 获取 redis 中存储的 jwt 数据
            JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class);
            // redis jwt 大于 当前时间戳,则 redis 中 jwt 为最新登录者,当前登录失败
            if (redisJwt.getTime() > currentTime) {
                return null;
            }
        }
        // 把数据存放在 redis 中,设置过期时间为 30 分钟
        redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30);
        return jwt;
    }

    /**
     * 使用密码进行真实登录操作
     * @param account 账号(用户名或手机号)
     * @param pwd 密码
     * @param status 是否使用用户名登录(0 表示用户名登录,1 表示手机号登录)
     * @return jwt
     */
    private String pwdLogin(String account, String pwd, String status) {
        // 新增查询条件
        QueryWrapper queryWrapper = new QueryWrapper();
        // 如果是用户名 + 密码登录,则根据 姓名 + 密码 查找数据
        if (USER_NAME_STATUS.equals(status)) {
            queryWrapper.eq("name", account);
        }
        // 如果是手机号 + 密码登录,则根据 手机号 + 密码 查找数据
        if (PHONE_STATUS.equals(status)) {
            queryWrapper.eq("mobile", account);
        }
        // 添加密码条件,密码进行 MD5 加密后再与数据库数据比较
        queryWrapper.eq("password", MD5Util.encrypt(pwd));
        // 获取用户数据
        SysUser sysUser = sysUserService.getOne(queryWrapper);
        // 如果存在用户数据
        if (sysUser != null) {
            return getJwt(sysUser);
        }
        return null;
    }

    /**
     * 使用 验证码进行真实登录操作
     * @param phone 手机号
     * @param code 验证码
     * @return jwt
     */
    private String codeLogin(String phone, String code) {
        // 获取 redis 中存放的验证码
        String redisCode = redisUtil.get(phone);
        // 存在验证码,且输入的验证码与 redis 存放的验证码相同,则根据手机号去数据库查询数据
        if (StringUtils.isNotEmpty(redisCode) && code.equals(redisCode)) {
            // 新增查询条件
            QueryWrapper queryWrapper = new QueryWrapper();
            // 根据手机号去查询数据
            queryWrapper.eq("mobile", phone);
            SysUser sysUser = sysUserService.getOne(queryWrapper);
            // 如果存在用户数据
            if (sysUser != null) {
                return getJwt(sysUser);
            }
        }
        return null;
    }

    @ApiOperation(value = "使用用户名、密码登录")
    @PostMapping("/login/namePwdLogin")
    public Result namePwdLogin(@Validated({LoginGroup.class}) @RequestBody NamePwdLoginVo namePwdLoginVo) {
        String jwt = pwdLogin(namePwdLoginVo.getUserName(), namePwdLoginVo.getPassword(), USER_NAME_STATUS);
        if (StringUtils.isNotEmpty(jwt)) {
            return Result.ok().message("登录成功").data("token", jwt);
        }
        return Result.error().message("登录失败").code(HttpStatus.SC_UNAUTHORIZED);
    }

    @ApiOperation(value = "使用手机号、密码登录")
    @PostMapping("/login/phonePwdLogin")
    public Result phonePwdLogin(@Validated({LoginGroup.class}) @RequestBody PhonePwdLoginVo phonePwdLoginVo) {
        String jwt = pwdLogin(phonePwdLoginVo.getPhone(), phonePwdLoginVo.getPassword(), PHONE_STATUS);
        if (StringUtils.isNotEmpty(jwt)) {
            return Result.ok().message("登录成功").data("token", jwt);
        }
        return Result.error().message("登录失败").code(HttpStatus.SC_UNAUTHORIZED);
    }

    @ApiOperation(value = "使用手机号、验证码登录")
    @PostMapping("/login/phoneCodeLogin")
    public Result phoneCodeLogin(@Validated({LoginGroup.class}) @RequestBody PhoneCodeLoginVo phoneCodeLoginVo) {
        String jwt = codeLogin(phoneCodeLoginVo.getPhone(), phoneCodeLoginVo.getCode());
        if (StringUtils.isNotEmpty(jwt)) {
            return Result.ok().message("登录成功").data("token", jwt);
        }
        return Result.error().message("登录失败").code(HttpStatus.SC_UNAUTHORIZED);
    }

    @ApiOperation(value = "获取短信验证码")
    @GetMapping("/login/getCode")
    public Result getCode(String phone) {
        // 设置默认过期时间
        Long defaultTime = 60L * 5;
        // 先判断 redis 中是否存储过验证码(设置期限为 1 分钟),防止重复获取验证码
        Long expire = redisUtil.getExpire(phone);
        if (expire != null && (defaultTime - expire < 60)) {
            return Result.error().message("验证码已发送,1 分钟后可再次获取验证码");
        } else {
            // 获取 短信验证码
            String code = smsUtil.sendSms(phone);
            if (StringUtils.isNotEmpty(code)) {
                // 把验证码存放在 redis 中,并设置 过期时间 为 5 分钟
                redisUtil.set(phone, code, defaultTime);
                return Result.ok().message("验证码获取成功").data("code", code);
            }
        }
        return Result.error().message("验证码获取失败");
    }

    @ApiOperation(value = "用户登出")
    @GetMapping("/logout")
    public Result logout(@RequestParam String userName) {
        // 先获取用户数据
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("name", userName);
        SysUser sysUser = sysUserService.getOne(queryWrapper);
        // 用户存在时
        if (sysUser != null) {
            // 生成并返回一个无效的 token
            String jwt = JwtUtil.getJwtToken(null, 1000L);
            // 删除 redis 中的 token
            redisUtil.del(String.valueOf(sysUser.getId()));
            return Result.ok().message("登出成功").data("token", jwt);
        }
        return Result.error().message("登出失败");
    }

    @ApiOperation(value = "用户注册")
    @PostMapping("/register")
    public Result register(@Validated({RegisterGroup.class}) @RequestBody RegisterVo registerVo) {
        if (save(registerVo)) {
            return Result.ok().message("用户注册成功");
        }
        return Result.error().message("用户注册失败").code(HttpStatus.SC_UNAUTHORIZED);
    }

    /**
     * 真实注册操作
     * @param registerVo 注册数据
     * @return true 为插入成功, false 为失败
     */
    public boolean save(RegisterVo registerVo) {
        // 判断 redis 中是否存在 验证码
        String code = redisUtil.get(registerVo.getPhone());
        // redis 中存在验证码且与当前验证码相同
        if (StringUtils.isNotEmpty(code) && code.equals(registerVo.getCode())) {
            SysUser sysUser = new SysUser();
            sysUser.setName(registerVo.getUserName()).setPassword(MD5Util.encrypt(registerVo.getPassword()));
            sysUser.setMobile(registerVo.getPhone());
            return sysUserService.saveUser(sysUser);
        }
        return false;
    }
}

9、定义一个拦截器,用于拦截除登录注册请求外的所有请求

(1)目的:

由于采用 JWT 进行单点登录,每次请求前都需要对 token 进行校验,为了避免在接口中重复进行校验操作,此处可以使用拦截器,拦截每个请求,校验通过后放行请求并返回数据,校验未通过直接返回错误数据。

拦截器需要直接放行登录、注册等请求,未登录、注册时没有 token 数据,只有登录后才有 token 数据,拦截了 登录、注册请求后,不会产生 token,成为一个死循环。

(2)代码实现如下:

Step1:定义一个拦截器

对于拦截的请求,首先检查 token 是否过期,过期返回 401 状态码。未过期进行下面操作。

获取 token 信息,并根据 token 的 id 值从 redis 中获取 redis 中存储的 token。若 redis 中不存在 token,即用户未登录,返回 401 状态码。存在 token 则进行下面操作。

若两 token 相同,即 同一用户再次访问系统,放行该请求。token 不同,则意味着 同一用户 在不同地方进行登录,需保留最新的登录者信息。根据时间戳比较,谁大谁为最新登录者,并将其值保存在 redis 中。

/**
 * 定义一个拦截器,用于拦截请求,并对 JWT 进行验证
 */
class JWTInterceptor extends HandlerInterceptorAdapter {

    /**
     * 访问 controller 前被调用
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取 token(从 header 或者 参数中获取)
        String token = request.getHeader("token");
        if (StringUtils.isBlank(token)) {
            token = request.getParameter("token");
        }
        // 验证 token 是否过期(根据时间戳比较)
        if (JwtUtil.checkToken(token)) {
            // 获取 token 中的数据
            Claims claims = JwtUtil.getTokenBody(token);
            System.out.println(claims.getExpiration());
            JwtVo jwt = GsonUtil.fromJson(String.valueOf(claims.get("data")), JwtVo.class);
            // 获取 redis 中存储的 token
            String redisToken = redisUtil.get(String.valueOf(jwt.getId()));
            // 当前 token 与 redis 中存储的 token 进行比较
            if (StringUtils.isNotEmpty(redisToken)) {
                // 获取 redis 中 token 的数据
                JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(redisToken).get("data")), JwtVo.class);
                // 若两者 token 相同,则为同一用户再次访问系统,放行
                if (redisToken.equals(token)) {
                    return true;
                } else if (redisJwt.getTime() <= jwt.getTime()){
                    // redis 中 token 生成时间戳 小于等于 当前 token 生成时间戳,即当前用户为最新登录者
                    // redis 保存当前最新的 token,并放行
                    redisUtil.set(String.valueOf(redisJwt.getId()), token, 60 * 30);
                    return true;
                }
            }
        }
        // 认证失败,返回数据,并返回 401 状态码
        returnJsonData(response);
        return false;
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

Step2:定义拦截请求后的数据返回结果。

返回 json 数据,并定义 code 为 401(授权失败)。

/**
 * 返回 json 格式的数据
 */
public void returnJsonData(HttpServletResponse response) {
    PrintWriter pw = null;
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");
    try {
        pw = response.getWriter();
        // 返回 code 为 401,表示 token 失效。
        pw.print(GsonUtil.toJson(Result.error().message("token 失效或过期").code(HttpStatus.SC_UNAUTHORIZED)));
    } catch (IOException e) {
        log.error(e.getMessage());
        throw new RuntimeException(e);
    }
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

Step3:定义拦截请求规则:

/**
 * 定义拦截器,拦截请求。
 * 其中:
 *      addPathPatterns 用于添加需要拦截的请求。
 *      excludePathPatterns 用于添加不需要拦截的请求。
 * 此处:
 *      拦截所有请求,但是排除 登录、注册 请求 以及 swagger 请求。
 */
@Bean(name = "JWTInterceptor")
public WebMvcConfigurer JWTInterceptor() {
    return new WebMvcConfigurer() {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new JWTInterceptor())
                // 拦截所有请求
                .addPathPatterns("/**")
                // 不拦截 登录、注册、忘记密码请求
                .excludePathPatterns("/sys/sys-user/login/*", "/sys/sys-user/register")
                // 不拦截 swagger 请求
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
        }
    };
}

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

完整拦截逻辑:

package com.lyh.admin_template.back.common.config;

import com.lyh.admin_template.back.common.utils.GsonUtil;
import com.lyh.admin_template.back.common.utils.JwtUtil;
import com.lyh.admin_template.back.common.utils.RedisUtil;
import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.modules.sys.vo.JwtVo;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Slf4j
@Configuration
public class JWTConfig {

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 定义拦截器,拦截请求。
     * 其中:
     *      addPathPatterns 用于添加需要拦截的请求。
     *      excludePathPatterns 用于添加不需要拦截的请求。
     * 此处:
     *      拦截所有请求,但是排除 登录、注册 请求 以及 swagger 请求。
     */
    @Bean(name = "JWTInterceptor")
    public WebMvcConfigurer JWTInterceptor() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(new JWTInterceptor())
                    // 拦截所有请求
                    .addPathPatterns("/**")
                    // 不拦截 登录、注册、忘记密码请求
                    .excludePathPatterns("/sys/sys-user/login/*", "/sys/sys-user/register")
                    // 不拦截 swagger 请求
                    .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
            }
        };
    }

    /**
     * 定义一个拦截器,用于拦截请求,并对 JWT 进行验证
     */
    class JWTInterceptor extends HandlerInterceptorAdapter {

        /**
         * 访问 controller 前被调用
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 获取 token(从 header 或者 参数中获取)
            String token = request.getHeader("token");
            if (StringUtils.isBlank(token)) {
                token = request.getParameter("token");
            }
            // 验证 token 是否过期(根据时间戳比较)
            if (JwtUtil.checkToken(token)) {
                // 获取 token 中的数据
                Claims claims = JwtUtil.getTokenBody(token);
                JwtVo jwt = GsonUtil.fromJson(String.valueOf(claims.get("data")), JwtVo.class);
                // 获取 redis 中存储的 token
                String redisToken = redisUtil.get(String.valueOf(jwt.getId()));
                // 当前 token 与 redis 中存储的 token 进行比较
                if (StringUtils.isNotEmpty(redisToken)) {
                    // 获取 redis 中 token 的数据
                    JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(redisToken).get("data")), JwtVo.class);
                    // 若两者 token 相同,则为同一用户再次访问系统,放行
                    if (redisToken.equals(token)) {
                        return true;
                    } else if (redisJwt.getTime() <= jwt.getTime()){
                        // redis 中 token 生成时间戳 小于等于 当前 token 生成时间戳,即当前用户为最新登录者
                        // redis 保存当前最新的 token,并放行
                        redisUtil.set(String.valueOf(redisJwt.getId()), token, 60 * 30);
                        return true;
                    }
                }
            }
            // 认证失败,返回数据,并返回 401 状态码
            returnJsonData(response);
            return false;
        }
    }

    /**
     * 返回 json 格式的数据
     */
    public void returnJsonData(HttpServletResponse response) {
        PrintWriter pw = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            pw = response.getWriter();
            // 返回 code 为 401,表示 token 失效。
            pw.print(GsonUtil.toJson(Result.error().message("token 失效或过期").code(HttpStatus.SC_UNAUTHORIZED)));
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

10、给 Swagger 添加统一验证参数(设置 token)

(1)目的:

由于后台使用过滤器拦截了请求,使用 swagger 测试时,由于未携带 token 而被拦截,导致 返回 401 状态码。

可以给 Swagger 添加统一验证参数,在请求发送前统一给 header 加上 token 参数。

(2)代码实现:

来源于网络,没有深究为什么这么写,套用即可。

在原本 swagger 基础上,添加如下代码:

securitySchemes(security())

securityContexts(securityContexts());

package com.lyh.admin_template.back.common.config;

import com.google.common.collect.Lists;
import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

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

@Configuration
@EnableSwagger2
@Profile({"dev","test"})
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // 加了ApiOperation注解的类,才会生成接口文档
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                // 指定包下的类,才生成接口文档
                .apis(RequestHandlerSelectors.basePackage("com.lyh.admin_template.back"))
                .paths(PathSelectors.any())
                .build()
                .securitySchemes(security())
                .securityContexts(securityContexts());
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Swagger 测试")
                .description("Swagger 测试文档")
                .termsOfServiceUrl("https://www.cnblogs.com/l-y-h/")
                .version("1.0.0")
                .build();
    }

    private List<ApiKey> security() {
        return Lists.newArrayList(
                new ApiKey("token", "token", "header")
        );
    }

    private List<SecurityContext> securityContexts() {
        return Lists.newArrayList(
                SecurityContext.builder().securityReferences(defaultAuth())
                    //过滤要验证的路径
                    .forPaths(PathSelectors.regex("^(?!auth).*$"))
                    .build()
        );
    }

    //增加全局认证
    List<SecurityReference> defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        List<SecurityReference> securityReferences = new ArrayList<>();
        // 由于 securitySchemes() 方法中 header 写入值为 token,所以此处为 token
        securityReferences.add(new SecurityReference("token", authorizationScopes));
        return securityReferences;
    }
}

(3)简单测试一下:

首先登录,获取到 token。没有设置 token 时,访问 登出接口 会被拦截。

设置 token 后,登出接口不会被拦截。

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、red...

原文  http://www.cnblogs.com/l-y-h/p/13264307.html
正文到此结束
Loading...