SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(五): 數據表設計、使用 jwt、redis、sms 工具類完善註冊登陸邏輯

(1) 相關博文地址:html

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)目的:
  因爲此項目做爲一個後臺管理系統模板,不一樣用戶登陸後應該有不一樣的操做權限,因此此處實現一個簡單的菜單權限控制。即不一樣用戶登陸系統後,會展現不一樣的菜單,並對菜單具備操做(增刪改查)的權限。vue

(2)數據表設計(本身瞎搗鼓的,有不對的地方還望 DBA 大神不吝賜教(=_=)):
需求:
  一個用戶登陸系統後,根據其所表明的的角色,去查詢其對應的菜單權限,並返回相應的菜單數據。java

  整個設計核心能夠分爲:用戶、用戶角色(下面簡稱角色)、菜單權限(下面簡稱菜單)。ios

思考一:
  一個用戶只擁有一個角色,一個角色能夠被多個用戶擁有。
  一個角色能夠有多個菜單,一個菜單能夠被多個角色擁有。
  即 角色 與 用戶間爲 1 對 多關係,角色 與 菜單 間爲 多對多關係。
  因此能夠在用戶表中定義一個字段做爲外鍵 關聯到 角色表。
  而角色表 與 菜單表 採用 中間表去維護。git

思考二:
  一個用戶能夠有多個角色,一個角色能夠被多個用戶擁有。
  一個角色能夠有多個菜單,一個菜單能夠被多個角色擁有。
  即 菜單 與 角色 間屬於 多對多關係,用戶 與 角色間 也屬於 多對多關係。
  因此 用戶表 與 角色表間、角色表 與 菜單表間都可以採用 中間表維護。github

爲了不使用外鍵,此處我均採用中間表對三張表進行數據關聯。web

 

最終設計(三個主表,兩個中間表):
  用戶表 sys_user
  用戶角色表 sys_user_role
  角色表 sys_role
  角色菜單表 sys_role_menu
  菜單表 sys_menuredis

 

二、用戶表(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 用戶表---------------------------------------

 

 

三、角色表(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 角色表---------------------------------------

 

 

四、菜單權限表(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 菜單權限表---------------------------------------

 

 

五、中間表設計(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 用戶角色表---------------------------------------

 

 

(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 系統角色菜單表---------------------------------------

 

 

六、完整表結構以及相關數據插入

-- 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 系統角色菜單表---------------------------------------

 

2、完善註冊登陸邏輯

一、註冊、登陸需求分析:

(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 進行交互,有更方便的方法還望不吝賜教)。

 

二、生成基本代碼

(1)使用 mybatis-plus 代碼生成器根據 sys_user 表生成基本代碼。
此處再也不重複截圖,詳細使用過程參考:
  https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_1
此處只截細節部分:
Step1:
  修改實體類,添加 @TableField(用於自動填充)、@TableLogic(用於邏輯刪除) 註解。

 

 

Step2:
  因爲新增了填充字段 disabledFlag,因此需給其添加填充規則。

 

 

Step3:
  修改 mapper 掃描路徑,此處可使用通配符 **(只用一個 * 不生效時使用兩個 **)。

 

 

三、編寫一個工具類( 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);
        }
    }
}

 

 

四、調整 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));
    }
}

 

 

五、完善三種登陸方式

(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 分鐘後再次獲取)。
  而後根據 手機號和驗證碼進行登陸操做。

 

 

六、完善註冊邏輯

(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 簡單測試一下,添加數據。

 

 

七、完善登出邏輯

(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 中數據。

 

 

八、完整的登陸、註冊、登出接口代碼

  包括三種登陸接口、註冊接口、登出接口、獲取驗證碼接口。

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

 

九、定義一個攔截器,用於攔截除登陸註冊請求外的全部請求

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

 

十、給 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 後,登出接口不會被攔截。

相關文章
相關標籤/搜索