1. 程式人生 > >SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(五): 資料表設計、使用 jwt、redis、sms 工具類完善註冊登入邏輯

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

(1) 相關博文地址:

SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(一):搭建基本環境:https://www.cnblogs.com/l-y-h/p/12930895.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(二):引入 element-ui 定義基本頁面顯示:https://www.cnblogs.com/l-y-h/p/12935300.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(三):引入 js-cookie、axios、mock 封裝請求處理以及返回結果:https://www.cnblogs.com/l-y-h/p/12955001.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(四):引入 vuex 進行狀態管理、引入 vue-i18n 進行國際化管理:https://www.cnblogs.com/l-y-h/p/12963576.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(五):引入 vue-router 進行路由管理、模組化封裝 axios 請求、使用 iframe 標籤巢狀頁面:https://www.cnblogs.com/l-y-h/p/12973364.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(六):使用 vue-router 進行動態載入選單:https://www.cnblogs.com/l-y-h/p/13052196.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(一): 搭建基本環境、整合 Swagger、MyBatisPlus、JSR303 以及國際化操作:https://www.cnblogs.com/l-y-h/p/13083375.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(二): 整合 Redis(常用工具類、快取)、整合郵件傳送功能:https://www.cnblogs.com/l-y-h/p/13163653.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(三): 整合阿里雲 OSS 服務 -- 上傳、下載檔案、圖片:https://www.cnblogs.com/l-y-h/p/13202746.html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(四): 整合阿里雲 簡訊服務、整合 JWT 單點登入:https://www.cnblogs.com/l-y-h/p/13214493.html

(2)程式碼地址:

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

 

一、資料表設計

1、需求分析

(1)目的:
  由於此專案作為一個後臺管理系統模板,不同使用者登入後應該有不同的操作許可權,所以此處實現一個簡單的選單許可權控制。即不同使用者登入系統後,會展示不同的選單,並對選單具有操作(增刪改查)的許可權。

(2)資料表設計(自己瞎搗鼓的,有不對的地方還望 DBA 大神不吝賜教(=_=)):
需求:
  一個使用者登入系統後,根據其所代表的的角色,去查詢其對應的選單許可權,並返回相應的選單資料。

  整個設計核心可以分為:使用者、使用者角色(下面簡稱角色)、選單許可權(下面簡稱選單)。

思考一:
  一個使用者只擁有一個角色,一個角色可以被多個使用者擁有。
  一個角色可以有多個選單,一個選單可以被多個角色擁有。
  即 角色 與 使用者間為 1 對 多關係,角色 與 選單 間為 多對多關係。
  所以可以在使用者表中定義一個欄位作為外來鍵 關聯到 角色表。
  而角色表 與 選單表 採用 中間表去維護。

思考二:
  一個使用者可以有多個角色,一個角色可以被多個使用者擁有。
  一個角色可以有多個選單,一個選單可以被多個角色擁有。
  即 選單 與 角色 間屬於 多對多關係,使用者 與 角色間 也屬於 多對多關係。
  所以 使用者表 與 角色表間、角色表 與 選單表間均可以採用 中間表維護。

為了避免使用外來鍵,此處我均採用中間表對三張表進行資料關聯。

 

最終設計(三個主表,兩個中間表):
  使用者表 sys_user
  使用者角色表 sys_user_role
  角色表 sys_role
  角色選單表 sys_role_menu
  選單表 sys_menu

 

2、使用者表(sys_user)設計

(1)必須欄位:
  使用者 ID、使用者名稱、使用者手機號、使用者密碼。
其中:
  使用者手機號 作為使用者註冊、登入的依據(使用者名稱也可以登入)。
  使用者名稱為 使用者登入後顯示的 暱稱。
  使用者密碼 需要密文儲存(此專案中 前端、後端均對密碼進行 MD5 加密處理)。

(2)資料表結構如下:

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

