(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', '', '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', '', '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', '', '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 用户表---------------------------------------

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', '', '超级管理员','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755452551, 'admin', '', '普通管理员','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755458779, 'user', '', '普通用户','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_role 角色表---------------------------------------

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 菜单权限表---------------------------------------

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, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755453544, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755454664, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_user_role 用户角色表---------------------------------------

(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, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461112, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461113, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461114, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461115, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461116, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461117, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461118, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461119, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461120, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461121, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461122, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461123, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461124, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461125, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461126, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461127, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461128, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461129, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462111, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462112, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462113, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462114, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462115, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462116, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462117, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462118, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462119, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462120, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755463111, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755463112, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755463113, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_role_menu 系统角色菜单表---------------------------------------

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', '', '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', '', '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', '', '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', '', '超级管理员','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755452551, 'admin', '', '普通管理员','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755458779, 'user', '', '普通用户','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, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755453544, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755454664, '', '', '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, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461112, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461113, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461114, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461115, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461116, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461117, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461118, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461119, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461120, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461121, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461122, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461123, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461124, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461125, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461126, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461127, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461128, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755461129, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462111, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462112, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462113, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462114, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462115, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462116, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462117, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462118, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462119, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755462120, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755463111, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755463112, '', '', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
(1278601251755463113, '', '', '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 进行过期时间控制)。

(3)登录需求:
  登录方式:密码登录、短信登录。
其中:
  短信登录 是根据 手机号以及验证码 进行登录(跳过密码输入操作)。
  密码登录 是根据 手机号 或者 用户名 加密码 的方式进行登录。

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

  登录时限制同一账号登陆人数。
注:
  此项目中限制同一账号登陆人数为 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 给用户,即登录失败,引导用户重新登录)。

注意:
  此处为了实现效果,还需要修改 单点登录 逻辑,之前单点登录逻辑中,根据 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(用于逻辑删除) 注解。

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

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

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);
}
}
}

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();
}
}

(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));
}
}

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=用户名格式错误

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 {
}

为了逻辑看起来简单,此处使用了三种 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;
}

定义一个 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;
}

(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("登录失败");
}
}

使用 swagger 简单测试一下:
  点击用户名 + 密码登录,生成 token,存入 redis 中并设置过期时间 30 分钟(1800 秒)。
  点击手机号 + 密码登录,会重新生成 token,并存入 redis 中。
  并发操作,可以使用 Jmeter 进行测试(此处省略)。

(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("验证码获取失败");
}
}

使用 swagger 简单测试一下:
  首先获取验证码,其会存放于 redis 中,过期时间为 5 分钟(300 秒)。若 1 分钟内重复点击验证码,会提示相关信息(验证码已发送,1 分钟后再次获取)。
  然后根据 手机号和验证码进行登录操作。

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;
}

接口如下:
  由于 注册 用户均属于 普通用户,所以注册的同时需要给其绑定角色,即向 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);
}

在 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;
}
}

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;
}
}

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

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("登出失败");
}
}

