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