-- --------------------------sys_user 使用者表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_user;
-- 使用者表
CREATE TABLE sys_user (
    id bigint NOT NULL COMMENT '使用者 ID',
    name varchar(20) NOT NULL COMMENT '使用者名稱',
    mobile varchar(20) NOT NULL COMMENT '使用者手機號',
    password varchar(64) NOT NULL COMMENT '使用者密碼',
   sex tinyint DEFAULT NULL COMMENT '性別, 0 表示女, 1 表示男',
   age tinyint DEFAULT NULL COMMENT '年齡',
   avatar varchar(255) DEFAULT NULL COMMENT '頭像',
   email varchar(100) DEFAULT NULL COMMENT '郵箱',
    create_time datetime DEFAULT NULL COMMENT '建立時間',
    update_time datetime DEFAULT NULL COMMENT '修改時間',
    delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除',
   disabled_flag tinyint DEFAULT NULL COMMENT '禁用標誌, 0 表示未禁用, 1 表示禁用',
   wx_id varchar(128) DEFAULT NULL COMMENT '微信 openid(拓展欄位、用於第三方微信登入)',
   qq_id varchar(128) DEFAULT NULL COMMENT 'QQ openid(拓展欄位、用於第三方 QQ 登入)',
    PRIMARY KEY(id),
    UNIQUE INDEX(name),
    UNIQUE INDEX(mobile)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統使用者表';


-- 插入資料
INSERT INTO `sys_user`(`id`, `name`, `mobile`, `password`, `sex`, `age`, `avatar`, `email`, `create_time`, `update_time`, `delete_flag`, `disabled_flag`, `wx_id`, `qq_id`)
VALUES (1278601251755454466, 'superAdmin', '17730125031', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "[email protected]", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL),
    (1278601251755451232, 'admin', '17730125032', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "[email protected]", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL),
    (1278601251755456778, 'jack', '17730125033', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "[email protected]", '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', '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 角色表---------------------------------------

 

 

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, '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 系統角色選單表---------------------------------------

 

 

6、完整表結構以及相關資料插入

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

-- --------------------------sys_user 使用者表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_user;
-- 使用者表
CREATE TABLE sys_user (
    id bigint NOT NULL COMMENT '使用者 ID',
    name varchar(20) NOT NULL COMMENT '使用者名稱',
    mobile varchar(20) NOT NULL COMMENT '使用者手機號',
    password varchar(64) NOT NULL COMMENT '使用者密碼',
   sex tinyint DEFAULT NULL COMMENT '性別, 0 表示女, 1 表示男',
   age tinyint DEFAULT NULL COMMENT '年齡',
   avatar varchar(255) DEFAULT NULL COMMENT '頭像',
   email varchar(100) DEFAULT NULL COMMENT '郵箱',
    create_time datetime DEFAULT NULL COMMENT '建立時間',
    update_time datetime DEFAULT NULL COMMENT '修改時間',
    delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除',
   disabled_flag tinyint DEFAULT NULL COMMENT '禁用標誌, 0 表示未禁用, 1 表示禁用',
   wx_id varchar(128) DEFAULT NULL COMMENT '微信 openid(拓展欄位、用於第三方微信登入)',
   qq_id varchar(128) DEFAULT NULL COMMENT 'QQ openid(拓展欄位、用於第三方 QQ 登入)',
    PRIMARY KEY(id),
    UNIQUE INDEX(name, mobile)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統使用者表';


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

-- --------------------------sys_user 使用者表---------------------------------------

-- --------------------------sys_role 角色表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_role;
-- 系統使用者角色表
CREATE TABLE sys_role (
    id bigint NOT NULL COMMENT '角色 ID',
    role_name varchar(20) NOT NULL COMMENT '角色名稱',
   role_code varchar(20) DEFAULT NULL COMMENT '角色碼',
   remark varchar(255) DEFAULT NULL COMMENT '角色備註',
    create_time datetime DEFAULT NULL COMMENT '建立時間',
    update_time datetime DEFAULT NULL COMMENT '修改時間',
    delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統使用者角色表';


-- 插入資料
INSERT INTO `sys_role`(`id`, `role_name`, `role_code`, `remark`, `create_time`, `update_time`, `delete_flag`)
VALUES (1278601251755451245, 'superAdmin', '1001', '超級管理員','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755452551, 'admin', '2001', '普通管理員','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (1278601251755458779, 'user', '3001', '普通使用者','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0);

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


-- --------------------------sys_user_role 使用者角色表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_user_role;
-- 系統使用者角色表
CREATE TABLE sys_user_role (
    id bigint NOT NULL COMMENT '使用者角色表 ID',
    role_id bigint NOT NULL COMMENT '角色 ID',
   user_id bigint NOT NULL COMMENT '使用者 ID',
    create_time datetime DEFAULT NULL COMMENT '建立時間',
    update_time datetime DEFAULT NULL COMMENT '修改時間',
    delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統使用者角色表';


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

-- --------------------------sys_user_role 使用者角色表---------------------------------------

-- --------------------------sys_menu 選單許可權表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_menu;
-- 系統選單許可權表
CREATE TABLE sys_menu (
    menu_id bigint NOT NULL COMMENT '當前選單 ID',
    parent_id bigint NOT NULL COMMENT '當前選單父選單 ID',
   name_zh varchar(20) NOT NULL COMMENT '中文選單名稱',
   name_en varchar(40) NOT NULL COMMENT '英文選單名稱',
   type tinyint NOT NULL COMMENT '選單型別,0 表示目錄,1 表示選單項,2 表示按鈕',
   url varchar(100) NOT NULL COMMENT '訪問路徑',
   icon varchar(100) DEFAULT NULL COMMENT '選單圖示',
   order_num int DEFAULT NULL COMMENT '選單項順序',
    create_time datetime DEFAULT NULL COMMENT '建立時間',
    update_time datetime DEFAULT NULL COMMENT '修改時間',
    delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除',
    PRIMARY KEY(menu_id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統選單許可權表';

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

    (127860125172221, 127860125172211, '新增', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172231, 127860125172211, '刪除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172241, 127860125172211, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125172251, 127860125172211, '檢視', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125173321, 127860125173311, '新增', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173331, 127860125173311, '刪除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173341, 127860125173311, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125173351, 127860125173311, '檢視', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

    (127860125174421, 127860125174411, '新增', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174431, 127860125174411, '刪除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174441, 127860125174411, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),
    (127860125174451, 127860125174411, '檢視', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0),

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

-- --------------------------sys_menu 選單許可權表---------------------------------------

-- --------------------------sys_role_menu 系統角色選單表---------------------------------------
USE admin_template;
DROP TABLE IF EXISTS sys_role_menu;
-- 系統角色選單表
CREATE TABLE sys_role_menu (
    id bigint NOT NULL COMMENT '角色選單表 ID',
    role_id bigint NOT NULL COMMENT '角色 ID',
   menu_id varchar(20) NOT NULL COMMENT '選單 ID',
    create_time datetime DEFAULT NULL COMMENT '建立時間',
    update_time datetime DEFAULT NULL COMMENT '修改時間',
    delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除',
    PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統角色選單表';


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

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

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

-- --------------------------sys_role_menu 系統角色選單表---------------------------------------

 

二、完善註冊登入邏輯

1、註冊、登入需求分析:

(1)使用者種類:
  超級管理員、普通管理員、普通使用者。
其中:
  通過註冊方式建立的使用者均為 普通使用者。
  普通管理員由超級管理員建立。
  超級管理員使用 系統預設的資料(不可建立、修改)。
預設:
  普通使用者 -- 賬號:jack 密碼:123456
  普通管理員 -- 賬號:admin 密碼:123456
  超級管理員 -- 賬號:superAdmin 密碼:123456

(2)註冊需求:
  輸入使用者名稱、密碼,並根據 手機號 傳送驗證碼進行註冊。
其中:
  使用者名稱 不能為 純數字 組成 或者 包含 @ 符號(為了與手機號、郵箱進行區分)。
  密碼前後端均採用 MD5 加密,兩次加密。
  驗證碼時效性為 5 分鐘(此專案中借用 redis 進行過期時間控制)。

 

 

(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 中是否存在驗證碼,若不存在驗證碼(驗證碼不存在或失效)或者驗證碼與當前驗證碼不同,則註冊失敗,如存在且相同,則進行下面操作。
  根據使用者名稱與手機號,對資料庫資料進行查詢,若存在資料則註冊失敗,若不存在,則