使用 swagger 简单测试一下:
  某用户登录后,会返回一个有效 token,并在 redis 中保存。
  用户登出后,返回一个无效 token,并删除 redis 中数据。

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;
}
}

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);
}
}

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/**");
}
};
}

完整拦截逻辑:

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、redis、sms 工具类完善注册登录逻辑的更多相关文章

  1. SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(一): 搭建基本环境、整合 Swagger、MyBatisPlus、JSR303 以及国际化操作

    相关 (1) 相关博文地址: SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y- ...

  2. vue+element-ui JYAdmin后台管理系统模板-集成方案【项目搭建篇2】

    项目搭建时间:2020-06-29 本章节:讲述基于vue/cli, 项目的基础搭建. 本主题讲述了: 1.跨域配置 2.axios请求封装 3.eslint配置 4.环境dev,test,pro(开 ...

  3. Vue + Element-ui实现后台管理系统(2)---项目搭建 + ⾸⻚布局实现

    项目搭建 + ⾸⻚布局实现 上篇对该项目做了个总述 :Vue + Element-ui实现后台管理系统(1) --- 总述 这篇主要讲解 项目搭建 + 后台⾸⻚布局实现 : 整体效果 后台首页按布局一 ...

  4. Vue + Element-ui实现后台管理系统(4)---封装一个ECharts组件的一点思路

    封装一个ECharts组件的一点思路 有关后台管理系统之前写过三遍博客,看这篇之前最好先看下这三篇博客.另外这里只展示关键部分代码,项目代码放在github上: mall-manage-system ...

  5. Vue + Element-ui实现后台管理系统(3)---面包屑 + Tag标签切换功能

    面包屑 + Tag标签切换功能 有关后台管理系统之前写过两遍博客,看这篇之前最好先看下这两篇博客.另外这里只展示关键部分代码,项目代码放在github上: mall-manage-system 1.V ...

  6. Vue + Element-ui实现后台管理系统(5)---封装一个Form表单组件和Table表格组件

    封装一个Form表单组件和Table组件 有关后台管理系统之前写过四遍博客,看这篇之前最好先看下这四篇博客.另外这里只展示关键部分代码,项目代码放在github上: mall-manage-syste ...

  7. 保姆级别的vue + ElementUI 搭建后台管理系统教程

    vue + ElementUI 搭建后台管理系统记录 本文档记录了该系统从零配置的完整过程 项目源码请访问:https://gitee.com/szxio/vue2Admin,如果感觉对你有帮助,请点 ...

  8. Vue + Element-ui实现后台管理系统(1) --- 总述

    总述 一.项目效果  整体效果 登陆页 后台首页 用户管理页 说明 这里所有的数据都不是直接通过后端获取的, 而是通过Mock这个工具来模拟后端返回的接口数据. 附上github地址: mall-ma ...

  9. vue+element-ui后台管理系统模板

    vue+element-ui后台管理系统模板 前端:基于vue2.0+或3.0+加上element-ui组件框架 后端:springboot+mybatis-plus写接口 通过Axios调用接口完成 ...

随机推荐

  1. Centos7快速安装RocketMQ

    1. 为什么要用MQ 消息队列是一种"先进先出"的数据结构 其应用场景主要包含以下3个方面 应用解耦 系统的耦合性越高,容错性就越低.以电商应用为例,用户创建订单后,如果耦合调用库 ...

  2. Python--字典(三级菜单)

    # -*- coding:utf-8 -*- data = { "腾讯":{ "LOL":{ "上单":["诺手",&q ...

  3. opencv 移植

    1.ubunut系统搭建opencv+python开发环境 1.1.ubuntu系统安装pip3工具 sudo apt-get install python3-pip //安装python模块安装工具 ...

  4. ubuntu18.04安装部署typecho个人博客

    LNMP一键安装包安装 wget http://soft.vpser.net/lnmp/lnmp1.5.tar.gz -cO lnmp1.5.tar.gz && tar zxf lnm ...

  5. VMWare的三种网络连接方式

    VMWare和主机的三种网络连接方式 桥接 这种模式下,虚拟机通过主机的网卡与主机通信,如果主机能够上网,则虚拟机也能联网. 在虚拟机中,需要将虚拟机的IP配置为与主机处于同一网段. 虚拟机也可以与同 ...

  6. Divisors (求解组合数因子个数)【唯一分解定理】

    Divisors 题目链接(点击) Your task in this problem is to determine the number of divisors of Cnk. Just for ...

  7. 【从单体架构到分布式架构】(三)请求增多,单点变集群(2):Nginx

    上一个章节,我们学习了负载均衡的理论知识,那么是不是把应用部署多套,前面挂一个负载均衡的软件或硬件就可以应对高并发了?其实还有很多问题需要考虑.比如: 1. 当一台服务器挂掉,请求如何转发到其他正常的 ...

  8. mysql小数类型

    原文链接:https://blog.csdn.net/weixin_42047611/article/details/81449663 MySQL 中使用浮点数和定点数来表示小数. 浮点类型有两种,分 ...

  9. python生成批量格式化字符串

    在学习tensorflow管道化有关操作时,有一个操作是先生成一个文件名队列.在书上使用了这样的代码: filenames = ['test%d.txt'%i for in in range(1,4) ...

  10. SpringCloud(一)版本选择

    Springboot版本 官网:https://spring.io/projects/spring-boot 在官网上 springboot已经更新到最新2.2.6 Spingcloud版本 官网:h ...