(1) 相關博文地址:html
SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(一):搭建基本環境:https://www.cnblogs.com/l-y-h/p/12930895.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(二):引入 element-ui 定義基本頁面顯示:https://www.cnblogs.com/l-y-h/p/12935300.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(三):引入 js-cookie、axios、mock 封裝請求處理以及返回結果:https://www.cnblogs.com/l-y-h/p/12955001.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(四):引入 vuex 進行狀態管理、引入 vue-i18n 進行國際化管理:https://www.cnblogs.com/l-y-h/p/12963576.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(五):引入 vue-router 進行路由管理、模塊化封裝 axios 請求、使用 iframe 標籤嵌套頁面:https://www.cnblogs.com/l-y-h/p/12973364.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(六):使用 vue-router 進行動態加載菜單:https://www.cnblogs.com/l-y-h/p/13052196.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(一): 搭建基本環境、整合 Swagger、MyBatisPlus、JSR303 以及國際化操做:https://www.cnblogs.com/l-y-h/p/13083375.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(二): 整合 Redis(經常使用工具類、緩存)、整合郵件發送功能:https://www.cnblogs.com/l-y-h/p/13163653.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(三): 整合阿里雲 OSS 服務 -- 上傳、下載文件、圖片:https://www.cnblogs.com/l-y-h/p/13202746.html SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 後端篇(四): 整合阿里雲 短信服務、整合 JWT 單點登陸:https://www.cnblogs.com/l-y-h/p/13214493.html
(2)代碼地址:前端
https://github.com/lyh-man/admin-vue-template.git
(1)目的:
因爲此項目做爲一個後臺管理系統模板,不一樣用戶登陸後應該有不一樣的操做權限,因此此處實現一個簡單的菜單權限控制。即不一樣用戶登陸系統後,會展現不一樣的菜單,並對菜單具備操做(增刪改查)的權限。vue
(2)數據表設計(本身瞎搗鼓的,有不對的地方還望 DBA 大神不吝賜教(=_=)):
需求:
一個用戶登陸系統後,根據其所表明的的角色,去查詢其對應的菜單權限,並返回相應的菜單數據。java
整個設計核心能夠分爲:用戶、用戶角色(下面簡稱角色)、菜單權限(下面簡稱菜單)。ios
思考一:
一個用戶只擁有一個角色,一個角色能夠被多個用戶擁有。
一個角色能夠有多個菜單,一個菜單能夠被多個角色擁有。
即 角色 與 用戶間爲 1 對 多關係,角色 與 菜單 間爲 多對多關係。
因此能夠在用戶表中定義一個字段做爲外鍵 關聯到 角色表。
而角色表 與 菜單表 採用 中間表去維護。git
思考二:
一個用戶能夠有多個角色,一個角色能夠被多個用戶擁有。
一個角色能夠有多個菜單,一個菜單能夠被多個角色擁有。
即 菜單 與 角色 間屬於 多對多關係,用戶 與 角色間 也屬於 多對多關係。
因此 用戶表 與 角色表間、角色表 與 菜單表間都可以採用 中間表維護。github
爲了不使用外鍵,此處我均採用中間表對三張表進行數據關聯。web
最終設計(三個主表,兩個中間表):
用戶表 sys_user
用戶角色表 sys_user_role
角色表 sys_role
角色菜單表 sys_role_menu
菜單表 sys_menuredis
(1)必須字段:
用戶 ID、用戶名、用戶手機號、用戶密碼。
其中:
用戶手機號 做爲用戶註冊、登陸的依據(用戶名也能夠登陸)。
用戶名爲 用戶登陸後顯示的 暱稱。
用戶密碼 須要密文存儲(此項目中 前端、後端均對密碼進行 MD5 加密處理)。算法
(2)數據表結構以下:
-- DROP DATABASE IF EXISTS admin_template; -- -- CREATE DATABASE admin_template; -- --------------------------sys_user 用戶表--------------------------------------- USE admin_template; DROP TABLE IF EXISTS sys_user; -- 用戶表 CREATE TABLE sys_user ( id bigint NOT NULL COMMENT '用戶 ID', name varchar(20) NOT NULL COMMENT '用戶名', mobile varchar(20) NOT NULL COMMENT '用戶手機號', password varchar(64) NOT NULL COMMENT '用戶密碼', sex tinyint DEFAULT NULL COMMENT '性別, 0 表示女, 1 表示男', age tinyint DEFAULT NULL COMMENT '年齡', avatar varchar(255) DEFAULT NULL COMMENT '頭像', email varchar(100) DEFAULT NULL COMMENT '郵箱', create_time datetime DEFAULT NULL COMMENT '建立時間', update_time datetime DEFAULT NULL COMMENT '修改時間', delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除', disabled_flag tinyint DEFAULT NULL COMMENT '禁用標誌, 0 表示未禁用, 1 表示禁用', wx_id varchar(128) DEFAULT NULL COMMENT '微信 openid(拓展字段、用於第三方微信登陸)', qq_id varchar(128) DEFAULT NULL COMMENT 'QQ openid(拓展字段、用於第三方 QQ 登陸)', PRIMARY KEY(id), UNIQUE INDEX(name), UNIQUE INDEX(mobile) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統用戶表'; -- 插入數據 INSERT INTO `sys_user`(`id`, `name`, `mobile`, `password`, `sex`, `age`, `avatar`, `email`, `create_time`, `update_time`, `delete_flag`, `disabled_flag`, `wx_id`, `qq_id`) VALUES (1278601251755454466, 'superAdmin', '17730125031', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL), (1278601251755451232, 'admin', '17730125032', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL), (1278601251755456778, 'jack', '17730125033', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL); -- --------------------------sys_user 用戶表---------------------------------------
(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 角色表---------------------------------------
(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 菜單權限表---------------------------------------
(1)設計原則:
中間表存儲的是相關聯兩表的主鍵。
(2)用戶角色表以下:
-- DROP DATABASE IF EXISTS admin_template; -- -- CREATE DATABASE admin_template; -- --------------------------sys_user_role 用戶角色表--------------------------------------- USE admin_template; DROP TABLE IF EXISTS sys_user_role; -- 系統用戶角色表 CREATE TABLE sys_user_role ( id bigint NOT NULL COMMENT '用戶角色表 ID', role_id bigint NOT NULL COMMENT '角色 ID', user_id bigint NOT NULL COMMENT '用戶 ID', create_time datetime DEFAULT NULL COMMENT '建立時間', update_time datetime DEFAULT NULL COMMENT '修改時間', delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統用戶角色表'; -- 插入數據 INSERT INTO `sys_user_role`(`id`, `role_id`, `user_id`, `create_time`, `update_time`, `delete_flag`) VALUES (1278601251755452234, '1278601251755451245', '1278601251755454466', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755453544, '1278601251755452551', '1278601251755451232', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755454664, '1278601251755458779', '1278601251755456778', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_user_role 用戶角色表---------------------------------------
(3)角色菜單表以下:
-- DROP DATABASE IF EXISTS admin_template; -- -- CREATE DATABASE admin_template; -- --------------------------sys_role_menu 系統角色菜單表--------------------------------------- USE admin_template; DROP TABLE IF EXISTS sys_role_menu; -- 系統角色菜單表 CREATE TABLE sys_role_menu ( id bigint NOT NULL COMMENT '角色菜單表 ID', role_id bigint NOT NULL COMMENT '角色 ID', menu_id varchar(20) NOT NULL COMMENT '菜單 ID', create_time datetime DEFAULT NULL COMMENT '建立時間', update_time datetime DEFAULT NULL COMMENT '修改時間', delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統角色菜單表'; -- 插入數據 INSERT INTO `sys_role_menu`(`id`, `role_id`, `menu_id`, `create_time`, `update_time`, `delete_flag`) VALUES (1278601251755461111, '1278601251755451245', '1278601251755451111', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461112, '1278601251755451245', '1278601251755452211', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461113, '1278601251755451245', '1278601251755453311', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461114, '1278601251755451245', '1278601251755454411', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461115, '1278601251755451245', '1278601251755452221', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461116, '1278601251755451245', '1278601251755452231', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461117, '1278601251755451245', '1278601251755452241', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461118, '1278601251755451245', '1278601251755452251', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461119, '1278601251755451245', '1278601251755453321', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461120, '1278601251755451245', '1278601251755453331', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461121, '1278601251755451245', '1278601251755453341', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461122, '1278601251755451245', '1278601251755453351', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461123, '1278601251755451245', '1278601251755454421', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461124, '1278601251755451245', '1278601251755454431', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461125, '1278601251755451245', '1278601251755454441', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461126, '1278601251755451245', '1278601251755454451', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461127, '1278601251755451245', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461128, '1278601251755451245', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461129, '1278601251755451245', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462111, '1278601251755452551', '1278601251755451111', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462112, '1278601251755452551', '1278601251755452211', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462113, '1278601251755452551', '1278601251755453311', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462114, '1278601251755452551', '1278601251755454411', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462115, '1278601251755452551', '1278601251755452251', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462116, '1278601251755452551', '1278601251755453351', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462117, '1278601251755452551', '1278601251755454451', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462118, '1278601251755452551', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462119, '1278601251755452551', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462120, '1278601251755452551', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755463111, '1278601251755458779', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755463112, '1278601251755458779', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755463113, '1278601251755458779', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_role_menu 系統角色菜單表---------------------------------------
-- DROP DATABASE IF EXISTS admin_template; -- -- CREATE DATABASE admin_template; -- --------------------------sys_user 用戶表--------------------------------------- USE admin_template; DROP TABLE IF EXISTS sys_user; -- 用戶表 CREATE TABLE sys_user ( id bigint NOT NULL COMMENT '用戶 ID', name varchar(20) NOT NULL COMMENT '用戶名', mobile varchar(20) NOT NULL COMMENT '用戶手機號', password varchar(64) NOT NULL COMMENT '用戶密碼', sex tinyint DEFAULT NULL COMMENT '性別, 0 表示女, 1 表示男', age tinyint DEFAULT NULL COMMENT '年齡', avatar varchar(255) DEFAULT NULL COMMENT '頭像', email varchar(100) DEFAULT NULL COMMENT '郵箱', create_time datetime DEFAULT NULL COMMENT '建立時間', update_time datetime DEFAULT NULL COMMENT '修改時間', delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除', disabled_flag tinyint DEFAULT NULL COMMENT '禁用標誌, 0 表示未禁用, 1 表示禁用', wx_id varchar(128) DEFAULT NULL COMMENT '微信 openid(拓展字段、用於第三方微信登陸)', qq_id varchar(128) DEFAULT NULL COMMENT 'QQ openid(拓展字段、用於第三方 QQ 登陸)', PRIMARY KEY(id), UNIQUE INDEX(name, mobile) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統用戶表'; -- 插入數據 INSERT INTO `sys_user`(`id`, `name`, `mobile`, `password`, `sex`, `age`, `avatar`, `email`, `create_time`, `update_time`, `delete_flag`, `disabled_flag`, `wx_id`, `qq_id`) VALUES (1278601251755454466, 'superAdmin', '17730125031', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL), (1278601251755451232, 'admin', '17730125032', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL), (1278601251755456778, 'jack', '17730125033', 'e10adc3949ba59abbe56e057f20f883e', 1, 23, NULL, "m_17730125031@163.com", '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0, 0, NULL, NULL); -- --------------------------sys_user 用戶表--------------------------------------- -- --------------------------sys_role 角色表--------------------------------------- USE admin_template; DROP TABLE IF EXISTS sys_role; -- 系統用戶角色表 CREATE TABLE sys_role ( id bigint NOT NULL COMMENT '角色 ID', role_name varchar(20) NOT NULL COMMENT '角色名稱', role_code varchar(20) DEFAULT NULL COMMENT '角色碼', remark varchar(255) DEFAULT NULL COMMENT '角色備註', create_time datetime DEFAULT NULL COMMENT '建立時間', update_time datetime DEFAULT NULL COMMENT '修改時間', delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統用戶角色表'; -- 插入數據 INSERT INTO `sys_role`(`id`, `role_name`, `role_code`, `remark`, `create_time`, `update_time`, `delete_flag`) VALUES (1278601251755451245, 'superAdmin', '1001', '超級管理員','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755452551, 'admin', '2001', '普通管理員','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755458779, 'user', '3001', '普通用戶','2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_role 角色表--------------------------------------- -- --------------------------sys_user_role 用戶角色表--------------------------------------- USE admin_template; DROP TABLE IF EXISTS sys_user_role; -- 系統用戶角色表 CREATE TABLE sys_user_role ( id bigint NOT NULL COMMENT '用戶角色表 ID', role_id bigint NOT NULL COMMENT '角色 ID', user_id bigint NOT NULL COMMENT '用戶 ID', create_time datetime DEFAULT NULL COMMENT '建立時間', update_time datetime DEFAULT NULL COMMENT '修改時間', delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統用戶角色表'; -- 插入數據 INSERT INTO `sys_user_role`(`id`, `role_id`, `user_id`, `create_time`, `update_time`, `delete_flag`) VALUES (1278601251755452234, '1278601251755451245', '1278601251755454466', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755453544, '1278601251755452551', '1278601251755451232', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755454664, '1278601251755458779', '1278601251755456778', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_user_role 用戶角色表--------------------------------------- -- --------------------------sys_menu 菜單權限表--------------------------------------- USE admin_template; DROP TABLE IF EXISTS sys_menu; -- 系統菜單權限表 CREATE TABLE sys_menu ( menu_id bigint NOT NULL COMMENT '當前菜單 ID', parent_id bigint NOT NULL COMMENT '當前菜單父菜單 ID', name_zh varchar(20) NOT NULL COMMENT '中文菜單名稱', name_en varchar(40) NOT NULL COMMENT '英文菜單名稱', type tinyint NOT NULL COMMENT '菜單類型,0 表示目錄,1 表示菜單項,2 表示按鈕', url varchar(100) NOT NULL COMMENT '訪問路徑', icon varchar(100) DEFAULT NULL COMMENT '菜單圖標', order_num int DEFAULT NULL COMMENT '菜單項順序', create_time datetime DEFAULT NULL COMMENT '建立時間', update_time datetime DEFAULT NULL COMMENT '修改時間', delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除', PRIMARY KEY(menu_id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統菜單權限表'; -- 插入數據 INSERT INTO `sys_menu`(`menu_id`, `parent_id`, `name_zh`, `name_en`, `type`, `url`, `icon`, `order_num`, `create_time`, `update_time`, `delete_flag`) VALUES (127860125171111, 0, '系統管理', 'System Control', 0, '', 'el-icon-setting', 0,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125172211, 127860125171111, '用戶管理', 'User Control', 1, 'sys/UserList', 'el-icon-user', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125173311, 127860125171111, '角色管理', 'Role Control', 1, 'sys/RoleControl', 'el-icon-price-tag', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125174411, 127860125171111, '菜單管理', 'Menu Control', 1, 'sys/MenuControl', 'el-icon-menu', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125172221, 127860125172211, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125172231, 127860125172211, '刪除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125172241, 127860125172211, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125172251, 127860125172211, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125173321, 127860125173311, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125173331, 127860125173311, '刪除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125173341, 127860125173311, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125173351, 127860125173311, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125174421, 127860125174411, '添加', 'Add', 2, '', '', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125174431, 127860125174411, '刪除', 'Delete', 2, '', '', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125174441, 127860125174411, '修改', 'Update', 2, '', '', 3,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125174451, 127860125174411, '查看', 'List', 2, '', '', 4,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125175511, 0, '幫助', 'help', 0, '', 'el-icon-info', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125175521, 127860125175511, '百度', 'Baidu', 1, 'https://www.baidu.com/', 'el-icon-menu', 1,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (127860125175531, 127860125175511, '博客', 'Blog', 1, 'https://www.cnblogs.com/l-y-h/', 'el-icon-menu', 2,'2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_menu 菜單權限表--------------------------------------- -- --------------------------sys_role_menu 系統角色菜單表--------------------------------------- USE admin_template; DROP TABLE IF EXISTS sys_role_menu; -- 系統角色菜單表 CREATE TABLE sys_role_menu ( id bigint NOT NULL COMMENT '角色菜單表 ID', role_id bigint NOT NULL COMMENT '角色 ID', menu_id varchar(20) NOT NULL COMMENT '菜單 ID', create_time datetime DEFAULT NULL COMMENT '建立時間', update_time datetime DEFAULT NULL COMMENT '修改時間', delete_flag tinyint DEFAULT NULL COMMENT '邏輯刪除標誌,0 表示未刪除, 1 表示刪除', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系統角色菜單表'; -- 插入數據 INSERT INTO `sys_role_menu`(`id`, `role_id`, `menu_id`, `create_time`, `update_time`, `delete_flag`) VALUES (1278601251755461111, '1278601251755451245', '1278601251755451111', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461112, '1278601251755451245', '1278601251755452211', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461113, '1278601251755451245', '1278601251755453311', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461114, '1278601251755451245', '1278601251755454411', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461115, '1278601251755451245', '1278601251755452221', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461116, '1278601251755451245', '1278601251755452231', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461117, '1278601251755451245', '1278601251755452241', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461118, '1278601251755451245', '1278601251755452251', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461119, '1278601251755451245', '1278601251755453321', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461120, '1278601251755451245', '1278601251755453331', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461121, '1278601251755451245', '1278601251755453341', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461122, '1278601251755451245', '1278601251755453351', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461123, '1278601251755451245', '1278601251755454421', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461124, '1278601251755451245', '1278601251755454431', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461125, '1278601251755451245', '1278601251755454441', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461126, '1278601251755451245', '1278601251755454451', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461127, '1278601251755451245', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461128, '1278601251755451245', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755461129, '1278601251755451245', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462111, '1278601251755452551', '1278601251755451111', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462112, '1278601251755452551', '1278601251755452211', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462113, '1278601251755452551', '1278601251755453311', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462114, '1278601251755452551', '1278601251755454411', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462115, '1278601251755452551', '1278601251755452251', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462116, '1278601251755452551', '1278601251755453351', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462117, '1278601251755452551', '1278601251755454451', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462118, '1278601251755452551', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462119, '1278601251755452551', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755462120, '1278601251755452551', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755463111, '1278601251755458779', '1278601251755455511', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755463112, '1278601251755458779', '1278601251755455521', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0), (1278601251755463113, '1278601251755458779', '1278601251755455531', '2020-07-02 16:07:48', '2020-07-02 16:07:48', 0); -- --------------------------sys_role_menu 系統角色菜單表---------------------------------------
(1)用戶種類:
超級管理員、普通管理員、普通用戶。
其中:
經過註冊方式建立的用戶均爲 普通用戶。
普通管理員由超級管理員建立。
超級管理員使用 系統默認的數據(不可建立、修改)。
默認:
普通用戶 -- 帳號:jack 密碼:123456
普通管理員 -- 帳號:admin 密碼:123456
超級管理員 -- 帳號:superAdmin 密碼:123456
(2)註冊需求:
輸入用戶名、密碼,並根據 手機號 發送驗證碼進行註冊。
其中:
用戶名 不能爲 純數字 組成 或者 包含 @ 符號(爲了與手機號、郵箱進行區分)。
密碼先後端均採用 MD5 加密,兩次加密。
驗證碼時效性爲 5 分鐘(此項目中借用 redis 進行過時時間控制)。
(3)登陸需求:
登陸方式:密碼登陸、短信登陸。
其中:
短信登陸 是根據 手機號以及驗證碼 進行登陸(跳過密碼輸入操做)。
密碼登陸 是根據 手機號 或者 用戶名 加密碼 的方式進行登陸。
登陸時提供忘記密碼功能,根據手機號重置密碼。
登陸時限制同一帳號登錄人數。
注:
此項目中限制同一帳號登錄人數爲 1 人,即同時只容許一個 帳號登錄系統。
實現限制同一帳號登錄人數思路:
併發執行時,存在同一個用戶在多處同時登錄,此處爲了限制只能容許一我的登錄系統,使用 redis 進行輔助。其中 key 爲 用戶名(或者 ID 值)、 value 爲 token 值(JWT 值)。
用戶第一次訪問系統時,首先斷定是否爲第一次登陸系統(檢查 redis 中是否存在 token),不存在則爲第一次登陸,須要將 token 存入 redis 中,並將該 token 返回給用戶。存在則繼續斷定是否爲重複登陸系統(檢查 token 是否一致)。token 一致,則爲同一用戶再次訪問系統。token 不一致,則用戶爲重複登陸系統,此時須要剔除前一個登陸用戶(比較當前 token 與 redis 中 token 的時間戳),若是當前 token 時間戳 大於等於 redis 中 token 時間戳,則當前時間戳爲最新登陸者,此時剔除 redis 中的 token 數據(即將 當前 token 數據存入 redis),若是 小於 redis 中 token 時間戳,則 redis 中 token 爲最新登陸者,需剔除當前 token(不返回 token 給用戶,即登陸失敗,引導用戶從新登陸)。
注意:
此處爲了實現效果,還須要修改 單點登陸 邏輯,以前單點登陸邏輯中,根據 token 能夠直接解析出 用戶信息。
可是在此處 token 並不必定有效,由於存在同一用戶在多處登陸,每一次登陸均會產生一個 token(定義攔截器,攔截除了登陸請求外的全部請求,這樣使每次登陸請求均能產生 token,非登陸請求驗證是否存在 token),此時爲了限制只容許一人登陸,即只有一個 token 生效。
須要與 redis 中存儲的 token 比較後纔可確認。若 二者 token 不一樣,需引導用戶從新進行登陸操做,並將最新的 token 存入 redis(感受代碼好像變得有點冗餘了(=_=),畢竟每次還得與 redis 進行交互,有更方便的方法還望不吝賜教)。
(1)使用 mybatis-plus 代碼生成器根據 sys_user 表生成基本代碼。
此處再也不重複截圖,詳細使用過程參考:
https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_1
此處只截細節部分:
Step1:
修改實體類,添加 @TableField(用於自動填充)、@TableLogic(用於邏輯刪除) 註解。
Step2:
因爲新增了填充字段 disabledFlag,因此需給其添加填充規則。
Step3:
修改 mapper 掃描路徑,此處可使用通配符 **(只用一個 * 不生效時使用兩個 **)。
(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); } } }
(1)目的:
以前考慮的有點欠缺,這兩個工具類使用起來有點問題,稍做修改。
(2)修改 JWT 工具類 JwtUtil.java
主要修改 自定義數據 的方式,以及自定義 過時時間。
package com.lyh.admin_template.back.common.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.apache.commons.lang3.StringUtils; import javax.servlet.http.HttpServletRequest; import java.util.Date; /** * JWT 操做工具類 */ public class JwtUtil { // 設置默認過時時間(15 分鐘) private static final long DEFAULT_EXPIRE = 1000L * 60 * 15; // 設置 jwt 生成 secret(隨意指定) private static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; /** * 生成 jwt token,並指定默認過時時間 15 分鐘 */ public static String getJwtToken(Object data) { return getJwtToken(data, DEFAULT_EXPIRE); } /** * 生成 jwt token,根據指定的 過時時間 */ public static String getJwtToken(Object data, Long expire) { String JwtToken = Jwts.builder() // 設置 jwt 類型 .setHeaderParam("typ", "JWT") // 設置 jwt 加密方法 .setHeaderParam("alg", "HS256") // 設置 jwt 主題 .setSubject("admin-user") // 設置 jwt 發佈時間 .setIssuedAt(new Date()) // 設置 jwt 過時時間 .setExpiration(new Date(System.currentTimeMillis() + expire)) // 設置自定義數據 .claim("data", data) // 設置密鑰與算法 .signWith(SignatureAlgorithm.HS256, APP_SECRET) // 生成 token .compact(); return JwtToken; } /** * 判斷token是否存在與有效,true 表示未過時,false 表示過時或不存在 */ public static boolean checkToken(String jwtToken) { if (StringUtils.isEmpty(jwtToken)) { return false; } try { // 獲取 token 數據 Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); // 判斷是否過時 return claimsJws.getBody().getExpiration().after(new Date()); } catch (Exception e) { throw new RuntimeException(e); } } /** * 判斷token是否存在與有效 */ public static boolean checkToken(HttpServletRequest request) { return checkToken(request.getHeader("token")); } /** * 根據 token 獲取數據 */ public static Claims getTokenBody(HttpServletRequest request) { return getTokenBody(request.getHeader("token")); } /** * 根據 token 獲取數據 */ public static Claims getTokenBody(String jwtToken) { if (StringUtils.isEmpty(jwtToken)) { return null; } Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); return claimsJws.getBody(); } }
(3)修改 短信發送工具類 SmsUtil.java
主要修改 其返回數據的方式,返回 code,而非 boolean 數據。
package com.lyh.admin_template.back.common.utils; import com.aliyuncs.CommonRequest; import com.aliyuncs.CommonResponse; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.http.MethodType; import com.aliyuncs.profile.DefaultProfile; import com.lyh.admin_template.back.modules.sms.entity.SmsResponse; import lombok.Data; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * sms 短信發送工具類 */ @Data @Component public class SmsUtil { @Value("${aliyun.accessKeyId}") private String accessKeyId; @Value("${aliyun.accessKeySecret}") private String accessKeySecret; @Value("${aliyun.signName}") private String signName; @Value("${aliyun.templateCode}") private String templateCode; @Value("${aliyun.regionId}") private String regionId; private final static String OK = "OK"; /** * 發送短信 */ public String sendSms(String phoneNumbers) { if (StringUtils.isEmpty(phoneNumbers)) { return null; } DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); IAcsClient client = new DefaultAcsClient(profile); CommonRequest request = new CommonRequest(); // 固定參數,無需修改 request.setSysMethod(MethodType.POST); request.setSysDomain("dysmsapi.aliyuncs.com"); request.setSysVersion("2017-05-25"); request.setSysAction("SendSms"); request.putQueryParameter("RegionId", regionId); // 設置手機號 request.putQueryParameter("PhoneNumbers", phoneNumbers); // 設置簽名模板 request.putQueryParameter("SignName", signName); // 設置短信模板 request.putQueryParameter("TemplateCode", templateCode); // 設置短信驗證碼 String code = getCode(); request.putQueryParameter("TemplateParam", "{\"code\":" + code +"}"); try { CommonResponse response = client.getCommonResponse(request); System.out.println(response.getData()); // 轉換返回的數據(需引入 Gson 依賴) SmsResponse smsResponse = GsonUtil.fromJson(response.getData(), SmsResponse.class); // 當 message 與 code 均爲 ok 時,短信發送成功、不然失敗 if (SmsUtil.OK.equals(smsResponse.getMessage()) && SmsUtil.OK.equals(smsResponse.getCode())) { return code; } return null; } catch (Exception e) { throw new RuntimeException(e); } } /** * 獲取 6 位驗證碼 */ public String getCode() { return String.valueOf((int)((Math.random()*9+1)*100000)); } }
(1)三種登陸方式:
密碼登陸:
用戶名 + 密碼。
手機號 + 密碼。
驗證碼登陸:
手機號 + 驗證碼。
(2)定義相關 vo 類 以及 進行 國際化、JSR303 處理
定義 vo(viewObject)實體類去接收數據,並對其進行 JSR303 校驗,固然國際化也得一塊兒處理。
國際化數據以下:
詳細使用請參考:https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_4
【en】 sys.user.name.notEmpty=Sys user name cannot be null sys.user.phone.notEmpty=Sys user mobile cannot be null sys.user.password.notEmpty=Sys user password cannot be null sys.user.code.notEmpty=Sys user code cannot be null sys.user.phone.format.error=Sys user mobile format error sys.user.name.format.error=Sys user name format error 【zh】 sys.user.name.notEmpty=用戶名不能爲空 sys.user.phone.notEmpty=用戶手機號不能爲空 sys.user.password.notEmpty=用戶密碼不能爲空 sys.user.code.notEmpty=驗證碼不能爲空 sys.user.phone.format.error=用戶手機號格式錯誤 sys.user.name.format.error=用戶名格式錯誤
vo 以及 JSR303 數據校驗以下:
定義分組,用於不一樣場景的數據校驗(不定義也行)。
詳細使用可參考:https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_2
【LoginGroup】 package com.lyh.admin_template.back.common.validator.group.sys; /** * 新增登陸的 Group 校驗規則 */ public interface LoginGroup { } 【RegisterGroup】 package com.lyh.admin_template.back.common.validator.group.sys; /** * 新增註冊的 Group 校驗規則 */ public interface RegisterGroup { }
爲了邏輯看起來簡單,此處使用了三種 vo 分別接受不一樣場景下的登陸數據。
三種 vo 以下:
【用戶名 + 密碼】 package com.lyh.admin_template.back.modules.sys.vo; import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup; import lombok.Data; import javax.validation.constraints.NotEmpty; /** * 登陸時的視圖數據類(view object), * 用於接收使用 用戶名 + 密碼 登錄的數據與操做。 */ @Data public class NamePwdLoginVo { @NotEmpty(message = "{sys.user.name.notEmpty}", groups = {LoginGroup.class}) private String userName; @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {LoginGroup.class}) private String password; } 【手機號 + 密碼】 package com.lyh.admin_template.back.modules.sys.vo; import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup; import lombok.Data; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; /** * 登陸時的視圖數據類(view object), * 用於接收使用 手機號 + 密碼 登錄的數據與操做。 */ @Data public class PhonePwdLoginVo { @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {LoginGroup.class}) @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {LoginGroup.class}) private String phone; @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {LoginGroup.class}) private String password; } 【手機號 + 驗證碼】 package com.lyh.admin_template.back.modules.sys.vo; import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup; import lombok.Data; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; /** * 登陸時的視圖數據類(view object), * 用於接收使用 手機號 + 驗證碼 登錄的數據與操做。 */ @Data public class PhoneCodeLoginVo { @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {LoginGroup.class}) @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {LoginGroup.class}) private String phone; @NotEmpty(message = "{sys.user.code.notEmpty}", groups = {LoginGroup.class}) private String code; }
定義一個 vo,用於存儲 jwt 自定義數據。
package com.lyh.admin_template.back.modules.sys.vo; import lombok.Data; /** * 保存 JWT 對應存儲的數據 */ @Data public class JwtVo { // 保存用戶 ID private Long id; // 保存用戶名 private String name; // 保存用戶手機號 private String phone; // 保存 JWT 建立時間戳 private Long time; }
(3)密碼登陸
主要流程:
接收數據,並對數據校驗,對經過校驗的數據進行操做。
根據數據去數據庫查找數據,若查找失敗,則返回相關異常數據。若存在數據,進行下面操做。
使用 JWT 工具類將相關數據封裝,並存放在 redis 中,其中以數據 ID 爲 key,jwt 爲 value。
最後將 jwt 數據返回,命名爲 token(前臺接收數據並保存,通常存放於 cookie 的 header )。
jwt 與 redis 邏輯須要注意一下:
因爲此項目中只容許某用戶同時登錄系統的人數爲 1,即某用戶屢次登陸時,後一次登陸的 jwt 須要替換掉 redis 中的 jwt,併發操做執行可能致使 後一次 jwt 的生成時機 在 redis 中 jwt 以前,直接替換會使最新的登陸者被剔除,因此每次登陸操做不能直接替換掉 redis 中的 jwt。
每次登陸前,生成 jwt 後,應該去查詢 redis 中是否存在對應的 jwt,若是不存在,則直接將當前 jwt 存入 redis 中,若是存在,則比較兩個 jwt 的時間戳,若 redis 中 jwt 大於當前 jwt,則當前登陸失敗,不然將當前 jwt 存入 redis 中。
後臺代碼實現以下:(前臺代碼後續再整合)
package com.lyh.admin_template.back.modules.sys.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.lyh.admin_template.back.common.utils.*; import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup; import com.lyh.admin_template.back.modules.sys.entity.SysUser; import com.lyh.admin_template.back.modules.sys.service.SysUserService; import com.lyh.admin_template.back.modules.sys.vo.JwtVo; import com.lyh.admin_template.back.modules.sys.vo.NamePwdLoginVo; import com.lyh.admin_template.back.modules.sys.vo.PhonePwdLoginVo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Date; /** * <p> * 系統用戶表 前端控制器 * </p> * * @author lyh * @since 2020-07-02 */ @RestController @RequestMapping("/sys/sys-user") @Api(tags = "用戶登陸、註冊操做") public class SysUserController { /** * 用於操做 sys_user 表 */ @Autowired private SysUserService sysUserService; /** * 用於操做 redis */ @Autowired private RedisUtil redisUtil; /** * 常量,表示用戶密碼登陸操做 */ private static final String USER_NAME_STATUS = "0"; /** * 常量,表示手機號密碼登陸操做 */ private static final String PHONE_STATUS = "1"; /** * 獲取 jwt * @return jwt */ private String getJwt(SysUser sysUser) { // 獲取須要保存在 jwt 中的數據 JwtVo jwtVo = new JwtVo(); jwtVo.setId(sysUser.getId()); jwtVo.setName(sysUser.getName()); jwtVo.setPhone(sysUser.getMobile()); jwtVo.setTime(new Date().getTime()); // 獲取 jwt 數據,設置過時時間爲 30 分鐘 String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30); // 判斷用戶是否重複登陸(code 有值則重複登陸,須要保留最新的登陸者,剔除前一個登陸者) String code = redisUtil.get(String.valueOf(sysUser.getId())); // 獲取當前時間戳 Long currentTime = new Date().getTime(); // 若是 redis 中存在 jwt 數據,則根據時間戳比較誰爲最新的登錄者 if (StringUtils.isNotEmpty(code)) { // 獲取 redis 中存儲的 jwt 數據 JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class); // redis jwt 大於 當前時間戳,則 redis 中 jwt 爲最新登陸者,當前登陸失敗 if (redisJwt.getTime() > currentTime) { return null; } } // 把數據存放在 redis 中,設置過時時間爲 30 分鐘 redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30); return jwt; } /** * 使用密碼進行真實登陸操做 * @param account 帳號(用戶名或手機號) * @param pwd 密碼 * @param status 是否使用用戶名登陸(0 表示用戶名登陸,1 表示手機號登陸) * @return jwt */ private String pwdLogin(String account, String pwd, String status) { // 新增查詢條件 QueryWrapper queryWrapper = new QueryWrapper(); // 若是是用戶名 + 密碼登陸,則根據 姓名 + 密碼 查找數據 if (USER_NAME_STATUS.equals(status)) { queryWrapper.eq("name", account); } // 若是是手機號 + 密碼登陸,則根據 手機號 + 密碼 查找數據 if (PHONE_STATUS.equals(status)) { queryWrapper.eq("mobile", account); } // 添加密碼條件,密碼進行 MD5 加密後再與數據庫數據比較 queryWrapper.eq("password", MD5Util.encrypt(pwd)); // 獲取用戶數據 SysUser sysUser = sysUserService.getOne(queryWrapper); // 若是存在用戶數據 if (sysUser != null) { return getJwt(sysUser); } return null; } @ApiOperation(value = "使用用戶名、密碼登陸") @PostMapping("/login/namePwdLogin") public Result namePwdLogin(@Validated({LoginGroup.class}) @RequestBody NamePwdLoginVo namePwdLoginVo) { String jwt = pwdLogin(namePwdLoginVo.getUserName(), namePwdLoginVo.getPassword(), USER_NAME_STATUS); if (StringUtils.isNotEmpty(jwt)) { return Result.ok().message("登陸成功").data("token", jwt); } return Result.error().message("登陸失敗"); } @ApiOperation(value = "使用手機號、密碼登陸") @PostMapping("/login/phonePwdLogin") public Result phonePwdLogin(@Validated({LoginGroup.class}) @RequestBody PhonePwdLoginVo phonePwdLoginVo) { String jwt = pwdLogin(phonePwdLoginVo.getPhone(), phonePwdLoginVo.getPassword(), PHONE_STATUS); if (StringUtils.isNotEmpty(jwt)) { return Result.ok().message("登陸成功").data("token", jwt); } return Result.error().message("登陸失敗"); } }
使用 swagger 簡單測試一下:
點擊用戶名 + 密碼登陸,生成 token,存入 redis 中並設置過時時間 30 分鐘(1800 秒)。
點擊手機號 + 密碼登陸,會從新生成 token,並存入 redis 中。
併發操做,可使用 Jmeter 進行測試(此處省略)。
(4)驗證碼登陸
獲取驗證碼流程:
首先獲取驗證碼(此處不考慮併發狀況,畢竟手機號只有一個用戶能用,應該避免重複獲取驗證碼的狀況),並將其存放與 redis 中,設置過時時間爲 5 分鐘。
爲了不重複獲取驗證碼,能夠根據其已過時時間是否小於 1 分鐘判斷,即 1 分鐘內不能夠重複獲取驗證碼。
驗證碼登陸流程:
接收數據,並校驗數據,經過檢驗的數據進行下面處理。
先檢查 redis 中是否存在驗證碼,若不存在驗證碼(驗證碼不存在或失效),則登陸失敗。不然,根據手機號去查詢用戶數據,生成 jwt,存放與 redis 中並返回。
後臺代碼實現以下:(前臺代碼後續再整合)
package com.lyh.admin_template.back.modules.sys.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.lyh.admin_template.back.common.utils.*; import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup; import com.lyh.admin_template.back.modules.sys.entity.SysUser; import com.lyh.admin_template.back.modules.sys.service.SysUserService; import com.lyh.admin_template.back.modules.sys.vo.JwtVo; import com.lyh.admin_template.back.modules.sys.vo.PhoneCodeLoginVo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.Date; /** * <p> * 系統用戶表 前端控制器 * </p> * * @author lyh * @since 2020-07-02 */ @RestController @RequestMapping("/sys/sys-user") @Api(tags = "用戶登陸、註冊操做") public class SysUserController { /** * 用於操做 sys_user 表 */ @Autowired private SysUserService sysUserService; /** * 用於操做 redis */ @Autowired private RedisUtil redisUtil; /** * 用於操做 短信驗證碼發送 */ @Autowired private SmsUtil smsUtil; /** * 獲取 jwt * @return jwt */ private String getJwt(SysUser sysUser) { // 獲取須要保存在 jwt 中的數據 JwtVo jwtVo = new JwtVo(); jwtVo.setId(sysUser.getId()); jwtVo.setName(sysUser.getName()); jwtVo.setPhone(sysUser.getMobile()); jwtVo.setTime(new Date().getTime()); // 獲取 jwt 數據,設置過時時間爲 30 分鐘 String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30); // 判斷用戶是否重複登陸(code 有值則重複登陸,須要保留最新的登陸者,剔除前一個登陸者) String code = redisUtil.get(String.valueOf(sysUser.getId())); // 獲取當前時間戳 Long currentTime = new Date().getTime(); // 若是 redis 中存在 jwt 數據,則根據時間戳比較誰爲最新的登錄者 if (StringUtils.isNotEmpty(code)) { // 獲取 redis 中存儲的 jwt 數據 JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class); // redis jwt 大於 當前時間戳,則 redis 中 jwt 爲最新登陸者,當前登陸失敗 if (redisJwt.getTime() > currentTime) { return null; } } // 把數據存放在 redis 中,設置過時時間爲 30 分鐘 redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30); return jwt; } /** * 使用 驗證碼進行真實登陸操做 * @param phone 手機號 * @param code 驗證碼 * @return jwt */ private String codeLogin(String phone, String code) { // 獲取 redis 中存放的驗證碼 String redisCode = redisUtil.get(phone); // 存在驗證碼,且輸入的驗證碼與 redis 存放的驗證碼相同,則根據手機號去數據庫查詢數據 if (StringUtils.isNotEmpty(redisCode) && code.equals(redisCode)) { // 新增查詢條件 QueryWrapper queryWrapper = new QueryWrapper(); // 根據手機號去查詢數據 queryWrapper.eq("mobile", phone); SysUser sysUser = sysUserService.getOne(queryWrapper); // 若是存在用戶數據 if (sysUser != null) { return getJwt(sysUser); } } return null; } @ApiOperation(value = "使用手機號、驗證碼登陸") @PostMapping("/login/phoneCodeLogin") public Result phoneCodeLogin(@Validated({LoginGroup.class}) @RequestBody PhoneCodeLoginVo phoneCodeLoginVo) { String jwt = codeLogin(phoneCodeLoginVo.getPhone(), phoneCodeLoginVo.getCode()); if (StringUtils.isNotEmpty(jwt)) { return Result.ok().message("登陸成功").data("token", jwt); } return Result.error().message("登陸失敗"); } @ApiOperation(value = "獲取短信驗證碼") @GetMapping("/login/getCode") public Result getCode(String phone) { // 設置默認過時時間 Long defaultTime = 60L * 5; // 先判斷 redis 中是否存儲過驗證碼(設置期限爲 1 分鐘),防止重複獲取驗證碼 Long expire = redisUtil.getExpire(phone); if (expire != null && (defaultTime - expire < 60)) { return Result.error().message("驗證碼已發送,1 分鐘後可再次獲取驗證碼"); } else { // 獲取 短信驗證碼 String code = smsUtil.sendSms(phone); if (StringUtils.isNotEmpty(code)) { // 把驗證碼存放在 redis 中,並設置 過時時間 爲 5 分鐘 redisUtil.set(phone, code, defaultTime); return Result.ok().message("驗證碼獲取成功").data("code", code); } } return Result.error().message("驗證碼獲取失敗"); } }
使用 swagger 簡單測試一下:
首先獲取驗證碼,其會存放於 redis 中,過時時間爲 5 分鐘(300 秒)。若 1 分鐘內重複點擊驗證碼,會提示相關信息(驗證碼已發送,1 分鐘後再次獲取)。
而後根據 手機號和驗證碼進行登陸操做。
(1)主要流程:
先獲取驗證碼,驗證碼處理與驗證碼登陸相同(此處再也不重複)。
輸入用戶名、密碼、手機號、以及獲得的驗證碼,後端對數據進行校驗,校驗經過的數據進行下面操做。
先檢查 redis 中是否存在驗證碼,若不存在驗證碼(驗證碼不存在或失效)或者驗證碼與當前驗證碼不一樣,則註冊失敗,如存在且相同,則進行下面操做。
根據用戶名與手機號,對數據庫數據進行查找,若存在數據則註冊失敗,若不存在,則向數據庫添加數據。因爲給用戶名和手機號添加了惟一性約束,因此能夠直接進行插入操做,存在數據會返回異常,不存在數據會直接插入。
(2)代碼實現以下:
首先定義一個 vo 類,用於接收數據。
package com.lyh.admin_template.back.modules.sys.vo; import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup; import lombok.Data; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; /** * 註冊時對應的視圖數據類(view object), * 用於接收並處理 註冊時的數據。 */ @Data public class RegisterVo { @NotEmpty(message = "{sys.user.name.notEmpty}", groups = {RegisterGroup.class}) @Pattern(message = "{sys.user.name.format.error}", regexp = "^.*[^\\d].*$", groups = {RegisterGroup.class}) private String userName; @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {RegisterGroup.class}) private String password; @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {RegisterGroup.class}) @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {RegisterGroup.class}) private String phone; @NotEmpty(message = "{sys.user.code.notEmpty}", groups = {RegisterGroup.class}) private String code; }
接口以下:
因爲 註冊 用戶均屬於 普通用戶,因此註冊的同時須要給其綁定角色,即向 sys_user 插入數據後,還須要向 sys_user_role 插入數據(須要使用代碼生成器生成相關代碼,此處省略)。
因爲出現多表插入操做,此處使用 @Transactional 對事務進行控制。
注:
@Transactional 須要寫在 Service 層,寫在 Controller 層不生效。
在 service 層定義一個 saveUser 方法。
package com.lyh.admin_template.back.modules.sys.service; import com.baomidou.mybatisplus.extension.service.IService; import com.lyh.admin_template.back.modules.sys.entity.SysUser; /** * <p> * 系統用戶表 服務類 * </p> * * @author lyh * @since 2020-07-02 */ public interface SysUserService extends IService<SysUser> { public boolean saveUser(SysUser sysUser); }
在 service 實現類中,重寫方法並完善註冊邏輯。
package com.lyh.admin_template.back.modules.sys.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.lyh.admin_template.back.modules.sys.entity.SysRole; import com.lyh.admin_template.back.modules.sys.entity.SysUser; import com.lyh.admin_template.back.modules.sys.entity.SysUserRole; import com.lyh.admin_template.back.modules.sys.mapper.SysUserMapper; import com.lyh.admin_template.back.modules.sys.service.SysRoleService; import com.lyh.admin_template.back.modules.sys.service.SysUserRoleService; import com.lyh.admin_template.back.modules.sys.service.SysUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; /** * <p> * 系統用戶表 服務實現類 * </p> * * @author lyh * @since 2020-07-02 */ @Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Autowired private SysRoleService sysRoleService; @Autowired private SysUserRoleService sysUserRoleService; /** * 先插入數據到 用戶表 sys_user 中。 * 再獲取數據 ID 與 角色 ID 並插入到 用戶角色表 sys_user_role 中。 * @param sysUser 用戶數據 * @return true 表示插入成功, false 表示失敗 */ @Override @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,timeout=36000,rollbackFor=Exception.class) public boolean saveUser(SysUser sysUser) { // 向 sys_user 表中插入數據 if (this.save(sysUser)) { // 獲取當前用戶的 ID QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("name", sysUser.getName()); SysUser sysUser2 = this.getOne(queryWrapper); // 獲取普通用戶角色 ID QueryWrapper queryWrapper2 = new QueryWrapper(); queryWrapper2.eq("role_name", "user"); SysRole sysRole = sysRoleService.getOne(queryWrapper2); // 插入到 用戶-角色 表中(sys_user_role) SysUserRole sysUserRole = new SysUserRole(); sysUserRole.setUserId(sysUser2.getId()).setRoleId(sysRole.getId()); return sysUserRoleService.save(sysUserRole); } return false; } }
controller 層接口以下:
package com.lyh.admin_template.back.modules.sys.controller; import com.lyh.admin_template.back.common.utils.MD5Util; import com.lyh.admin_template.back.common.utils.RedisUtil; import com.lyh.admin_template.back.common.utils.Result; import com.lyh.admin_template.back.common.utils.SmsUtil; import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup; import com.lyh.admin_template.back.modules.sys.entity.SysUser; import com.lyh.admin_template.back.modules.sys.service.SysUserService; import com.lyh.admin_template.back.modules.sys.vo.RegisterVo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; /** * <p> * 系統用戶表 前端控制器 * </p> * * @author lyh * @since 2020-07-02 */ @RestController @RequestMapping("/sys/sys-user") @Api(tags = "用戶登陸、註冊操做") public class SysUserController { /** * 用於操做 sys_user 表 */ @Autowired private SysUserService sysUserService; /** * 用於操做 redis */ @Autowired private RedisUtil redisUtil; /** * 用於操做 短信驗證碼發送 */ @Autowired private SmsUtil smsUtil; @ApiOperation(value = "獲取短信驗證碼") @GetMapping("/login/getCode") public Result getCode(String phone) { // 設置默認過時時間 Long defaultTime = 60L * 5; // 先判斷 redis 中是否存儲過驗證碼(設置期限爲 1 分鐘),防止重複獲取驗證碼 Long expire = redisUtil.getExpire(phone); if (expire != null && (defaultTime - expire < 60)) { return Result.error().message("驗證碼已發送,1 分鐘後可再次獲取驗證碼"); } else { // 獲取 短信驗證碼 String code = smsUtil.sendSms(phone); if (StringUtils.isNotEmpty(code)) { // 把驗證碼存放在 redis 中,並設置 過時時間 爲 5 分鐘 redisUtil.set(phone, code, defaultTime); return Result.ok().message("驗證碼獲取成功").data("code", code); } } return Result.error().message("驗證碼獲取失敗"); } @ApiOperation(value = "用戶註冊") @PostMapping("/register") public Result register(@Validated({RegisterGroup.class}) @RequestBody RegisterVo registerVo) { if (save(registerVo)) { return Result.ok().message("用戶註冊成功"); } return Result.error().message("用戶註冊失敗"); } /** * 真實註冊操做 * @param registerVo 註冊數據 * @return true 爲插入成功, false 爲失敗 */ public boolean save(RegisterVo registerVo) { // 判斷 redis 中是否存在 驗證碼 String code = redisUtil.get(registerVo.getPhone()); // redis 中存在驗證碼且與當前驗證碼相同 if (StringUtils.isNotEmpty(code) && code.equals(registerVo.getCode())) { SysUser sysUser = new SysUser(); sysUser.setName(registerVo.getUserName()).setPassword(MD5Util.encrypt(registerVo.getPassword())); sysUser.setMobile(registerVo.getPhone()); return sysUserService.saveUser(sysUser); } return false; } }
使用 swagger 簡單測試一下,添加數據。
(1)目的:
讓客戶端 保存的 token 失效,則用戶再次訪問系統後因爲 token 失效而沒法繼續訪問,需從新登陸後纔可訪問。
後臺操做(非必須操做):
返回一個 過時時間爲 1 秒的 token(或返回一個無效 token),並刪除 redis 中的 token。
前臺操做:
前臺保存無效的 token。
清除 token(簡單粗暴)。
(2)代碼以下:(僅後臺代碼,前臺代碼此處省略、後續整合)
package com.lyh.admin_template.back.modules.sys.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.lyh.admin_template.back.common.utils.JwtUtil; import com.lyh.admin_template.back.common.utils.RedisUtil; import com.lyh.admin_template.back.common.utils.Result; import com.lyh.admin_template.back.modules.sys.entity.SysUser; import com.lyh.admin_template.back.modules.sys.service.SysUserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * <p> * 系統用戶表 前端控制器 * </p> * * @author lyh * @since 2020-07-02 */ @RestController @RequestMapping("/sys/sys-user") @Api(tags = "用戶登陸、註冊操做") public class SysUserController { /** * 用於操做 sys_user 表 */ @Autowired private SysUserService sysUserService; /** * 用於操做 redis */ @Autowired private RedisUtil redisUtil; @ApiOperation(value = "用戶登出") @GetMapping("/logout") public Result logout(@RequestParam String userName) { // 先獲取用戶數據 QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("name", userName); SysUser sysUser = sysUserService.getOne(queryWrapper); // 用戶存在時 if (sysUser != null) { // 生成並返回一個無效的 token String jwt = JwtUtil.getJwtToken(null, 1000L); // 刪除 redis 中的 token redisUtil.del(String.valueOf(sysUser.getId())); return Result.ok().message("登出成功").data("token", jwt); } return Result.error().message("登出失敗"); } }
使用 swagger 簡單測試一下:
某用戶登陸後,會返回一個有效 token,並在 redis 中保存。
用戶登出後,返回一個無效 token,並刪除 redis 中數據。
包括三種登陸接口、註冊接口、登出接口、獲取驗證碼接口。
package com.lyh.admin_template.back.modules.sys.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.lyh.admin_template.back.common.utils.*; import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup; import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup; import com.lyh.admin_template.back.modules.sys.entity.SysUser; import com.lyh.admin_template.back.modules.sys.service.SysUserService; import com.lyh.admin_template.back.modules.sys.vo.*; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.Date; /** * <p> * 系統用戶表 前端控制器 * </p> * * @author lyh * @since 2020-07-02 */ @RestController @RequestMapping("/sys/sys-user") @Api(tags = "用戶登陸、註冊操做") public class SysUserController { /** * 用於操做 sys_user 表 */ @Autowired private SysUserService sysUserService; /** * 用於操做 redis */ @Autowired private RedisUtil redisUtil; /** * 用於操做 短信驗證碼發送 */ @Autowired private SmsUtil smsUtil; /** * 常量,表示用戶密碼登陸操做 */ private static final String USER_NAME_STATUS = "0"; /** * 常量,表示手機號密碼登陸操做 */ private static final String PHONE_STATUS = "1"; /** * 獲取 jwt * @return jwt */ private String getJwt(SysUser sysUser) { // 獲取須要保存在 jwt 中的數據 JwtVo jwtVo = new JwtVo(); jwtVo.setId(sysUser.getId()); jwtVo.setName(sysUser.getName()); jwtVo.setPhone(sysUser.getMobile()); jwtVo.setTime(new Date().getTime()); // 獲取 jwt 數據,設置過時時間爲 30 分鐘 String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30); // 判斷用戶是否重複登陸(code 有值則重複登陸,須要保留最新的登陸者,剔除前一個登陸者) String code = redisUtil.get(String.valueOf(sysUser.getId())); // 獲取當前時間戳 Long currentTime = new Date().getTime(); // 若是 redis 中存在 jwt 數據,則根據時間戳比較誰爲最新的登錄者 if (StringUtils.isNotEmpty(code)) { // 獲取 redis 中存儲的 jwt 數據 JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class); // redis jwt 大於 當前時間戳,則 redis 中 jwt 爲最新登陸者,當前登陸失敗 if (redisJwt.getTime() > currentTime) { return null; } } // 把數據存放在 redis 中,設置過時時間爲 30 分鐘 redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30); return jwt; } /** * 使用密碼進行真實登陸操做 * @param account 帳號(用戶名或手機號) * @param pwd 密碼 * @param status 是否使用用戶名登陸(0 表示用戶名登陸,1 表示手機號登陸) * @return jwt */ private String pwdLogin(String account, String pwd, String status) { // 新增查詢條件 QueryWrapper queryWrapper = new QueryWrapper(); // 若是是用戶名 + 密碼登陸,則根據 姓名 + 密碼 查找數據 if (USER_NAME_STATUS.equals(status)) { queryWrapper.eq("name", account); } // 若是是手機號 + 密碼登陸,則根據 手機號 + 密碼 查找數據 if (PHONE_STATUS.equals(status)) { queryWrapper.eq("mobile", account); } // 添加密碼條件,密碼進行 MD5 加密後再與數據庫數據比較 queryWrapper.eq("password", MD5Util.encrypt(pwd)); // 獲取用戶數據 SysUser sysUser = sysUserService.getOne(queryWrapper); // 若是存在用戶數據 if (sysUser != null) { return getJwt(sysUser); } return null; } /** * 使用 驗證碼進行真實登陸操做 * @param phone 手機號 * @param code 驗證碼 * @return jwt */ private String codeLogin(String phone, String code) { // 獲取 redis 中存放的驗證碼 String redisCode = redisUtil.get(phone); // 存在驗證碼,且輸入的驗證碼與 redis 存放的驗證碼相同,則根據手機號去數據庫查詢數據 if (StringUtils.isNotEmpty(redisCode) && code.equals(redisCode)) { // 新增查詢條件 QueryWrapper queryWrapper = new QueryWrapper(); // 根據手機號去查詢數據 queryWrapper.eq("mobile", phone); SysUser sysUser = sysUserService.getOne(queryWrapper); // 若是存在用戶數據 if (sysUser != null) { return getJwt(sysUser); } } return null; } @ApiOperation(value = "使用用戶名、密碼登陸") @PostMapping("/login/namePwdLogin") public Result namePwdLogin(@Validated({LoginGroup.class}) @RequestBody NamePwdLoginVo namePwdLoginVo) { String jwt = pwdLogin(namePwdLoginVo.getUserName(), namePwdLoginVo.getPassword(), USER_NAME_STATUS); if (StringUtils.isNotEmpty(jwt)) { return Result.ok().message("登陸成功").data("token", jwt); } return Result.error().message("登陸失敗").code(HttpStatus.SC_UNAUTHORIZED); } @ApiOperation(value = "使用手機號、密碼登陸") @PostMapping("/login/phonePwdLogin") public Result phonePwdLogin(@Validated({LoginGroup.class}) @RequestBody PhonePwdLoginVo phonePwdLoginVo) { String jwt = pwdLogin(phonePwdLoginVo.getPhone(), phonePwdLoginVo.getPassword(), PHONE_STATUS); if (StringUtils.isNotEmpty(jwt)) { return Result.ok().message("登陸成功").data("token", jwt); } return Result.error().message("登陸失敗").code(HttpStatus.SC_UNAUTHORIZED); } @ApiOperation(value = "使用手機號、驗證碼登陸") @PostMapping("/login/phoneCodeLogin") public Result phoneCodeLogin(@Validated({LoginGroup.class}) @RequestBody PhoneCodeLoginVo phoneCodeLoginVo) { String jwt = codeLogin(phoneCodeLoginVo.getPhone(), phoneCodeLoginVo.getCode()); if (StringUtils.isNotEmpty(jwt)) { return Result.ok().message("登陸成功").data("token", jwt); } return Result.error().message("登陸失敗").code(HttpStatus.SC_UNAUTHORIZED); } @ApiOperation(value = "獲取短信驗證碼") @GetMapping("/login/getCode") public Result getCode(String phone) { // 設置默認過時時間 Long defaultTime = 60L * 5; // 先判斷 redis 中是否存儲過驗證碼(設置期限爲 1 分鐘),防止重複獲取驗證碼 Long expire = redisUtil.getExpire(phone); if (expire != null && (defaultTime - expire < 60)) { return Result.error().message("驗證碼已發送,1 分鐘後可再次獲取驗證碼"); } else { // 獲取 短信驗證碼 String code = smsUtil.sendSms(phone); if (StringUtils.isNotEmpty(code)) { // 把驗證碼存放在 redis 中,並設置 過時時間 爲 5 分鐘 redisUtil.set(phone, code, defaultTime); return Result.ok().message("驗證碼獲取成功").data("code", code); } } return Result.error().message("驗證碼獲取失敗"); } @ApiOperation(value = "用戶登出") @GetMapping("/logout") public Result logout(@RequestParam String userName) { // 先獲取用戶數據 QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("name", userName); SysUser sysUser = sysUserService.getOne(queryWrapper); // 用戶存在時 if (sysUser != null) { // 生成並返回一個無效的 token String jwt = JwtUtil.getJwtToken(null, 1000L); // 刪除 redis 中的 token redisUtil.del(String.valueOf(sysUser.getId())); return Result.ok().message("登出成功").data("token", jwt); } return Result.error().message("登出失敗"); } @ApiOperation(value = "用戶註冊") @PostMapping("/register") public Result register(@Validated({RegisterGroup.class}) @RequestBody RegisterVo registerVo) { if (save(registerVo)) { return Result.ok().message("用戶註冊成功"); } return Result.error().message("用戶註冊失敗").code(HttpStatus.SC_UNAUTHORIZED); } /** * 真實註冊操做 * @param registerVo 註冊數據 * @return true 爲插入成功, false 爲失敗 */ public boolean save(RegisterVo registerVo) { // 判斷 redis 中是否存在 驗證碼 String code = redisUtil.get(registerVo.getPhone()); // redis 中存在驗證碼且與當前驗證碼相同 if (StringUtils.isNotEmpty(code) && code.equals(registerVo.getCode())) { SysUser sysUser = new SysUser(); sysUser.setName(registerVo.getUserName()).setPassword(MD5Util.encrypt(registerVo.getPassword())); sysUser.setMobile(registerVo.getPhone()); return sysUserService.saveUser(sysUser); } return false; } }
(1)目的:
因爲採用 JWT 進行單點登陸,每次請求前都須要對 token 進行校驗,爲了不在接口中重複進行校驗操做,此處可使用攔截器,攔截每一個請求,校驗經過後放行請求並返回數據,校驗未經過直接返回錯誤數據。
攔截器須要直接放行登陸、註冊等請求,未登陸、註冊時沒有 token 數據,只有登陸後纔有 token 數據,攔截了 登陸、註冊請求後,不會產生 token,成爲一個死循環。
(2)代碼實現以下:
Step1:定義一個攔截器
對於攔截的請求,首先檢查 token 是否過時,過時返回 401 狀態碼。未過時進行下面操做。
獲取 token 信息,並根據 token 的 id 值從 redis 中獲取 redis 中存儲的 token。若 redis 中不存在 token,即用戶未登陸,返回 401 狀態碼。存在 token 則進行下面操做。
若兩 token 相同,即 同一用戶再次訪問系統,放行該請求。token 不一樣,則意味着 同一用戶 在不一樣地方進行登陸,需保留最新的登陸者信息。根據時間戳比較,誰大誰爲最新登陸者,並將其值保存在 redis 中。
/** * 定義一個攔截器,用於攔截請求,並對 JWT 進行驗證 */ class JWTInterceptor extends HandlerInterceptorAdapter { /** * 訪問 controller 前被調用 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 獲取 token(從 header 或者 參數中獲取) String token = request.getHeader("token"); if (StringUtils.isBlank(token)) { token = request.getParameter("token"); } // 驗證 token 是否過時(根據時間戳比較) if (JwtUtil.checkToken(token)) { // 獲取 token 中的數據 Claims claims = JwtUtil.getTokenBody(token); System.out.println(claims.getExpiration()); JwtVo jwt = GsonUtil.fromJson(String.valueOf(claims.get("data")), JwtVo.class); // 獲取 redis 中存儲的 token String redisToken = redisUtil.get(String.valueOf(jwt.getId())); // 當前 token 與 redis 中存儲的 token 進行比較 if (StringUtils.isNotEmpty(redisToken)) { // 獲取 redis 中 token 的數據 JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(redisToken).get("data")), JwtVo.class); // 若二者 token 相同,則爲同一用戶再次訪問系統,放行 if (redisToken.equals(token)) { return true; } else if (redisJwt.getTime() <= jwt.getTime()){ // redis 中 token 生成時間戳 小於等於 當前 token 生成時間戳,即當前用戶爲最新登陸者 // redis 保存當前最新的 token,並放行 redisUtil.set(String.valueOf(redisJwt.getId()), token, 60 * 30); return true; } } } // 認證失敗,返回數據,並返回 401 狀態碼 returnJsonData(response); return false; } }
Step2:定義攔截請求後的數據返回結果。
返回 json 數據,並定義 code 爲 401(受權失敗)。
/** * 返回 json 格式的數據 */ public void returnJsonData(HttpServletResponse response) { PrintWriter pw = null; response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); try { pw = response.getWriter(); // 返回 code 爲 401,表示 token 失效。 pw.print(GsonUtil.toJson(Result.error().message("token 失效或過時").code(HttpStatus.SC_UNAUTHORIZED))); } catch (IOException e) { log.error(e.getMessage()); throw new RuntimeException(e); } }
Step3:定義攔截請求規則:
/** * 定義攔截器,攔截請求。 * 其中: * addPathPatterns 用於添加須要攔截的請求。 * excludePathPatterns 用於添加不須要攔截的請求。 * 此處: * 攔截全部請求,可是排除 登陸、註冊 請求 以及 swagger 請求。 */ @Bean(name = "JWTInterceptor") public WebMvcConfigurer JWTInterceptor() { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) // 攔截全部請求 .addPathPatterns("/**") // 不攔截 登陸、註冊、忘記密碼請求 .excludePathPatterns("/sys/sys-user/login/*", "/sys/sys-user/register") // 不攔截 swagger 請求 .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"); } }; }
完整攔截邏輯:
package com.lyh.admin_template.back.common.config; import com.lyh.admin_template.back.common.utils.GsonUtil; import com.lyh.admin_template.back.common.utils.JwtUtil; import com.lyh.admin_template.back.common.utils.RedisUtil; import com.lyh.admin_template.back.common.utils.Result; import com.lyh.admin_template.back.modules.sys.vo.JwtVo; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Slf4j @Configuration public class JWTConfig { @Autowired private RedisUtil redisUtil; /** * 定義攔截器,攔截請求。 * 其中: * addPathPatterns 用於添加須要攔截的請求。 * excludePathPatterns 用於添加不須要攔截的請求。 * 此處: * 攔截全部請求,可是排除 登陸、註冊 請求 以及 swagger 請求。 */ @Bean(name = "JWTInterceptor") public WebMvcConfigurer JWTInterceptor() { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) // 攔截全部請求 .addPathPatterns("/**") // 不攔截 登陸、註冊、忘記密碼請求 .excludePathPatterns("/sys/sys-user/login/*", "/sys/sys-user/register") // 不攔截 swagger 請求 .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"); } }; } /** * 定義一個攔截器,用於攔截請求,並對 JWT 進行驗證 */ class JWTInterceptor extends HandlerInterceptorAdapter { /** * 訪問 controller 前被調用 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 獲取 token(從 header 或者 參數中獲取) String token = request.getHeader("token"); if (StringUtils.isBlank(token)) { token = request.getParameter("token"); } // 驗證 token 是否過時(根據時間戳比較) if (JwtUtil.checkToken(token)) { // 獲取 token 中的數據 Claims claims = JwtUtil.getTokenBody(token); JwtVo jwt = GsonUtil.fromJson(String.valueOf(claims.get("data")), JwtVo.class); // 獲取 redis 中存儲的 token String redisToken = redisUtil.get(String.valueOf(jwt.getId())); // 當前 token 與 redis 中存儲的 token 進行比較 if (StringUtils.isNotEmpty(redisToken)) { // 獲取 redis 中 token 的數據 JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(redisToken).get("data")), JwtVo.class); // 若二者 token 相同,則爲同一用戶再次訪問系統,放行 if (redisToken.equals(token)) { return true; } else if (redisJwt.getTime() <= jwt.getTime()){ // redis 中 token 生成時間戳 小於等於 當前 token 生成時間戳,即當前用戶爲最新登陸者 // redis 保存當前最新的 token,並放行 redisUtil.set(String.valueOf(redisJwt.getId()), token, 60 * 30); return true; } } } // 認證失敗,返回數據,並返回 401 狀態碼 returnJsonData(response); return false; } } /** * 返回 json 格式的數據 */ public void returnJsonData(HttpServletResponse response) { PrintWriter pw = null; response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); try { pw = response.getWriter(); // 返回 code 爲 401,表示 token 失效。 pw.print(GsonUtil.toJson(Result.error().message("token 失效或過時").code(HttpStatus.SC_UNAUTHORIZED))); } catch (IOException e) { log.error(e.getMessage()); throw new RuntimeException(e); } } }
(1)目的:
因爲後臺使用過濾器攔截了請求,使用 swagger 測試時,因爲未攜帶 token 而被攔截,致使 返回 401 狀態碼。
能夠給 Swagger 添加統一驗證參數,在請求發送前統一給 header 加上 token 參數。
(2)代碼實現:
來源於網絡,沒有深究爲何這麼寫,套用便可。
在本來 swagger 基礎上,添加以下代碼:
securitySchemes(security())
securityContexts(securityContexts());
package com.lyh.admin_template.back.common.config; import com.google.common.collect.Lists; import io.swagger.annotations.ApiOperation; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.ApiKey; import springfox.documentation.service.AuthorizationScope; import springfox.documentation.service.SecurityReference; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spi.service.contexts.SecurityContext; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList; import java.util.List; @Configuration @EnableSwagger2 @Profile({"dev","test"}) public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() // 加了ApiOperation註解的類,纔會生成接口文檔 .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) // 指定包下的類,才生成接口文檔 .apis(RequestHandlerSelectors.basePackage("com.lyh.admin_template.back")) .paths(PathSelectors.any()) .build() .securitySchemes(security()) .securityContexts(securityContexts()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("Swagger 測試") .description("Swagger 測試文檔") .termsOfServiceUrl("https://www.cnblogs.com/l-y-h/") .version("1.0.0") .build(); } private List<ApiKey> security() { return Lists.newArrayList( new ApiKey("token", "token", "header") ); } private List<SecurityContext> securityContexts() { return Lists.newArrayList( SecurityContext.builder().securityReferences(defaultAuth()) //過濾要驗證的路徑 .forPaths(PathSelectors.regex("^(?!auth).*$")) .build() ); } //增長全局認證 List<SecurityReference> defaultAuth() { AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; authorizationScopes[0] = authorizationScope; List<SecurityReference> securityReferences = new ArrayList<>(); // 因爲 securitySchemes() 方法中 header 寫入值爲 token,因此此處爲 token securityReferences.add(new SecurityReference("token", authorizationScopes)); return securityReferences; } }
(3)簡單測試一下:
首先登陸,獲取到 token。沒有設置 token 時,訪問 登出接口 會被攔截。
設置 token 後,登出接口不會被攔截。