zoukankan      html  css  js  c++  java
  • SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、redis、sms 工具类完善注册登录逻辑

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

    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, "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、注册、登录需求分析:

    (1)用户种类:
      超级管理员、普通管理员、普通用户。
    其中:
      通过注册方式创建的用户均为 普通用户。
      普通管理员由超级管理员创建。
      超级管理员使用 系统默认的数据(不可创建、修改)。
    默认:
      普通用户 -- 账号:jack 密码:123456
      普通管理员 -- 账号:admin 密码:123456
      超级管理员 -- 账号:superAdmin 密码:123456

    (2)注册需求:
      输入用户名、密码,并根据 手机号 发送验证码进行注册。
    其中:
      用户名 不能为 纯数字 组成 或者 包含 @ 符号(为了与手机号、邮箱进行区分)。
      密码前后端均采用 MD5 加密,两次加密。
      验证码时效性为 5 分钟(此项目中借用 redis 进行过期时间控制)。

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

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

      登录时限制同一账号登陆人数。
    注:
      此项目中限制同一账号登陆人数为 1 人,即同时只允许一个 账号登陆系统。

    实现限制同一账号登陆人数思路:
      并发执行时,存在同一个用户在多处同时登陆,此处为了限制只能允许一个人登陆系统,使用 redis 进行辅助。其中 key 为 用户名(或者 ID 值)、 value 为 token 值(JWT 值)。
      用户第一次访问系统时,首先判定是否为第一次登录系统(检查 redis 中是否存在 token),不存在则为第一次登录,需要将 token 存入 redis 中,并将该 token 返回给用户。存在则继续判定是否为重复登录系统(检查 token 是否一致)。token 一致,则为同一用户再次访问系统。token 不一致,则用户为重复登录系统,此时需要剔除前一个登录用户(比较当前 token 与 redis 中 token 的时间戳),如果当前 token 时间戳 大于等于 redis 中 token 时间戳,则当前时间戳为最新登录者,此时剔除 redis 中的 token 数据(即将 当前 token 数据存入 redis),如果 小于 redis 中 token 时间戳,则 redis 中 token 为最新登录者,需剔除当前 token(不返回 token 给用户,即登录失败,引导用户重新登录)。

    注意:
      此处为了实现效果,还需要修改 单点登录 逻辑,之前单点登录逻辑中,根据 token 可以直接解析出 用户信息。
      但是在此处 token 并不一定有效,因为存在同一用户在多处登录,每一次登录均会产生一个 token(定义拦截器,拦截除了登录请求外的所有请求,这样使每次登录请求均能产生 token,非登录请求验证是否存在 token),此时为了限制只允许一人登录,即只有一个 token 生效。
      需要与 redis 中存储的 token 比较后才可确认。若 两者 token 不同,需引导用户重新进行登录操作,并将最新的 token 存入 redis(感觉代码好像变得有点冗余了(=_=),毕竟每次还得与 redis 进行交互,有更方便的方法还望不吝赐教)。

    2、生成基本代码

    (1)使用 mybatis-plus 代码生成器根据 sys_user 表生成基本代码。
    此处不再重复截图,详细使用过程参考:
      https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_1
    此处只截细节部分:
    Step1:
      修改实体类,添加 @TableField(用于自动填充)、@TableLogic(用于逻辑删除) 注解。

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

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

    3、编写一个工具类( Md5Util.java) 用于加密密码

    (1)目的
      此项目中使用 MD5 进行密码加密,使用其他方式亦可。
      此加密方式网上随便搜搜就可以搜的到,代码实现也不尽相同,此处代码来源于网络。

    (2)代码实现如下:

    package com.lyh.admin_template.back.common.utils;
    
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    
    public class MD5Util {
        public static String encrypt(String strSrc) {
            try {
                char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                        '9', 'a', 'b', 'c', 'd', 'e', 'f' };
                byte[] bytes = strSrc.getBytes();
                MessageDigest md = MessageDigest.getInstance("MD5");
                md.update(bytes);
                bytes = md.digest();
                int j = bytes.length;
                char[] chars = new char[j * 2];
                int k = 0;
                for (int i = 0; i < bytes.length; i++) {
                    byte b = bytes[I];
                    chars[k++] = hexChars[b >>> 4 & 0xf];
                    chars[k++] = hexChars[b & 0xf];
                }
                return new String(chars);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("MD5加密出错!!+" + e);
            }
        }
    }

     

    4、调整 JWT 工具类、SMS 工具类

    (1)目的:
      之前考虑的有点欠缺,这两个工具类使用起来有点问题,稍作修改。

    (2)修改 JWT 工具类 JwtUtil.java
      主要修改 自定义数据 的方式,以及自定义 过期时间。

    package com.lyh.admin_template.back.common.utils;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jws;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.apache.commons.lang3.StringUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Date;
    
    /**
     * JWT 操作工具类
     */
    public class JwtUtil {
    
        // 设置默认过期时间(15 分钟)
        private static final long DEFAULT_EXPIRE = 1000L * 60 * 15;
        // 设置 jwt 生成 secret(随意指定)
        private static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
    
        /**
         * 生成 jwt token,并指定默认过期时间 15 分钟
         */
        public static String getJwtToken(Object data) {
            return getJwtToken(data, DEFAULT_EXPIRE);
        }
    
        /**
         * 生成 jwt token,根据指定的 过期时间
         */
        public static String getJwtToken(Object data, Long expire) {
            String JwtToken = Jwts.builder()
                    // 设置 jwt 类型
                    .setHeaderParam("typ", "JWT")
                    // 设置 jwt 加密方法
                    .setHeaderParam("alg", "HS256")
                    // 设置 jwt 主题
                    .setSubject("admin-user")
                    // 设置 jwt 发布时间
                    .setIssuedAt(new Date())
                    // 设置 jwt 过期时间
                    .setExpiration(new Date(System.currentTimeMillis() + expire))
                    // 设置自定义数据
                    .claim("data", data)
                    // 设置密钥与算法
                    .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                    // 生成 token
                    .compact();
            return JwtToken;
        }
    
        /**
         * 判断token是否存在与有效,true 表示未过期,false 表示过期或不存在
         */
        public static boolean checkToken(String jwtToken) {
            if (StringUtils.isEmpty(jwtToken)) {
                return false;
            }
            try {
                // 获取 token 数据
                Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
                // 判断是否过期
                return claimsJws.getBody().getExpiration().after(new Date());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        /**
         * 判断token是否存在与有效
         */
        public static boolean checkToken(HttpServletRequest request) {
            return checkToken(request.getHeader("token"));
        }
    
        /**
         * 根据 token 获取数据
         */
        public static Claims getTokenBody(HttpServletRequest request) {
            return getTokenBody(request.getHeader("token"));
        }
    
        /**
         * 根据 token 获取数据
         */
        public static Claims getTokenBody(String jwtToken) {
            if (StringUtils.isEmpty(jwtToken)) {
                return null;
            }
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            return claimsJws.getBody();
        }
    }

    (3)修改 短信发送工具类 SmsUtil.java
      主要修改 其返回数据的方式,返回 code,而非 boolean 数据。

    package com.lyh.admin_template.back.common.utils;
    
    import com.aliyuncs.CommonRequest;
    import com.aliyuncs.CommonResponse;
    import com.aliyuncs.DefaultAcsClient;
    import com.aliyuncs.IAcsClient;
    import com.aliyuncs.http.MethodType;
    import com.aliyuncs.profile.DefaultProfile;
    import com.lyh.admin_template.back.modules.sms.entity.SmsResponse;
    import lombok.Data;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    /**
     * sms 短信发送工具类
     */
    @Data
    @Component
    public class SmsUtil {
        @Value("${aliyun.accessKeyId}")
        private String accessKeyId;
        @Value("${aliyun.accessKeySecret}")
        private String accessKeySecret;
        @Value("${aliyun.signName}")
        private String signName;
        @Value("${aliyun.templateCode}")
        private String templateCode;
        @Value("${aliyun.regionId}")
        private String regionId;
        private final static String OK = "OK";
    
        /**
         * 发送短信
         */
        public String sendSms(String phoneNumbers) {
            if (StringUtils.isEmpty(phoneNumbers)) {
                return null;
            }
            DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
            IAcsClient client = new DefaultAcsClient(profile);
    
            CommonRequest request = new CommonRequest();
            // 固定参数,无需修改
            request.setSysMethod(MethodType.POST);
            request.setSysDomain("dysmsapi.aliyuncs.com");
            request.setSysVersion("2017-05-25");
            request.setSysAction("SendSms");
            request.putQueryParameter("RegionId", regionId);
    
            // 设置手机号
            request.putQueryParameter("PhoneNumbers", phoneNumbers);
            // 设置签名模板
            request.putQueryParameter("SignName", signName);
            // 设置短信模板
            request.putQueryParameter("TemplateCode", templateCode);
            // 设置短信验证码
            String code = getCode();
            request.putQueryParameter("TemplateParam", "{"code":" + code +"}");
            try {
                CommonResponse response = client.getCommonResponse(request);
                System.out.println(response.getData());
                // 转换返回的数据(需引入 Gson 依赖)
                SmsResponse smsResponse = GsonUtil.fromJson(response.getData(), SmsResponse.class);
                // 当 message 与 code 均为 ok 时,短信发送成功、否则失败
                if (SmsUtil.OK.equals(smsResponse.getMessage()) && SmsUtil.OK.equals(smsResponse.getCode())) {
                    return code;
                }
                return null;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        /**
         * 获取 6 位验证码
         */
        public String getCode() {
            return String.valueOf((int)((Math.random()*9+1)*100000));
        }
    }

    5、完善三种登录方式

    (1)三种登录方式:
    密码登录:
      用户名 + 密码。
      手机号 + 密码。

    验证码登录:
      手机号 + 验证码。

    (2)定义相关 vo 类 以及 进行 国际化、JSR303 处理
      定义 vo(viewObject)实体类去接收数据,并对其进行 JSR303 校验,当然国际化也得一起处理。

    国际化数据如下:
      详细使用请参考:https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_4

    【en】
    sys.user.name.notEmpty=Sys user name cannot be null
    sys.user.phone.notEmpty=Sys user mobile cannot be null
    sys.user.password.notEmpty=Sys user password cannot be null
    sys.user.code.notEmpty=Sys user code cannot be null
    sys.user.phone.format.error=Sys user mobile format error
    sys.user.name.format.error=Sys user name format error
    
    【zh】
    sys.user.name.notEmpty=用户名不能为空
    sys.user.phone.notEmpty=用户手机号不能为空
    sys.user.password.notEmpty=用户密码不能为空
    sys.user.code.notEmpty=验证码不能为空
    sys.user.phone.format.error=用户手机号格式错误
    sys.user.name.format.error=用户名格式错误

    vo 以及 JSR303 数据校验如下:
      定义分组,用于不同场景的数据校验(不定义也行)。
      详细使用可参考:https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_2

    【LoginGroup】
    package com.lyh.admin_template.back.common.validator.group.sys;
    
    /**
     * 新增登录的 Group 校验规则
     */
    public interface LoginGroup {
    }
    
    【RegisterGroup】
    package com.lyh.admin_template.back.common.validator.group.sys;
    
    /**
     * 新增注册的 Group 校验规则
     */
    public interface RegisterGroup {
    }

    为了逻辑看起来简单,此处使用了三种 vo 分别接受不同场景下的登录数据。
    三种 vo 如下:

    【用户名 + 密码】
    package com.lyh.admin_template.back.modules.sys.vo;
    
    import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
    import lombok.Data;
    
    import javax.validation.constraints.NotEmpty;
    
    /**
     * 登录时的视图数据类(view object),
     * 用于接收使用 用户名 + 密码 登陆的数据与操作。
     */
    @Data
    public class NamePwdLoginVo {
        @NotEmpty(message = "{sys.user.name.notEmpty}", groups = {LoginGroup.class})
        private String userName;
        @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {LoginGroup.class})
        private String password;
    }
    
    【手机号 + 密码】
    package com.lyh.admin_template.back.modules.sys.vo;
    
    import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
    import lombok.Data;
    
    import javax.validation.constraints.NotEmpty;
    import javax.validation.constraints.Pattern;
    
    /**
     * 登录时的视图数据类(view object),
     * 用于接收使用 手机号 + 密码 登陆的数据与操作。
     */
    @Data
    public class PhonePwdLoginVo {
        @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {LoginGroup.class})
        @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {LoginGroup.class})
        private String phone;
        @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {LoginGroup.class})
        private String password;
    }
    
    
    【手机号 + 验证码】
    package com.lyh.admin_template.back.modules.sys.vo;
    
    import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
    import lombok.Data;
    
    import javax.validation.constraints.NotEmpty;
    import javax.validation.constraints.Pattern;
    
    /**
     * 登录时的视图数据类(view object),
     * 用于接收使用 手机号 + 验证码 登陆的数据与操作。
     */
    @Data
    public class PhoneCodeLoginVo {
        @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {LoginGroup.class})
        @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {LoginGroup.class})
        private String phone;
        @NotEmpty(message = "{sys.user.code.notEmpty}", groups = {LoginGroup.class})
        private String code;
    }

    定义一个 vo,用于存储 jwt 自定义数据。

    package com.lyh.admin_template.back.modules.sys.vo;
    
    import lombok.Data;
    
    /**
     * 保存 JWT 对应存储的数据
     */
    @Data
    public class JwtVo {
        // 保存用户 ID
        private Long id;
        // 保存用户名
        private String name;
        // 保存用户手机号
        private String phone;
        // 保存 JWT 创建时间戳
        private Long time;
    }

      

    (3)密码登录
    主要流程:
      接收数据,并对数据校验,对通过校验的数据进行操作。
      根据数据去数据库查找数据,若查找失败,则返回相关异常数据。若存在数据,进行下面操作。
      使用 JWT 工具类将相关数据封装,并存放在 redis 中,其中以数据 ID 为 key,jwt 为 value。
      最后将 jwt 数据返回,命名为 token(前台接收数据并保存,一般存放于 cookie 的 header )。

    jwt 与 redis 逻辑需要注意一下:
      由于此项目中只允许某用户同时登陆系统的人数为 1,即某用户多次登录时,后一次登录的 jwt 需要替换掉 redis 中的 jwt,并发操作执行可能导致 后一次 jwt 的生成时机 在 redis 中 jwt 之前,直接替换会使最新的登录者被剔除,所以每次登录操作不能直接替换掉 redis 中的 jwt。
      每次登录前,生成 jwt 后,应该去查询 redis 中是否存在对应的 jwt,如果不存在,则直接将当前 jwt 存入 redis 中,如果存在,则比较两个 jwt 的时间戳,若 redis 中 jwt 大于当前 jwt,则当前登录失败,否则将当前 jwt 存入 redis 中。

    后台代码实现如下:(前台代码后续再整合)

    package com.lyh.admin_template.back.modules.sys.controller;
    
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.lyh.admin_template.back.common.utils.*;
    import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
    import com.lyh.admin_template.back.modules.sys.entity.SysUser;
    import com.lyh.admin_template.back.modules.sys.service.SysUserService;
    import com.lyh.admin_template.back.modules.sys.vo.JwtVo;
    import com.lyh.admin_template.back.modules.sys.vo.NamePwdLoginVo;
    import com.lyh.admin_template.back.modules.sys.vo.PhonePwdLoginVo;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.Date;
    
    /**
     * <p>
     * 系统用户表 前端控制器
     * </p>
     *
     * @author lyh
     * @since 2020-07-02
     */
    @RestController
    @RequestMapping("/sys/sys-user")
    @Api(tags = "用户登录、注册操作")
    public class SysUserController {
    
        /**
         * 用于操作 sys_user 表
         */
        @Autowired
        private SysUserService sysUserService;
        /**
         * 用于操作 redis
         */
        @Autowired
        private RedisUtil redisUtil;
        /**
         * 常量,表示用户密码登录操作
         */
        private static final String USER_NAME_STATUS = "0";
        /**
         * 常量,表示手机号密码登录操作
         */
        private static final String PHONE_STATUS = "1";
    
        /**
         * 获取 jwt
         * @return jwt
         */
        private String getJwt(SysUser sysUser) {
            // 获取需要保存在 jwt 中的数据
            JwtVo jwtVo = new JwtVo();
            jwtVo.setId(sysUser.getId());
            jwtVo.setName(sysUser.getName());
            jwtVo.setPhone(sysUser.getMobile());
            jwtVo.setTime(new Date().getTime());
            // 获取 jwt 数据,设置过期时间为 30 分钟
            String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30);
            // 判断用户是否重复登录(code 有值则重复登录,需要保留最新的登录者,剔除前一个登录者)
            String code = redisUtil.get(String.valueOf(sysUser.getId()));
            // 获取当前时间戳
            Long currentTime = new Date().getTime();
            // 如果 redis 中存在 jwt 数据,则根据时间戳比较谁为最新的登陆者
            if (StringUtils.isNotEmpty(code)) {
                // 获取 redis 中存储的 jwt 数据
                JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class);
                // redis jwt 大于 当前时间戳,则 redis 中 jwt 为最新登录者,当前登录失败
                if (redisJwt.getTime() > currentTime) {
                    return null;
                }
            }
            // 把数据存放在 redis 中,设置过期时间为 30 分钟
            redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30);
            return jwt;
        }
    
        /**
         * 使用密码进行真实登录操作
         * @param account 账号(用户名或手机号)
         * @param pwd 密码
         * @param status 是否使用用户名登录(0 表示用户名登录,1 表示手机号登录)
         * @return jwt
         */
        private String pwdLogin(String account, String pwd, String status) {
            // 新增查询条件
            QueryWrapper queryWrapper = new QueryWrapper();
            // 如果是用户名 + 密码登录,则根据 姓名 + 密码 查找数据
            if (USER_NAME_STATUS.equals(status)) {
                queryWrapper.eq("name", account);
            }
            // 如果是手机号 + 密码登录,则根据 手机号 + 密码 查找数据
            if (PHONE_STATUS.equals(status)) {
                queryWrapper.eq("mobile", account);
            }
            // 添加密码条件,密码进行 MD5 加密后再与数据库数据比较
            queryWrapper.eq("password", MD5Util.encrypt(pwd));
            // 获取用户数据
            SysUser sysUser = sysUserService.getOne(queryWrapper);
            // 如果存在用户数据
            if (sysUser != null) {
                return getJwt(sysUser);
            }
            return null;
        }
    
        @ApiOperation(value = "使用用户名、密码登录")
        @PostMapping("/login/namePwdLogin")
        public Result namePwdLogin(@Validated({LoginGroup.class}) @RequestBody NamePwdLoginVo namePwdLoginVo) {
            String jwt = pwdLogin(namePwdLoginVo.getUserName(), namePwdLoginVo.getPassword(), USER_NAME_STATUS);
            if (StringUtils.isNotEmpty(jwt)) {
                return Result.ok().message("登录成功").data("token", jwt);
            }
            return Result.error().message("登录失败");
        }
    
        @ApiOperation(value = "使用手机号、密码登录")
        @PostMapping("/login/phonePwdLogin")
        public Result phonePwdLogin(@Validated({LoginGroup.class}) @RequestBody PhonePwdLoginVo phonePwdLoginVo) {
            String jwt = pwdLogin(phonePwdLoginVo.getPhone(), phonePwdLoginVo.getPassword(), PHONE_STATUS);
            if (StringUtils.isNotEmpty(jwt)) {
                return Result.ok().message("登录成功").data("token", jwt);
            }
            return Result.error().message("登录失败");
        }
    }

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

    (4)验证码登录
    获取验证码流程:
      首先获取验证码(此处不考虑并发情况,毕竟手机号只有一个用户能用,应该避免重复获取验证码的情况),并将其存放与 redis 中,设置过期时间为 5 分钟。
      为了避免重复获取验证码,可以根据其已过期时间是否小于 1 分钟判断,即 1 分钟内不可以重复获取验证码。

    验证码登录流程:
      接收数据,并校验数据,通过检验的数据进行下面处理。
      先检查 redis 中是否存在验证码,若不存在验证码(验证码不存在或失效),则登录失败。否则,根据手机号去查询用户数据,生成 jwt,存放与 redis 中并返回。

    后台代码实现如下:(前台代码后续再整合)

    package com.lyh.admin_template.back.modules.sys.controller;
    
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.lyh.admin_template.back.common.utils.*;
    import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
    import com.lyh.admin_template.back.modules.sys.entity.SysUser;
    import com.lyh.admin_template.back.modules.sys.service.SysUserService;
    import com.lyh.admin_template.back.modules.sys.vo.JwtVo;
    import com.lyh.admin_template.back.modules.sys.vo.PhoneCodeLoginVo;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.Date;
    
    /**
     * <p>
     * 系统用户表 前端控制器
     * </p>
     *
     * @author lyh
     * @since 2020-07-02
     */
    @RestController
    @RequestMapping("/sys/sys-user")
    @Api(tags = "用户登录、注册操作")
    public class SysUserController {
    
        /**
         * 用于操作 sys_user 表
         */
        @Autowired
        private SysUserService sysUserService;
        /**
         * 用于操作 redis
         */
        @Autowired
        private RedisUtil redisUtil;
        /**
         * 用于操作 短信验证码发送
         */
        @Autowired
        private SmsUtil smsUtil;
    
        /**
         * 获取 jwt
         * @return jwt
         */
        private String getJwt(SysUser sysUser) {
            // 获取需要保存在 jwt 中的数据
            JwtVo jwtVo = new JwtVo();
            jwtVo.setId(sysUser.getId());
            jwtVo.setName(sysUser.getName());
            jwtVo.setPhone(sysUser.getMobile());
            jwtVo.setTime(new Date().getTime());
            // 获取 jwt 数据,设置过期时间为 30 分钟
            String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30);
            // 判断用户是否重复登录(code 有值则重复登录,需要保留最新的登录者,剔除前一个登录者)
            String code = redisUtil.get(String.valueOf(sysUser.getId()));
            // 获取当前时间戳
            Long currentTime = new Date().getTime();
            // 如果 redis 中存在 jwt 数据,则根据时间戳比较谁为最新的登陆者
            if (StringUtils.isNotEmpty(code)) {
                // 获取 redis 中存储的 jwt 数据
                JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class);
                // redis jwt 大于 当前时间戳,则 redis 中 jwt 为最新登录者,当前登录失败
                if (redisJwt.getTime() > currentTime) {
                    return null;
                }
            }
            // 把数据存放在 redis 中,设置过期时间为 30 分钟
            redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30);
            return jwt;
        }
    
        /**
         * 使用 验证码进行真实登录操作
         * @param phone 手机号
         * @param code 验证码
         * @return jwt
         */
        private String codeLogin(String phone, String code) {
            // 获取 redis 中存放的验证码
            String redisCode = redisUtil.get(phone);
            // 存在验证码,且输入的验证码与 redis 存放的验证码相同,则根据手机号去数据库查询数据
            if (StringUtils.isNotEmpty(redisCode) && code.equals(redisCode)) {
                // 新增查询条件
                QueryWrapper queryWrapper = new QueryWrapper();
                // 根据手机号去查询数据
                queryWrapper.eq("mobile", phone);
                SysUser sysUser = sysUserService.getOne(queryWrapper);
                // 如果存在用户数据
                if (sysUser != null) {
                    return getJwt(sysUser);
                }
            }
            return null;
        }
    
        @ApiOperation(value = "使用手机号、验证码登录")
        @PostMapping("/login/phoneCodeLogin")
        public Result phoneCodeLogin(@Validated({LoginGroup.class}) @RequestBody PhoneCodeLoginVo phoneCodeLoginVo) {
            String jwt = codeLogin(phoneCodeLoginVo.getPhone(), phoneCodeLoginVo.getCode());
            if (StringUtils.isNotEmpty(jwt)) {
                return Result.ok().message("登录成功").data("token", jwt);
            }
            return Result.error().message("登录失败");
        }
    
        @ApiOperation(value = "获取短信验证码")
        @GetMapping("/login/getCode")
        public Result getCode(String phone) {
            // 设置默认过期时间
            Long defaultTime = 60L * 5;
            // 先判断 redis 中是否存储过验证码(设置期限为 1 分钟),防止重复获取验证码
            Long expire = redisUtil.getExpire(phone);
            if (expire != null && (defaultTime - expire < 60)) {
                return Result.error().message("验证码已发送,1 分钟后可再次获取验证码");
            } else {
                // 获取 短信验证码
                String code = smsUtil.sendSms(phone);
                if (StringUtils.isNotEmpty(code)) {
                    // 把验证码存放在 redis 中,并设置 过期时间 为 5 分钟
                    redisUtil.set(phone, code, defaultTime);
                    return Result.ok().message("验证码获取成功").data("code", code);
                }
            }
            return Result.error().message("验证码获取失败");
        }
    }

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

    6、完善注册逻辑

    (1)主要流程:
      先获取验证码,验证码处理与验证码登录相同(此处不再重复)。
      输入用户名、密码、手机号、以及得到的验证码,后端对数据进行校验,校验通过的数据进行下面操作。
      先检查 redis 中是否存在验证码,若不存在验证码(验证码不存在或失效)或者验证码与当前验证码不同,则注册失败,如存在且相同,则进行下面操作。
      根据用户名与手机号,对数据库数据进行查找,若存在数据则注册失败,若不存在,则向数据库添加数据。由于给用户名和手机号添加了唯一性约束,所以可以直接进行插入操作,存在数据会返回异常,不存在数据会直接插入。

    (2)代码实现如下:
      首先定义一个 vo 类,用于接收数据。

    package com.lyh.admin_template.back.modules.sys.vo;
    
    import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup;
    import lombok.Data;
    
    import javax.validation.constraints.NotEmpty;
    import javax.validation.constraints.Pattern;
    
    /**
     * 注册时对应的视图数据类(view object),
     * 用于接收并处理 注册时的数据。
     */
    @Data
    public class RegisterVo {
        @NotEmpty(message = "{sys.user.name.notEmpty}", groups = {RegisterGroup.class})
        @Pattern(message = "{sys.user.name.format.error}", regexp = "^.*[^\d].*$", groups = {RegisterGroup.class})
        private String userName;
        @NotEmpty(message = "{sys.user.password.notEmpty}", groups = {RegisterGroup.class})
        private String password;
        @NotEmpty(message = "{sys.user.phone.notEmpty}", groups = {RegisterGroup.class})
        @Pattern(message = "{sys.user.phone.format.error}", regexp = "0?(13|14|15|18|17)[0-9]{9}", groups = {RegisterGroup.class})
        private String phone;
        @NotEmpty(message = "{sys.user.code.notEmpty}", groups = {RegisterGroup.class})
        private String code;
    }

    接口如下:
      由于 注册 用户均属于 普通用户,所以注册的同时需要给其绑定角色,即向 sys_user 插入数据后,还需要向 sys_user_role   插入数据(需要使用代码生成器生成相关代码,此处省略)。
      由于出现多表插入操作,此处使用 @Transactional 对事务进行控制。
    注:
      @Transactional 需要写在 Service 层,写在 Controller 层不生效。

    在 service 层定义一个 saveUser 方法。

    package com.lyh.admin_template.back.modules.sys.service;
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.lyh.admin_template.back.modules.sys.entity.SysUser;
    
    /**
     * <p>
     * 系统用户表 服务类
     * </p>
     *
     * @author lyh
     * @since 2020-07-02
     */
    public interface SysUserService extends IService<SysUser> {
        public boolean saveUser(SysUser sysUser);
    }

    在 service 实现类中,重写方法并完善注册逻辑。

    package com.lyh.admin_template.back.modules.sys.service.impl;
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.lyh.admin_template.back.modules.sys.entity.SysRole;
    import com.lyh.admin_template.back.modules.sys.entity.SysUser;
    import com.lyh.admin_template.back.modules.sys.entity.SysUserRole;
    import com.lyh.admin_template.back.modules.sys.mapper.SysUserMapper;
    import com.lyh.admin_template.back.modules.sys.service.SysRoleService;
    import com.lyh.admin_template.back.modules.sys.service.SysUserRoleService;
    import com.lyh.admin_template.back.modules.sys.service.SysUserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Isolation;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    
    /**
     * <p>
     * 系统用户表 服务实现类
     * </p>
     *
     * @author lyh
     * @since 2020-07-02
     */
    @Service
    public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    
        @Autowired
        private SysRoleService sysRoleService;
        @Autowired
        private SysUserRoleService sysUserRoleService;
    
        /**
         * 先插入数据到 用户表 sys_user 中。
         * 再获取数据 ID 与 角色 ID 并插入到 用户角色表 sys_user_role 中。
         * @param sysUser 用户数据
         * @return true 表示插入成功, false 表示失败
         */
        @Override
        @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,timeout=36000,rollbackFor=Exception.class)
        public boolean saveUser(SysUser sysUser) {
            // 向 sys_user 表中插入数据
            if (this.save(sysUser)) {
                // 获取当前用户的 ID
                QueryWrapper queryWrapper = new QueryWrapper();
                queryWrapper.eq("name", sysUser.getName());
                SysUser sysUser2 = this.getOne(queryWrapper);
    
                // 获取普通用户角色 ID
                QueryWrapper queryWrapper2 = new QueryWrapper();
                queryWrapper2.eq("role_name", "user");
                SysRole sysRole = sysRoleService.getOne(queryWrapper2);
    
                // 插入到 用户-角色 表中(sys_user_role)
                SysUserRole sysUserRole = new SysUserRole();
                sysUserRole.setUserId(sysUser2.getId()).setRoleId(sysRole.getId());
                return sysUserRoleService.save(sysUserRole);
            }
            return false;
        }
    }

      

    controller 层接口如下:

    package com.lyh.admin_template.back.modules.sys.controller;
    
    
    import com.lyh.admin_template.back.common.utils.MD5Util;
    import com.lyh.admin_template.back.common.utils.RedisUtil;
    import com.lyh.admin_template.back.common.utils.Result;
    import com.lyh.admin_template.back.common.utils.SmsUtil;
    import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup;
    import com.lyh.admin_template.back.modules.sys.entity.SysUser;
    import com.lyh.admin_template.back.modules.sys.service.SysUserService;
    import com.lyh.admin_template.back.modules.sys.vo.RegisterVo;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * <p>
     * 系统用户表 前端控制器
     * </p>
     *
     * @author lyh
     * @since 2020-07-02
     */
    @RestController
    @RequestMapping("/sys/sys-user")
    @Api(tags = "用户登录、注册操作")
    public class SysUserController {
    
        /**
         * 用于操作 sys_user 表
         */
        @Autowired
        private SysUserService sysUserService;
        /**
         * 用于操作 redis
         */
        @Autowired
        private RedisUtil redisUtil;
        /**
         * 用于操作 短信验证码发送
         */
        @Autowired
        private SmsUtil smsUtil;
    
        @ApiOperation(value = "获取短信验证码")
        @GetMapping("/login/getCode")
        public Result getCode(String phone) {
            // 设置默认过期时间
            Long defaultTime = 60L * 5;
            // 先判断 redis 中是否存储过验证码(设置期限为 1 分钟),防止重复获取验证码
            Long expire = redisUtil.getExpire(phone);
            if (expire != null && (defaultTime - expire < 60)) {
                return Result.error().message("验证码已发送,1 分钟后可再次获取验证码");
            } else {
                // 获取 短信验证码
                String code = smsUtil.sendSms(phone);
                if (StringUtils.isNotEmpty(code)) {
                    // 把验证码存放在 redis 中,并设置 过期时间 为 5 分钟
                    redisUtil.set(phone, code, defaultTime);
                    return Result.ok().message("验证码获取成功").data("code", code);
                }
            }
            return Result.error().message("验证码获取失败");
        }
    
        @ApiOperation(value = "用户注册")
        @PostMapping("/register")
        public Result register(@Validated({RegisterGroup.class}) @RequestBody RegisterVo registerVo) {
            if (save(registerVo)) {
                return Result.ok().message("用户注册成功");
            }
            return Result.error().message("用户注册失败");
        }
    
        /**
         * 真实注册操作
         * @param registerVo 注册数据
         * @return true 为插入成功, false 为失败
         */
        public boolean save(RegisterVo registerVo) {
            // 判断 redis 中是否存在 验证码
            String code = redisUtil.get(registerVo.getPhone());
            // redis 中存在验证码且与当前验证码相同
            if (StringUtils.isNotEmpty(code) && code.equals(registerVo.getCode())) {
                SysUser sysUser = new SysUser();
                sysUser.setName(registerVo.getUserName()).setPassword(MD5Util.encrypt(registerVo.getPassword()));
                sysUser.setMobile(registerVo.getPhone());
                return sysUserService.saveUser(sysUser);
            }
            return false;
        }
    }

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

    7、完善登出逻辑

    (1)目的:
      让客户端 保存的 token 失效,则用户再次访问系统后由于 token 失效而无法继续访问,需重新登录后才可访问。

    后台操作(非必须操作):
      返回一个 过期时间为 1 秒的 token(或返回一个无效 token),并删除 redis 中的 token。
    前台操作:
      前台保存无效的 token。
      清除 token(简单粗暴)。

    (2)代码如下:(仅后台代码,前台代码此处省略、后续整合)

    package com.lyh.admin_template.back.modules.sys.controller;
    
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.lyh.admin_template.back.common.utils.JwtUtil;
    import com.lyh.admin_template.back.common.utils.RedisUtil;
    import com.lyh.admin_template.back.common.utils.Result;
    import com.lyh.admin_template.back.modules.sys.entity.SysUser;
    import com.lyh.admin_template.back.modules.sys.service.SysUserService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * <p>
     * 系统用户表 前端控制器
     * </p>
     *
     * @author lyh
     * @since 2020-07-02
     */
    @RestController
    @RequestMapping("/sys/sys-user")
    @Api(tags = "用户登录、注册操作")
    public class SysUserController {
    
        /**
         * 用于操作 sys_user 表
         */
        @Autowired
        private SysUserService sysUserService;
        /**
         * 用于操作 redis
         */
        @Autowired
        private RedisUtil redisUtil;
    
        @ApiOperation(value = "用户登出")
        @GetMapping("/logout")
        public Result logout(@RequestParam String userName) {
            // 先获取用户数据
            QueryWrapper queryWrapper = new QueryWrapper();
            queryWrapper.eq("name", userName);
            SysUser sysUser = sysUserService.getOne(queryWrapper);
            // 用户存在时
            if (sysUser != null) {
                // 生成并返回一个无效的 token
                String jwt = JwtUtil.getJwtToken(null, 1000L);
                // 删除 redis 中的 token
                redisUtil.del(String.valueOf(sysUser.getId()));
                return Result.ok().message("登出成功").data("token", jwt);
            }
            return Result.error().message("登出失败");
        }
    }

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

    8、完整的登录、注册、登出接口代码

      包括三种登录接口、注册接口、登出接口、获取验证码接口。

    package com.lyh.admin_template.back.modules.sys.controller;
    
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.lyh.admin_template.back.common.utils.*;
    import com.lyh.admin_template.back.common.validator.group.sys.LoginGroup;
    import com.lyh.admin_template.back.common.validator.group.sys.RegisterGroup;
    import com.lyh.admin_template.back.modules.sys.entity.SysUser;
    import com.lyh.admin_template.back.modules.sys.service.SysUserService;
    import com.lyh.admin_template.back.modules.sys.vo.*;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.http.HttpStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.Date;
    
    /**
     * <p>
     * 系统用户表 前端控制器
     * </p>
     *
     * @author lyh
     * @since 2020-07-02
     */
    @RestController
    @RequestMapping("/sys/sys-user")
    @Api(tags = "用户登录、注册操作")
    public class SysUserController {
    
        /**
         * 用于操作 sys_user 表
         */
        @Autowired
        private SysUserService sysUserService;
        /**
         * 用于操作 redis
         */
        @Autowired
        private RedisUtil redisUtil;
        /**
         * 用于操作 短信验证码发送
         */
        @Autowired
        private SmsUtil smsUtil;
        /**
         * 常量,表示用户密码登录操作
         */
        private static final String USER_NAME_STATUS = "0";
        /**
         * 常量,表示手机号密码登录操作
         */
        private static final String PHONE_STATUS = "1";
    
        /**
         * 获取 jwt
         * @return jwt
         */
        private String getJwt(SysUser sysUser) {
            // 获取需要保存在 jwt 中的数据
            JwtVo jwtVo = new JwtVo();
            jwtVo.setId(sysUser.getId());
            jwtVo.setName(sysUser.getName());
            jwtVo.setPhone(sysUser.getMobile());
            jwtVo.setTime(new Date().getTime());
            // 获取 jwt 数据,设置过期时间为 30 分钟
            String jwt = JwtUtil.getJwtToken(jwtVo, 1000L * 60 * 30);
            // 判断用户是否重复登录(code 有值则重复登录,需要保留最新的登录者,剔除前一个登录者)
            String code = redisUtil.get(String.valueOf(sysUser.getId()));
            // 获取当前时间戳
            Long currentTime = new Date().getTime();
            // 如果 redis 中存在 jwt 数据,则根据时间戳比较谁为最新的登陆者
            if (StringUtils.isNotEmpty(code)) {
                // 获取 redis 中存储的 jwt 数据
                JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(code).get("data")), JwtVo.class);
                // redis jwt 大于 当前时间戳,则 redis 中 jwt 为最新登录者,当前登录失败
                if (redisJwt.getTime() > currentTime) {
                    return null;
                }
            }
            // 把数据存放在 redis 中,设置过期时间为 30 分钟
            redisUtil.set(String.valueOf(sysUser.getId()), jwt, 60 * 30);
            return jwt;
        }
    
        /**
         * 使用密码进行真实登录操作
         * @param account 账号(用户名或手机号)
         * @param pwd 密码
         * @param status 是否使用用户名登录(0 表示用户名登录,1 表示手机号登录)
         * @return jwt
         */
        private String pwdLogin(String account, String pwd, String status) {
            // 新增查询条件
            QueryWrapper queryWrapper = new QueryWrapper();
            // 如果是用户名 + 密码登录,则根据 姓名 + 密码 查找数据
            if (USER_NAME_STATUS.equals(status)) {
                queryWrapper.eq("name", account);
            }
            // 如果是手机号 + 密码登录,则根据 手机号 + 密码 查找数据
            if (PHONE_STATUS.equals(status)) {
                queryWrapper.eq("mobile", account);
            }
            // 添加密码条件,密码进行 MD5 加密后再与数据库数据比较
            queryWrapper.eq("password", MD5Util.encrypt(pwd));
            // 获取用户数据
            SysUser sysUser = sysUserService.getOne(queryWrapper);
            // 如果存在用户数据
            if (sysUser != null) {
                return getJwt(sysUser);
            }
            return null;
        }
    
        /**
         * 使用 验证码进行真实登录操作
         * @param phone 手机号
         * @param code 验证码
         * @return jwt
         */
        private String codeLogin(String phone, String code) {
            // 获取 redis 中存放的验证码
            String redisCode = redisUtil.get(phone);
            // 存在验证码,且输入的验证码与 redis 存放的验证码相同,则根据手机号去数据库查询数据
            if (StringUtils.isNotEmpty(redisCode) && code.equals(redisCode)) {
                // 新增查询条件
                QueryWrapper queryWrapper = new QueryWrapper();
                // 根据手机号去查询数据
                queryWrapper.eq("mobile", phone);
                SysUser sysUser = sysUserService.getOne(queryWrapper);
                // 如果存在用户数据
                if (sysUser != null) {
                    return getJwt(sysUser);
                }
            }
            return null;
        }
    
        @ApiOperation(value = "使用用户名、密码登录")
        @PostMapping("/login/namePwdLogin")
        public Result namePwdLogin(@Validated({LoginGroup.class}) @RequestBody NamePwdLoginVo namePwdLoginVo) {
            String jwt = pwdLogin(namePwdLoginVo.getUserName(), namePwdLoginVo.getPassword(), USER_NAME_STATUS);
            if (StringUtils.isNotEmpty(jwt)) {
                return Result.ok().message("登录成功").data("token", jwt);
            }
            return Result.error().message("登录失败").code(HttpStatus.SC_UNAUTHORIZED);
        }
    
        @ApiOperation(value = "使用手机号、密码登录")
        @PostMapping("/login/phonePwdLogin")
        public Result phonePwdLogin(@Validated({LoginGroup.class}) @RequestBody PhonePwdLoginVo phonePwdLoginVo) {
            String jwt = pwdLogin(phonePwdLoginVo.getPhone(), phonePwdLoginVo.getPassword(), PHONE_STATUS);
            if (StringUtils.isNotEmpty(jwt)) {
                return Result.ok().message("登录成功").data("token", jwt);
            }
            return Result.error().message("登录失败").code(HttpStatus.SC_UNAUTHORIZED);
        }
    
        @ApiOperation(value = "使用手机号、验证码登录")
        @PostMapping("/login/phoneCodeLogin")
        public Result phoneCodeLogin(@Validated({LoginGroup.class}) @RequestBody PhoneCodeLoginVo phoneCodeLoginVo) {
            String jwt = codeLogin(phoneCodeLoginVo.getPhone(), phoneCodeLoginVo.getCode());
            if (StringUtils.isNotEmpty(jwt)) {
                return Result.ok().message("登录成功").data("token", jwt);
            }
            return Result.error().message("登录失败").code(HttpStatus.SC_UNAUTHORIZED);
        }
    
        @ApiOperation(value = "获取短信验证码")
        @GetMapping("/login/getCode")
        public Result getCode(String phone) {
            // 设置默认过期时间
            Long defaultTime = 60L * 5;
            // 先判断 redis 中是否存储过验证码(设置期限为 1 分钟),防止重复获取验证码
            Long expire = redisUtil.getExpire(phone);
            if (expire != null && (defaultTime - expire < 60)) {
                return Result.error().message("验证码已发送,1 分钟后可再次获取验证码");
            } else {
                // 获取 短信验证码
                String code = smsUtil.sendSms(phone);
                if (StringUtils.isNotEmpty(code)) {
                    // 把验证码存放在 redis 中,并设置 过期时间 为 5 分钟
                    redisUtil.set(phone, code, defaultTime);
                    return Result.ok().message("验证码获取成功").data("code", code);
                }
            }
            return Result.error().message("验证码获取失败");
        }
    
        @ApiOperation(value = "用户登出")
        @GetMapping("/logout")
        public Result logout(@RequestParam String userName) {
            // 先获取用户数据
            QueryWrapper queryWrapper = new QueryWrapper();
            queryWrapper.eq("name", userName);
            SysUser sysUser = sysUserService.getOne(queryWrapper);
            // 用户存在时
            if (sysUser != null) {
                // 生成并返回一个无效的 token
                String jwt = JwtUtil.getJwtToken(null, 1000L);
                // 删除 redis 中的 token
                redisUtil.del(String.valueOf(sysUser.getId()));
                return Result.ok().message("登出成功").data("token", jwt);
            }
            return Result.error().message("登出失败");
        }
    
        @ApiOperation(value = "用户注册")
        @PostMapping("/register")
        public Result register(@Validated({RegisterGroup.class}) @RequestBody RegisterVo registerVo) {
            if (save(registerVo)) {
                return Result.ok().message("用户注册成功");
            }
            return Result.error().message("用户注册失败").code(HttpStatus.SC_UNAUTHORIZED);
        }
    
        /**
         * 真实注册操作
         * @param registerVo 注册数据
         * @return true 为插入成功, false 为失败
         */
        public boolean save(RegisterVo registerVo) {
            // 判断 redis 中是否存在 验证码
            String code = redisUtil.get(registerVo.getPhone());
            // redis 中存在验证码且与当前验证码相同
            if (StringUtils.isNotEmpty(code) && code.equals(registerVo.getCode())) {
                SysUser sysUser = new SysUser();
                sysUser.setName(registerVo.getUserName()).setPassword(MD5Util.encrypt(registerVo.getPassword()));
                sysUser.setMobile(registerVo.getPhone());
                return sysUserService.saveUser(sysUser);
            }
            return false;
        }
    }

    9、定义一个拦截器,用于拦截除登录注册请求外的所有请求

    (1)目的:
      由于采用 JWT 进行单点登录,每次请求前都需要对 token 进行校验,为了避免在接口中重复进行校验操作,此处可以使用拦截器,拦截每个请求,校验通过后放行请求并返回数据,校验未通过直接返回错误数据。
      拦截器需要直接放行登录、注册等请求,未登录、注册时没有 token 数据,只有登录后才有 token 数据,拦截了 登录、注册请求后,不会产生 token,成为一个死循环。

    (2)代码实现如下:
    Step1:定义一个拦截器
      对于拦截的请求,首先检查 token 是否过期,过期返回 401 状态码。未过期进行下面操作。
      获取 token 信息,并根据 token 的 id 值从 redis 中获取 redis 中存储的 token。若 redis 中不存在 token,即用户未登录,返回 401 状态码。存在 token 则进行下面操作。
      若两 token 相同,即 同一用户再次访问系统,放行该请求。token 不同,则意味着 同一用户 在不同地方进行登录,需保留最新的登录者信息。根据时间戳比较,谁大谁为最新登录者,并将其值保存在 redis 中。

    /**
     * 定义一个拦截器,用于拦截请求,并对 JWT 进行验证
     */
    class JWTInterceptor extends HandlerInterceptorAdapter {
    
        /**
         * 访问 controller 前被调用
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 获取 token(从 header 或者 参数中获取)
            String token = request.getHeader("token");
            if (StringUtils.isBlank(token)) {
                token = request.getParameter("token");
            }
            // 验证 token 是否过期(根据时间戳比较)
            if (JwtUtil.checkToken(token)) {
                // 获取 token 中的数据
                Claims claims = JwtUtil.getTokenBody(token);
                System.out.println(claims.getExpiration());
                JwtVo jwt = GsonUtil.fromJson(String.valueOf(claims.get("data")), JwtVo.class);
                // 获取 redis 中存储的 token
                String redisToken = redisUtil.get(String.valueOf(jwt.getId()));
                // 当前 token 与 redis 中存储的 token 进行比较
                if (StringUtils.isNotEmpty(redisToken)) {
                    // 获取 redis 中 token 的数据
                    JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(redisToken).get("data")), JwtVo.class);
                    // 若两者 token 相同,则为同一用户再次访问系统,放行
                    if (redisToken.equals(token)) {
                        return true;
                    } else if (redisJwt.getTime() <= jwt.getTime()){
                        // redis 中 token 生成时间戳 小于等于 当前 token 生成时间戳,即当前用户为最新登录者
                        // redis 保存当前最新的 token,并放行
                        redisUtil.set(String.valueOf(redisJwt.getId()), token, 60 * 30);
                        return true;
                    }
                }
            }
            // 认证失败,返回数据,并返回 401 状态码
            returnJsonData(response);
            return false;
        }
    }

      

    Step2:定义拦截请求后的数据返回结果。
      返回 json 数据,并定义 code 为 401(授权失败)。

    /**
     * 返回 json 格式的数据
     */
    public void returnJsonData(HttpServletResponse response) {
        PrintWriter pw = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            pw = response.getWriter();
            // 返回 code 为 401,表示 token 失效。
            pw.print(GsonUtil.toJson(Result.error().message("token 失效或过期").code(HttpStatus.SC_UNAUTHORIZED)));
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e);
        }
    }

    Step3:定义拦截请求规则:

    /**
     * 定义拦截器,拦截请求。
     * 其中:
     *      addPathPatterns 用于添加需要拦截的请求。
     *      excludePathPatterns 用于添加不需要拦截的请求。
     * 此处:
     *      拦截所有请求,但是排除 登录、注册 请求 以及 swagger 请求。
     */
    @Bean(name = "JWTInterceptor")
    public WebMvcConfigurer JWTInterceptor() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(new JWTInterceptor())
                    // 拦截所有请求
                    .addPathPatterns("/**")
                    // 不拦截 登录、注册、忘记密码请求
                    .excludePathPatterns("/sys/sys-user/login/*", "/sys/sys-user/register")
                    // 不拦截 swagger 请求
                    .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
            }
        };
    }

    完整拦截逻辑:

    package com.lyh.admin_template.back.common.config;
    
    import com.lyh.admin_template.back.common.utils.GsonUtil;
    import com.lyh.admin_template.back.common.utils.JwtUtil;
    import com.lyh.admin_template.back.common.utils.RedisUtil;
    import com.lyh.admin_template.back.common.utils.Result;
    import com.lyh.admin_template.back.modules.sys.vo.JwtVo;
    import io.jsonwebtoken.Claims;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.http.HttpStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    @Slf4j
    @Configuration
    public class JWTConfig {
    
        @Autowired
        private RedisUtil redisUtil;
    
        /**
         * 定义拦截器,拦截请求。
         * 其中:
         *      addPathPatterns 用于添加需要拦截的请求。
         *      excludePathPatterns 用于添加不需要拦截的请求。
         * 此处:
         *      拦截所有请求,但是排除 登录、注册 请求 以及 swagger 请求。
         */
        @Bean(name = "JWTInterceptor")
        public WebMvcConfigurer JWTInterceptor() {
            return new WebMvcConfigurer() {
                @Override
                public void addInterceptors(InterceptorRegistry registry) {
                    registry.addInterceptor(new JWTInterceptor())
                        // 拦截所有请求
                        .addPathPatterns("/**")
                        // 不拦截 登录、注册、忘记密码请求
                        .excludePathPatterns("/sys/sys-user/login/*", "/sys/sys-user/register")
                        // 不拦截 swagger 请求
                        .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
                }
            };
        }
    
        /**
         * 定义一个拦截器,用于拦截请求,并对 JWT 进行验证
         */
        class JWTInterceptor extends HandlerInterceptorAdapter {
    
            /**
             * 访问 controller 前被调用
             */
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                // 获取 token(从 header 或者 参数中获取)
                String token = request.getHeader("token");
                if (StringUtils.isBlank(token)) {
                    token = request.getParameter("token");
                }
                // 验证 token 是否过期(根据时间戳比较)
                if (JwtUtil.checkToken(token)) {
                    // 获取 token 中的数据
                    Claims claims = JwtUtil.getTokenBody(token);
                    JwtVo jwt = GsonUtil.fromJson(String.valueOf(claims.get("data")), JwtVo.class);
                    // 获取 redis 中存储的 token
                    String redisToken = redisUtil.get(String.valueOf(jwt.getId()));
                    // 当前 token 与 redis 中存储的 token 进行比较
                    if (StringUtils.isNotEmpty(redisToken)) {
                        // 获取 redis 中 token 的数据
                        JwtVo redisJwt = GsonUtil.fromJson(String.valueOf(JwtUtil.getTokenBody(redisToken).get("data")), JwtVo.class);
                        // 若两者 token 相同,则为同一用户再次访问系统,放行
                        if (redisToken.equals(token)) {
                            return true;
                        } else if (redisJwt.getTime() <= jwt.getTime()){
                            // redis 中 token 生成时间戳 小于等于 当前 token 生成时间戳,即当前用户为最新登录者
                            // redis 保存当前最新的 token,并放行
                            redisUtil.set(String.valueOf(redisJwt.getId()), token, 60 * 30);
                            return true;
                        }
                    }
                }
                // 认证失败,返回数据,并返回 401 状态码
                returnJsonData(response);
                return false;
            }
        }
    
        /**
         * 返回 json 格式的数据
         */
        public void returnJsonData(HttpServletResponse response) {
            PrintWriter pw = null;
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            try {
                pw = response.getWriter();
                // 返回 code 为 401,表示 token 失效。
                pw.print(GsonUtil.toJson(Result.error().message("token 失效或过期").code(HttpStatus.SC_UNAUTHORIZED)));
            } catch (IOException e) {
                log.error(e.getMessage());
                throw new RuntimeException(e);
            }
        }
    }

    10、给 Swagger 添加统一验证参数(设置 token)

    (1)目的:
      由于后台使用过滤器拦截了请求,使用 swagger 测试时,由于未携带 token 而被拦截,导致 返回 401 状态码。
      可以给 Swagger 添加统一验证参数,在请求发送前统一给 header 加上 token 参数。

    (2)代码实现:
      来源于网络,没有深究为什么这么写,套用即可。
    在原本 swagger 基础上,添加如下代码:

      securitySchemes(security())
      securityContexts(securityContexts());

    package com.lyh.admin_template.back.common.config;
    
    import com.google.common.collect.Lists;
    import io.swagger.annotations.ApiOperation;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.service.ApiKey;
    import springfox.documentation.service.AuthorizationScope;
    import springfox.documentation.service.SecurityReference;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spi.service.contexts.SecurityContext;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Configuration
    @EnableSwagger2
    @Profile({"dev","test"})
    public class SwaggerConfig {
    
        @Bean
        public Docket createRestApi() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo())
                    .select()
                    // 加了ApiOperation注解的类,才会生成接口文档
                    .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                    // 指定包下的类,才生成接口文档
                    .apis(RequestHandlerSelectors.basePackage("com.lyh.admin_template.back"))
                    .paths(PathSelectors.any())
                    .build()
                    .securitySchemes(security())
                    .securityContexts(securityContexts());
        }
    
        private ApiInfo apiInfo() {
            return new ApiInfoBuilder()
                    .title("Swagger 测试")
                    .description("Swagger 测试文档")
                    .termsOfServiceUrl("https://www.cnblogs.com/l-y-h/")
                    .version("1.0.0")
                    .build();
        }
    
        private List<ApiKey> security() {
            return Lists.newArrayList(
                    new ApiKey("token", "token", "header")
            );
        }
    
        private List<SecurityContext> securityContexts() {
            return Lists.newArrayList(
                    SecurityContext.builder().securityReferences(defaultAuth())
                        //过滤要验证的路径
                        .forPaths(PathSelectors.regex("^(?!auth).*$"))
                        .build()
            );
        }
    
        //增加全局认证
        List<SecurityReference> defaultAuth() {
            AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
            AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
            authorizationScopes[0] = authorizationScope;
            List<SecurityReference> securityReferences = new ArrayList<>();
            // 由于 securitySchemes() 方法中 header 写入值为 token,所以此处为 token
            securityReferences.add(new SecurityReference("token", authorizationScopes));
            return securityReferences;
        }
    }

    (3)简单测试一下:
      首先登录,获取到 token。没有设置 token 时,访问 登出接口 会被拦截。
      设置 token 后,登出接口不会被拦截。

  • 相关阅读:
    Billing Invoice Split Rule
    From ProjectMagazine
    Link to PMP
    测试发现数据库性能问题后的SQL调优
    被jQuery折腾得半死,揭秘为何jQuery为何在IE/Firefox下均无法使用
    解决'将 expression 转换为数据类型 nvarchar 时出现算术溢出错误。'
    几年来ASP.NET官方站点首次改版,意味着MVC时代全面到来?
    Collection was modified; enumeration operation may not execute.的异常处理
    解决Sandcastle Help File Builder报错问题
    如何查看Windows服务所对应的端口?
  • 原文地址:https://www.cnblogs.com/huoyz/p/14378333.html
Copyright © 2011-2022 走看看