前后端分离下的SpringSecurity

完整的流程

  • 登录表单提交后的完整访问流程图

    SpringSecurity完整流程图

项目创建

  • 使用SpringBoot 初始化器创建SpringBoot 项目

  • 修改项目依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.9</version>
    <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>baizhi-security</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
    <java.version>1.8</java.version>
    </properties>

    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
    </dependency>

    <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.15</version>
    </dependency>
    <!-- 验证码 -->
    <dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
    <excludes>
    <exclude>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </exclude>
    </excludes>
    </configuration>
    </plugin>
    </plugins>
    </build>

    </project>

  • Java 环境

    JDK 1.8
  • YAML 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    spring:
    datasource:
    druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
    username: root
    password: root
    redis:
    host: 192.168.47.128 # 虚拟机 ip
    port: 6379 # (配置过主从复制) 必须使用 master 机器的端口号
    database: 0 # 选择的数据库实例
    connect-timeout: 10000 # 超时时间

    mybatis:
    type-aliases-package: com.example.baizhisecurity.entity
    mapper-locations: com/example/baizhisecurity/mapper/*Mapper.xml
    logging:
    level:
    com.example.baizhisecurity: debug # 查看 SQL

    # 修改服务器的过期时间为 1 分钟
    server:
    servlet:
    session:
    timeout: 1
    error:
    include-message: always # 指定错误响应中始终包含错误消息的详细信息。
    include-binding-errors: always # 指定错误响应中始终包含与绑定错误相关的详细信息。 如果将它们的值设置为 never,则不会在错误响应中包含这些信息。

数据库表

  • user 用户表

  • role 角色表

  • user_role 用户角色关联表

  • menu 菜单表(资源路径/admin/**,/user/**)

  • menu_role 菜单角色关联表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    /*
    Navicat Premium Data Transfer

    Source Server : wsd
    Source Server Type : MySQL
    Source Server Version : 80031
    Source Host : localhost:3306
    Source Schema : security

    Target Server Type : MySQL
    Target Server Version : 80031
    File Encoding : 65001

    Date: 07/04/2023 09:45:50
    */

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;

    -- ----------------------------
    -- Table structure for menu
    -- ----------------------------
    DROP TABLE IF EXISTS `menu`;
    CREATE TABLE `menu` (
    `id` int NULL DEFAULT NULL,
    `pattern` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;

    -- ----------------------------
    -- Records of menu
    -- ----------------------------
    INSERT INTO `menu` VALUES (1, '/admin/**');
    INSERT INTO `menu` VALUES (2, '/teacher/**');
    INSERT INTO `menu` VALUES (3, '/user/**');

    -- ----------------------------
    -- Table structure for menu_role
    -- ----------------------------
    DROP TABLE IF EXISTS `menu_role`;
    CREATE TABLE `menu_role` (
    `id` int NULL DEFAULT NULL,
    `mid` int NULL DEFAULT NULL,
    `rid` int NULL DEFAULT NULL
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

    -- ----------------------------
    -- Records of menu_role
    -- ----------------------------
    INSERT INTO `menu_role` VALUES (1, 1, 1);
    INSERT INTO `menu_role` VALUES (2, 2, 1);
    INSERT INTO `menu_role` VALUES (3, 2, 2);
    INSERT INTO `menu_role` VALUES (4, 3, 1);
    INSERT INTO `menu_role` VALUES (5, 3, 2);
    INSERT INTO `menu_role` VALUES (6, 3, 3);

    -- ----------------------------
    -- Table structure for persistent_logins
    -- ----------------------------
    DROP TABLE IF EXISTS `persistent_logins`;
    CREATE TABLE `persistent_logins` (
    `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
    `series` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
    `token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
    `last_used` timestamp NOT NULL,
    PRIMARY KEY (`series`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

    -- ----------------------------
    -- Records of persistent_logins
    -- ----------------------------
    INSERT INTO `persistent_logins` VALUES ('coder-itl', '+NCbggOD39giH/Vu9YOg8w==', 'ySX+tch8vl3rAwMdfyyiEw==', '2023-04-06 20:48:05');
    INSERT INTO `persistent_logins` VALUES ('teacher', '06WOm+L/vAmm3ZqSoHgrLA==', '6ZbIVk408R1YCjcIioMCcA==', '2023-04-07 09:09:11');
    INSERT INTO `persistent_logins` VALUES ('admin', '2hcnzyBJ3hafKcrMu8C+sg==', 'o+7/CQhTg5OUv31cTcUZSA==', '2023-04-05 14:04:57');
    INSERT INTO `persistent_logins` VALUES ('admin', '5ET4bpe2gzVJKmVlGbHDGg==', 'bPgJ57WNHTP97ctJpTonMA==', '2023-04-06 21:04:37');
    INSERT INTO `persistent_logins` VALUES ('admin', '6C8z2Ggdt5vbIU2YeLgCMg==', 'gsXpS2Lgzym4QMvCktTPmA==', '2023-04-05 14:00:50');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', '71EZlGL4pIORJLaD3QiuBA==', 'o12nQMywARciodt/yTulpQ==', '2023-04-07 09:45:33');
    INSERT INTO `persistent_logins` VALUES ('teacher', '7KsAd9xmBVdTwfZs7+6eRg==', 'QYSgnZgXxEnrxfnIIXSzWg==', '2023-04-06 10:06:34');
    INSERT INTO `persistent_logins` VALUES ('teacher', '8E2NghlfXgtNUjpCh5Ynow==', 'Ivz1aQ+DAjp0tA8JA6wXJg==', '2023-04-06 21:00:17');
    INSERT INTO `persistent_logins` VALUES ('teacher', '8Y+nhfNfx3rZPY2QgGUJ3Q==', 'C9BaH6ZeMslJ+W+yivisBA==', '2023-04-06 13:20:03');
    INSERT INTO `persistent_logins` VALUES ('admin', 'B90gSNbxYDJ7Kc9Vxqth8g==', 'MM6dHcBSKC4QGhSvC+nGpA==', '2023-04-06 13:03:00');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'bsQjW5UaNxF4hQYchfmiMg==', '7cuA44TE7sDf52akeFzfzA==', '2023-04-06 12:40:55');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'cwh4KW16m4f3WJkpjraE2Q==', 'vg386HDxAhefTkAoA0qHQg==', '2023-04-06 21:03:58');
    INSERT INTO `persistent_logins` VALUES ('admin', 'dI4NgEc8qvxs040tp+8hmA==', 'zTk18k4kduRkAlend9E/aw==', '2023-04-06 21:48:51');
    INSERT INTO `persistent_logins` VALUES ('admin', 'DoPmjtrs2FB5a2wmd0itgQ==', 'ceOOh1eOlgJaE10FuGgLEQ==', '2023-04-05 13:50:35');
    INSERT INTO `persistent_logins` VALUES ('admin', 'es4DbphWQATWfrSVnTlEZg==', 'N1US50Dv8/9NLfDQyCNU1Q==', '2023-04-06 21:20:18');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'FA/FTeV5sF0boEeM6DtDKQ==', 'QXqI8ERD1xxJCXjwnchvBA==', '2023-04-06 13:33:28');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'FcSttDuZrZv8o3orH5lZoQ==', 'fhuJICPg4yP74ByKUuNeqQ==', '2023-04-06 21:57:31');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'fo+iV1xxVOyajQFgDvM5NQ==', 'WMZzeawcT5eLsxcQUoSYxw==', '2023-04-06 15:02:45');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'FoVmkKDiet+JJK5hh545ZQ==', '+7o9H7CtAmR6KXSvkSf0WA==', '2023-04-06 20:45:29');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'FtbDgyh2RoZnyn0KY56BNA==', 'Imb8AafP9sV7lIFN7sdRrw==', '2023-04-06 13:20:41');
    INSERT INTO `persistent_logins` VALUES ('admin', 'fv3SaWzfyUv/QwMlzmavOA==', '1wlWcVWSQuRRuDwdR/Idow==', '2023-04-06 09:28:56');
    INSERT INTO `persistent_logins` VALUES ('admin', 'GTtauD/CbVwW34ZqLUYoXw==', '4gCbVHbIeZKmS0tWT/lx6w==', '2023-04-06 21:33:36');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'IFtSvMLDgIoZXF+GHYkvNw==', 'R8n4Xgx4fzxElPn+a8jR6g==', '2023-04-06 11:18:20');
    INSERT INTO `persistent_logins` VALUES ('admin', 'IiwnhArutTAwQfvblMl2sw==', 'uqpVK58S4rb8oN9vrj0BIw==', '2023-04-06 10:06:18');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'ixbbvsDzYXvPe2UDdMEQyw==', '1UOBLl7fkNvMMs/3M65QTA==', '2023-04-06 20:45:57');
    INSERT INTO `persistent_logins` VALUES ('admin', 'JIsjFU/hWGRjSnzSZYttjw==', 'Zo70GLchMJ590OwLoY+8FQ==', '2023-04-06 21:32:20');
    INSERT INTO `persistent_logins` VALUES ('admin', 'JLV+MNM+JvpI2OKj/1Y7nA==', '9d1OayYhJ3ifAQeEbZ8fhQ==', '2023-04-05 14:04:30');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'kDqb0W7hxnWp8KGX0Q48aA==', 'P5kt3uuc5oAm3qipHGiqQQ==', '2023-04-06 10:00:25');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'L1nMoNZxjqldtqTD70AE6g==', 'FYT72j6eECDTzNpri+khfA==', '2023-04-06 21:58:53');
    INSERT INTO `persistent_logins` VALUES ('admin', 'l3324E3ZESwy4+8m35p9IQ==', '//Bc+qFVbx63PxlS1iXU4w==', '2023-04-06 11:08:45');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'lNoGcjXBHs4JTZa0JVWFUg==', 'O6AzhN2WeCULJ7nwuNMg9w==', '2023-04-06 21:48:16');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'MczRCQbppFTmHDazBqWHPQ==', 'CMWCRoCCdfPVgOE7exe0dA==', '2023-04-06 10:05:48');
    INSERT INTO `persistent_logins` VALUES ('admin', 'Mfai26sgPjInTFn1h9xfnA==', 'XyAUI8C1OjxqBYMnpRdU7Q==', '2023-04-07 09:06:16');
    INSERT INTO `persistent_logins` VALUES ('admin', 'NBwtNDT0SbGr5Eb8VKNYHA==', '4RhsbTNUz4i6OyfCvwyUyA==', '2023-04-06 13:02:11');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'ncQTEA5Qz3XCUeSt3Av/ng==', 'hBNdBqLYu6q9Oi2jBvJRKw==', '2023-04-06 22:05:26');
    INSERT INTO `persistent_logins` VALUES ('admin', 'NFL/JsY+XByb3yC1eEt1BA==', 'mdrM9em/6/l1GdUZM3vTUA==', '2023-04-06 12:39:18');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'Nh5qKNjFu6hRn1b0Y4RJrg==', 'cqyc/4j3kX8COGurv3HBAg==', '2023-04-07 09:09:48');
    INSERT INTO `persistent_logins` VALUES ('admin', 'ofKt05ICWMrfNexu1s9U4Q==', '+EOT03kdTMPOHOnWjQPgPA==', '2023-04-06 11:06:54');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'oj1MCjsUBNAuwHEwxt464g==', 'Gz0IoeUoTjIqPIhUvXR4xg==', '2023-04-07 09:44:39');
    INSERT INTO `persistent_logins` VALUES ('admin', 'ol9+d2m0OzFE56qV7qaSqg==', 'jPZfTph5Wpw4mQ3stMHpXA==', '2023-04-06 09:55:47');
    INSERT INTO `persistent_logins` VALUES ('admin', 'OSK6RzGfrdo8pOwaDBMzxw==', 'THuhBzEg2Bpx7fer8pqQjQ==', '2023-04-06 13:17:09');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'ot3Dnr5DTuv/AUml8d/qdQ==', '/cQHLKxTh25R4i4z0oCiow==', '2023-04-06 09:58:04');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'R3/wJ+1pKXEIuXZozDvjvQ==', 'kvRFa3hfVPgasBet+c8LJg==', '2023-04-06 15:01:45');
    INSERT INTO `persistent_logins` VALUES ('admin', 'R5RGQLOgo5x+NyjeKuoTyQ==', 'QAqv1E+g35tyJiJqMOouqA==', '2023-04-06 15:02:12');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'RKddF5IMasqYwv3K0gi+bA==', 'aeywp7yA5q+D7z6WI97YQQ==', '2023-04-06 13:06:49');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'RXEcbNBFhOWeIiHL6/I1hg==', 'C+01NeYlnzpSq37zv7c2NA==', '2023-04-06 20:48:18');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'sCLJ7O8N0ALQv8j6uhttzA==', 'T9lfQELxcCLLFCswClbdLA==', '2023-04-06 20:46:36');
    INSERT INTO `persistent_logins` VALUES ('coder-itl', 'sTdo6JDuV3xg6C3iN5c+dw==', 'CEB4uBOmPQjJbf1q/I1PWw==', '2023-04-06 21:42:44');
    INSERT INTO `persistent_logins` VALUES ('admin', 'UI78CzD4iyww1nD56McpyQ==', 'O0OiyTLUV1fQdsY0sFZXOg==', '2023-04-06 21:04:19');
    INSERT INTO `persistent_logins` VALUES ('teacher', 'uNXpDClWndBjG6VVxrcQGA==', 'lysJuQP7VM4+xCs3vRwVQw==', '2023-04-06 22:05:07');
    INSERT INTO `persistent_logins` VALUES ('admin', 'uopl+JsXkoNxHqENcDf9vQ==', 'eNUgMWphh4yHp4C/vf7Cew==', '2023-04-06 13:18:59');
    INSERT INTO `persistent_logins` VALUES ('admin', 'usczqi2BcbpXDC6eWYgMQA==', 'B3n13cz04Ebv9XYUmc4/0g==', '2023-04-06 22:10:26');
    INSERT INTO `persistent_logins` VALUES ('admin', 'uWF/JllmNkGiyr7yZZ9N/A==', 'l5pAut3bu4x/vRv8Ml25ig==', '2023-04-06 21:56:55');
    INSERT INTO `persistent_logins` VALUES ('admin', 'vSoHQ7opottg/GOof6Hi7w==', 'XJ8lUrkQ1Vt08G1PDaDWIw==', '2023-04-06 20:44:35');
    INSERT INTO `persistent_logins` VALUES ('admin', 'w8KXdC7dmjqar3A4ng/G4Q==', '2xzYKDXa5Z9VUn0FuD3pqQ==', '2023-04-06 22:06:54');
    INSERT INTO `persistent_logins` VALUES ('admin', 'x1/sFDWjOCvT6GfV3HAZbQ==', 'LO5+2zexNvhFwm8r7QVr1w==', '2023-04-06 21:42:10');
    INSERT INTO `persistent_logins` VALUES ('admin', 'xEqiGRZccJBGvFJgnjOMkg==', 'Jr4QW88e5Reh3DeP01ZBBg==', '2023-04-06 13:32:43');
    INSERT INTO `persistent_logins` VALUES ('admin', 'y92BWDMAPhu5si9r2TMyoQ==', 'P4/XSEIb1LyIznak0asXEw==', '2023-04-06 15:18:06');
    INSERT INTO `persistent_logins` VALUES ('admin', 'YFfvNW+KwHWZEyQm+pPfjg==', 'LOXnIYFpTQV//yT2+BLb4g==', '2023-04-06 11:17:03');
    INSERT INTO `persistent_logins` VALUES ('admin', 'ygTIbNi3/ihYfVTygLU4OQ==', 'Yy4/GYpGIl2DGJgyswJdgw==', '2023-04-06 13:08:14');

    -- ----------------------------
    -- Table structure for role
    -- ----------------------------
    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role` (
    `id` int NOT NULL AUTO_INCREMENT COMMENT '角色ID',
    `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色名称',
    `name_zh` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色中文名称',
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

    -- ----------------------------
    -- Records of role
    -- ----------------------------
    INSERT INTO `role` VALUES (1, 'ROLE_ADMIN', '系统管理员');
    INSERT INTO `role` VALUES (2, 'ROLE_TEACHER', '教职工');
    INSERT INTO `role` VALUES (3, 'ROLE_USER', '普通用户');

    -- ----------------------------
    -- Table structure for user
    -- ----------------------------
    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
    `id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
    `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
    `enabled` tinyint(1) NULL DEFAULT NULL COMMENT '是否启用',
    `accountNonExpired` tinyint(1) NULL DEFAULT NULL COMMENT '账户是否过期',
    `accountNonLocked` tinyint(1) NULL DEFAULT NULL COMMENT '账户是否锁定',
    `credentialsNonExpired` tinyint(1) NULL DEFAULT NULL COMMENT '凭证是否过期',
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

    -- --------------------------------------------------------------------------------------------
    -- Records of user 密码可以将出现的 {bcrypt}$2a$10$LB0cpRH.LK6xbY3IgwM/buSlGhsVSU67t10TeEqORIniJEoIKc5y2

    -- 替换为自己的明文密码 '{noop}123',项目中使用了自动密码更新加密方案
    -- 明文密码是各自对应的,比如: admin/admin
    -- --------------------------------------------------------------------------------------------
    INSERT INTO `user` VALUES (1, 'admin', '{bcrypt}$2a$10$LB0cpRH.LK6xbY3IgwM/buSlGhsVSU67t10TeEqORIniJEoIKc5y2', 1, 1, 1, 1);
    INSERT INTO `user` VALUES (2, 'teacher', '{bcrypt}$2a$10$jcoCf2lcYK4A8DxKclpyJuDWKsHbbmj5E7elpgJgfOrZMlTISrYtq', 1, 1, 1, 1);
    INSERT INTO `user` VALUES (3, 'coder-itl', '{bcrypt}$2a$10$q3Fwfrg0j9dCwmJ51O1MnuBs66/V5yHgaTRxzHJF1cqOBV6qfA5p.', 1, 1, 1, 1);

    -- ----------------------------
    -- Table structure for user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `user_role`;
    CREATE TABLE `user_role` (
    `id` int NOT NULL AUTO_INCREMENT,
    `uid` int NULL DEFAULT NULL,
    `rid` int NULL DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE,
    INDEX `uid`(`uid` ASC) USING BTREE,
    INDEX `rid`(`rid` ASC) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

    -- ----------------------------
    -- Records of user_role
    -- ----------------------------
    INSERT INTO `user_role` VALUES (1, 1, 1);
    INSERT INTO `user_role` VALUES (2, 2, 2);
    INSERT INTO `user_role` VALUES (3, 3, 3);

    SET FOREIGN_KEY_CHECKS = 1;

实体类

  • 用户实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    package com.example.baizhisecurity.entity;


    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;

    import java.io.Serializable;
    import java.util.*;
    import java.util.stream.Collectors;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User implements UserDetails, Serializable {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    // 用户的角色集合
    private List<Role> roles = new ArrayList<>();

    // 权限集合
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
    return password;
    }

    @Override
    public String getUsername() {
    return username;
    }

    @Override
    public boolean isAccountNonExpired() {
    return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
    return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
    return enabled;
    }
    }
  • 角色实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.example.baizhisecurity.entity;

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Role {
    private Integer id;
    private String name;
    private String nameZh;
    }
  • 角色实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.example.baizhisecurity.entity;

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    import java.io.Serializable;
    import java.util.List;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Menu implements Serializable {
    private Integer id;
    private String pattern;
    // 这个菜单所需要的角色信息
    private List<Role> roles;
    }

控制器

  • 测试控制器类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    package com.example.baizhisecurity.controller.common;

    import com.example.baizhisecurity.common.ResultModel;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class HelloController {
    @GetMapping("/admin/hello")
    public ResultModel adminHello() {
    return ResultModel.success(HttpStatus.OK.value(), "数据获取成功", "Hello SpringSecurity Admin!");
    }

    @GetMapping("/teacher/hello")
    public ResultModel teacherHello() {
    return ResultModel.success(HttpStatus.OK.value(), "数据获取成功", "Hello SpringSecurity ADMIN and Teacher!");
    }

    @GetMapping("/user/hello")
    public ResultModel studentHello() {
    return ResultModel.success(HttpStatus.OK.value(), "数据获取成功", "Hello SpringSecurity Student!");
    }

    @GetMapping("/hello")
    public ResultModel hello() {
    return ResultModel.success(HttpStatus.OK.value(), "数据获取成功", "Hello SpringSecurity All!");
    }
    }

JSON 响应和统一数据返回

  • 响应

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ResponseUtil {
    public static void out(HttpServletResponse response,ResultModel resultModel){
    ObjectMapper objectMapper = new ObjectMapper();
    // 设置响应的状态为 200
    response.setStatus(HttpStatus.OK.value());
    // 设置响应的格式为 JSON 格式
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    try {
    // 使用jackson,把json格式的resultModel写入到response的输出流中
    objectMapper.writeValue(response.getOutputStream(),resultModel);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
  • 统一数据返回模型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    package com.example.baizhisecurity.common;

    import lombok.Data;
    import org.springframework.http.HttpStatus;

    import java.io.Serializable;

    @Data
    public class ResultModel<T> implements Serializable {
    //状态码
    private int code; //1000表示成功 401表示认证失败
    //消息
    private String message;
    //数据
    private T data;

    private static ResultModel resultModel = new ResultModel();

    public static ResultModel success(String message) {
    resultModel.setCode(1000);
    resultModel.setMessage(message);
    resultModel.setData(null);
    return resultModel;
    }

    public static ResultModel success(Object data) {
    resultModel.setCode(200);
    resultModel.setMessage("success");
    resultModel.setData(data);
    return resultModel;
    }

    public static ResultModel success(String message, Object data) {
    resultModel.setCode(200);
    resultModel.setMessage(message);
    resultModel.setData(data);
    return resultModel;
    }

    public static ResultModel success(Integer code, String message) {
    resultModel.setCode(200);
    resultModel.setMessage(message);
    return resultModel;
    }

    public static ResultModel success(Integer code, String message, Object data) {
    resultModel.setCode(code);
    resultModel.setMessage(message);
    resultModel.setData(data);
    return resultModel;
    }

    public static ResultModel error() {
    resultModel.setCode(500);
    resultModel.setMessage("error");
    resultModel.setData(null);
    return resultModel;
    }

    public static ResultModel error(String message) {
    resultModel.setCode(500);
    resultModel.setMessage(message);
    resultModel.setData(null);
    return resultModel;
    }

    public static ResultModel error(HttpStatus code, String message, Object data) {
    resultModel.setCode(code.value());
    resultModel.setMessage(message);
    resultModel.setData(data);
    return resultModel;
    }

    public static ResultModel error(int code, String message) {
    resultModel.setCode(code);
    resultModel.setMessage(message);
    resultModel.setData(null);
    return resultModel;
    }
    }

SpringSecurity 的配置

配置类

  • 配置类的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    package com.example.baizhisecurity.security.config;

    import com.example.baizhisecurity.custom.*;
    import com.example.baizhisecurity.filter.LoginFilter;
    import com.example.baizhisecurity.security.metasource.CustomSecurityMetadataSource;
    import com.example.baizhisecurity.security.rememberme.MyRememberServices;
    import com.example.baizhisecurity.service.MyUserDetalService;
    import com.example.baizhisecurity.utils.TokenManager;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
    import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.ObjectPostProcessor;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.annotation.web.configurers.UrlAuthorizationConfigurer;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
    import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
    import org.springframework.security.web.authentication.RememberMeServices;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.security.web.util.matcher.OrRequestMatcher;

    import javax.sql.DataSource;
    import java.util.UUID;

    @Slf4j
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // RememberMe 需要的数据源
    private final DataSource dataSource;
    // Jwt
    private final TokenManager tokenManager;
    // redis
    private final RedisTemplate redisTemplate;
    // 数据库数据源认证
    private final MyUserDetalService myUserDetalService;
    // 自定义授权异常处理
    private final MyAccessDeniedHandler myAccessDeniedHandler;
    // 登录成功处理
    private final MyLogoutSuccessHandler myLogoutSuccessHandler;
    // 自定义认证异常处理
    private final MyAuthenticationEntryPoint myAuthenticationEntryPoint;
    // 元数据信息集合
    private final CustomSecurityMetadataSource customSecurityMetadataSource;
    // 自定义认证成功处理
    private final MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    // 自定义认证失败处理
    private final MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    public SecurityConfig(
    DataSource dataSource,
    TokenManager tokenManager,
    RedisTemplate redisTemplate,
    MyUserDetalService myUserDetalService,
    MyAccessDeniedHandler myAccessDeniedHandler,
    MyLogoutSuccessHandler myLogoutSuccessHandler,
    MyAuthenticationEntryPoint myAuthenticationEntryPoint,
    CustomSecurityMetadataSource customSecurityMetadataSource,
    MyAuthenticationFailureHandler myAuthenticationFailureHandler,
    MyAuthenticationSuccessHandler myAuthenticationSuccessHandler
    ) {
    this.dataSource = dataSource;
    this.tokenManager = tokenManager;
    this.redisTemplate = redisTemplate;
    this.myUserDetalService = myUserDetalService;
    this.myAccessDeniedHandler = myAccessDeniedHandler;
    this.myLogoutSuccessHandler = myLogoutSuccessHandler;
    this.myAuthenticationEntryPoint = myAuthenticationEntryPoint;
    this.customSecurityMetadataSource = customSecurityMetadataSource;
    this.myAuthenticationSuccessHandler = myAuthenticationSuccessHandler;
    this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
    }

    // 放行资源白名单
    private String[] WHITE = {
    "/error",
    "/css/**",
    "/img/**",
    "/druid/**",
    "/common/**",
    "/webjars/**",
    "/*/api-docs",
    "/swagger-ui.html",
    "/swagger-resources/**"
    };


    /**
    * TODO: 自定义前后端分离 Form 表单 => JSON 格式
    * 自定义 Filter 交给工厂管理
    */
    @Bean
    public LoginFilter loginFilter() throws Exception {
    LoginFilter loginFilter = new LoginFilter(redisTemplate);
    // 设置认证路径
    loginFilter.setFilterProcessesUrl("/login");
    // 指定接受 json 用户名的 key
    loginFilter.setUsernameParameter("username");
    // 指定接受 json 密码的 key
    loginFilter.setPasswordParameter("password");
    // 指定接受 json 验证码的 key
    loginFilter.setKaptchaParameter("kaptcha");
    // 指定接受 json 记住我的 key
    loginFilter.setRememberMeParameter("remember-me");
    // TODO 什么作用
    loginFilter.setAuthenticationManager(authenticationManagerBean());
    // 认账成功处理
    loginFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
    // 认证失败处理
    loginFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
    loginFilter.setRememberMeServices(rememberMeServices());
    return loginFilter;
    }

    /**
    * authenticationManagerBean 是一个方法名,用于获取一个 Spring Security 的认证管理器实例,
    * 该方法将认证管理器实例化并将其注入到 Spring 上下文中以供其他 Bean 使用。
    * Spring Security 默认会为您提供一个认证管理器实例,但如果您需要在自己的代码中使用它,
    * 可以使用这个方法将其注入到您的代码中。
    * 在这个方法中,super.authenticationManagerBean() 调用了父类的同名方法,
    * 返回了一个 AuthenticationManager 实例。这个实例将被 Spring 管理并注入到上下文中。
    * Regenerate response
    *
    * @return
    * @throws Exception
    */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }

    /**
    * 自定义 AuthenticationManager 推荐
    * 它的作用是管理用户认证的过程。
    * 具体来说,它接收用户的登录请求并从Spring Security进行用户认证。在进行用户认证的过程中,AuthenticationManager
    * 首先根据用户名获取用户信息,
    * 然后将给定的用户名和密码与用户信息进行比较,如果验证通过,则认为用户已经被认证。如果验证失败,则会抛出异常,表示用户认证失败。
    *
    * @param auth
    * @throws Exception
    */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(myUserDetalService);
    }

    /**
    * 放行的资源不经过过滤器安全链
    * LOG: Will not secure Ant [pattern='/img/**']
    *
    * @param web
    * @throws Exception
    */
    @Override
    public void configure(WebSecurity web) throws Exception {
    web
    .debug(false)
    .ignoring()
    .antMatchers(WHITE);
    }


    /**
    * anyRequest | 匹配所有请求路径
    * access | SpringEl表达式结果为true时可以访问
    * anonymous | 匿名可以访问
    * denyAll | 用户不能访问
    * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
    * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
    * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
    * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
    * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
    * hasRole | 如果有参数,参数表示角色,则其角色可以访问
    * permitAll | 用户可以任意访问
    * rememberMe | 允许通过remember-me登录的用户访问
    * authenticated | 用户登录后可访问
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    // 添加 CORS
    http.cors();

    // 获取工厂对象
    ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
    // 设置自定义 url 权限处理
    http.apply(new UrlAuthorizationConfigurer<>(applicationContext)).withObjectPostProcessor(
    new ObjectPostProcessor<FilterSecurityInterceptor>() {
    @Override
    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
    object.setSecurityMetadataSource(customSecurityMetadataSource);
    // 是否拒绝公共资源访问 比如访问公共的 验证码(允许访问: false)
    object.setRejectPublicInvocations(false);
    return object;
    }
    });

    http.formLogin()
    .and()
    .authorizeRequests()
    .mvcMatchers("/login", "/logout").permitAll()
    .anyRequest().authenticated();

    http
    // CSRF 禁用,因为不使用 session
    .csrf().disable()
    // 禁用HTTP响应标头
    .headers().cacheControl().disable().and()
    // 认证异常的处理
    .exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
    // 授权异常处理
    .accessDeniedHandler(myAccessDeniedHandler).and()
    // 基于token,所以不需要session
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
    .headers().frameOptions().disable();

    // 注销
    http.logout() // 设置为表达式处理器

    // 前后端分离的处理方式,页面不跳转,响应 json 格式
    .logoutSuccessHandler(myLogoutSuccessHandler)
    // 清除会话、清楚认证标记、注销成功后的默认跳转到登录页等为默认配置,可以不声明出现
    // 退出的请求方式指定 GET、POST
    .logoutRequestMatcher(
    new OrRequestMatcher(
    new AntPathRequestMatcher("/logout", "POST"),
    // 可以指定多种同时指定请求方式
    new AntPathRequestMatcher("/myLogout", "POST"))
    );

    // 记住我
    http.rememberMe()
    .rememberMeServices(rememberMeServices())
    // TODO 前后端分离的实现: 设置自动登录使用那个 rememberMe 第二次设置的作用: 解码
    .tokenValiditySeconds(3600);
    // at: 用来某个 filter 替换过滤器链中那个 filter
    // before: 放在过滤器链中那个 filter 之前
    // after: 放在过滤器链中那个 filter 之后
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
    * 点击 JdbcTokenRepositoryImpl => 获取创建表结构的 SQL
    * public static final String CREATE_TABLE_SQL = "
    * create table persistent_logins (
    * username varchar(64) not null,
    * series varchar(64) primary key,
    * token varchar(64) not null,
    * last_used timestamp not null)
    * ";
    *
    * @return
    */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    return jdbcTokenRepository;
    }

    /**
    * 前后端分离记住我的实现
    *
    * @return MyRememberServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository)
    */
    @Bean
    public RememberMeServices rememberMeServices() {
    return new MyRememberServices(UUID.randomUUID().toString(), userDetailsService(), persistentTokenRepository());
    }
    }

前后端分离相关自定义实现

  • 自定义授权异常处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 自定义授权异常处理
    */
    @Component
    public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    ResponseUtil.out(response, ResultModel.error(HttpStatus.UNAUTHORIZED.value(), accessDeniedException.getMessage().toString()));
    }
    }
  • 自定义认证异常处理

    1
    2
    3
    4
    5
    6
    7
    @Component
    public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    ResponseUtil.out(response, ResultModel.error(HttpStatus.FORBIDDEN.value(), authException.getMessage().toString()));
    }
    }
  • 自定义认证失败处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    /**
    * 自定义认证失败的处理
    */
    @Component
    public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    ResponseUtil.out(response, ResultModel.error(HttpStatus.FORBIDDEN.value(), exception.getMessage().toString()));
    }
    }

  • 自定义认证成功后的处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 自定义认证成功后的处理
    */
    @Component
    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Overrid
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    ResponseUtil.out(response, ResultModel.success(HttpStatus.OK.value(), "认证成功", authentication));
    }
    }
  • 自定义注销成功的处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 自定义注销成功的处理
    */
    @Component
    public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    ResponseUtil.out(response, ResultModel.success(HttpStatus.OK.value(), "注销成功"));
    }
    }

  • 自定义前后端分离认证 Filter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    package com.example.baizhisecurity.filter;

    import com.example.baizhisecurity.exception.KaptchaNotMatchException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.util.ObjectUtils;

    import javax.servlet.ServletRequest;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Map;

    /**
    * 自定义前后端分离认证 Filter
    */
    @Slf4j
    public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private StringRedisTemplate redisTemplate;

    // 设置默认的表单验证码 name = kaptcha
    private static final String FORM_KAPTCHA_KEY = "kaptcha";
    private static final String FORM_REMEMBER_ME_KEY = "remember-me";

    private String kaptchaParameter = FORM_KAPTCHA_KEY;
    private String rememberMeParameter = FORM_REMEMBER_ME_KEY;

    public LoginFilter(StringRedisTemplate redisTemplate) {
    this.redisTemplate = redisTemplate;
    }

    // 提供自定义的验证码名称
    public String getKaptchaParameter() {
    return this.kaptchaParameter;
    }

    public void setKaptchaParameter(final String kaptchaParameter) {
    this.kaptchaParameter = kaptchaParameter;
    }

    public String getRememberMeParameter() {
    return rememberMeParameter;
    }

    public void setRememberMeParameter(String rememberMeParameter) {
    this.rememberMeParameter = rememberMeParameter;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
    // 1. 判断请求方式是否是 POST
    if (!request.getMethod().equals("POST")) {
    throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    // 2. 判断 数据是否是 JSON 格式
    ServletRequest re = (ServletRequest) request;
    if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
    try {
    // 将请求体中的数据进行反序列化
    Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
    // 获取 json 用户名
    String username = userInfo.get(getUsernameParameter());
    // 获取 json 密码
    String password = userInfo.get(getPasswordParameter());
    // 获取 json 验证码
    String kaptcha = userInfo.get(getKaptchaParameter());
    // 获取 session 中的验证码
    String redisCode = redisTemplate.opsForValue().get("kaptcha");
    log.info("redisCode: {}", redisCode);
    // 获取 json 中的记住我
    String rememberMe = userInfo.get(getRememberMeParameter());
    if (!ObjectUtils.isEmpty(rememberMe)) {
    // 将这个 remember-me 设置到作用域中
    request.setAttribute(getRememberMeParameter(), rememberMe);
    }
    // 用户输入的验证码和 session 作用域中的都不能为空
    if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(redisCode) && kaptcha.equalsIgnoreCase(redisCode)) {
    log.info("用户名: {} 密码: {},是否记住我: {}", userInfo, password, rememberMe);
    // 获取用户名和密码认证
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    setDetails(request, authRequest);
    return this.getAuthenticationManager().authenticate(authRequest);
    }
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    // 没有通过则执行自定义异常
    throw new KaptchaNotMatchException("验证码不匹配!");
    }
    // 如果不是 JSON 格式数据,则调用传统方式进行认证
    return super.attemptAuthentication(request, response);
    }
    }

记住我

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package com.example.baizhisecurity.config.rememberme;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.log.LogMessage;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
    import org.springframework.util.ObjectUtils;

    import javax.servlet.http.HttpServletRequest;

    /**
    * TODO 这个类不能被 Spring 容器管理
    * 自定义记住我 service 的实现,这个类必须实现它的构造方法
    */
    @Slf4j
    public class MyRememberServices extends PersistentTokenBasedRememberMeServices {
    public MyRememberServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
    super(key, userDetailsService, tokenRepository);
    log.info("what is the key: {}", key);
    }

    /**
    * 自定义前后端分离获取 remember-me 的方式
    *
    * @param request
    * @param parameter
    * @return
    */
    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
    log.info("parameter:{}", parameter);
    // 获取作用域中存储的
    String rememberMe = (String) request.getAttribute(parameter);
    if (!ObjectUtils.isEmpty(rememberMe)) {
    if (rememberMe.equalsIgnoreCase("true") || rememberMe.equalsIgnoreCase("on") || rememberMe.equalsIgnoreCase("yes") || rememberMe.equalsIgnoreCase("1")) {
    return true;
    }
    }
    this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
    return false;
    }
    }

  • 疑惑: 关于remember-me 此选项本来应该是可选项,但是在后续的配置中发现成为了必选项

    未添加remember-me 添加rememeber-me
    访问接口,首先是可以通过/login,但是后续接口不能被访问 可直接访问接口获得数据

跨域配置

  • SpringBoot 中的配置全局解决前后端的跨域

    在配置使用的这个过程中真的很难受,本来第一种专业的在SpringSecurity 配置在前后端分离项目中不生效,但是又在后续测试中莫名其妙的又可以了。下面这种方式是可以直接生效的,但是在学习的过程中提到由于过滤器链的顺序可能导致某些不生效

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.example.baizhisecurity.config;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    /**
    * 1. 先对 SpringBoot 配置,运行跨域请求
    */
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    // 设置允许跨域的路径
    registry.addMapping("/**")
    // 设置允许跨域请求的域名
    .allowedOriginPatterns("*")
    // 是否允许 Cookie
    .allowCredentials(true)
    // 设置允许的请求方式
    .allowedMethods("GET", "POST", "DELETE", "PUT")
    // 设置允许的 header 属性
    .allowedHeaders("*")
    // 设置允许时间
    .maxAge(3600L);
    }
    }
    1
    2
    // 2. 最后只需要在 SpringSecurity 的 hppt 配置跨域
    http.cors();

    在经过上面两步后,成功解决CORS 引起的问题并成功的获取到了数据。【在此时前后端分离时推荐使用此种方式,见效快,不要停留太久在无用的配置上】

  • 专业的SpringSecurity 配置【有这个功能,但是前后端分离项目中未能解决跨域】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 注意所在 包
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

    http.cors().configurationSource(configurationSource())

    CorsConfigurationSource corsConfigurationSource(){
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
    corsConfiguration.setAllowedMethods(Arrays.asList("*"));
    corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
    corsConfiguration.setMaxAge(3600L);
    UrlBasedCorsConfigurationSource source= new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**",corsConfiguration);
    return source;
    }
    在使用的时候关闭了 csrf
  • 两种跨域如何选择

    在学习使用过程中其实是推荐 SpringSecurity专业的方式的,但是在前后端分离项目中依然会出现跨域CORS 错误时,请毫不犹豫的去使用第一种, 快速有效

    个人也在前后端分离时成功的获取到过数据,如上图,但是再次使用时依然存在跨域错误,后续发现配置失效了

验证码

  • 配置验证码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Configuration
    public class KaptchaConfig {
    @Bean
    public Producer kaptcha() {
    Properties properties = new Properties();
    properties.setProperty("kaptcha.image.width", "120");
    properties.setProperty("kaptcha.image.height", "40");
    properties.setProperty("kaptcha.textproducer.char.string", "0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOASDFGHJKLZXCVBNM");
    properties.setProperty("kaptcha.textproducer.char.length", "4");
    Config config = new Config(properties);
    DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
    defaultKaptcha.setConfig(config);
    return defaultKaptcha;
    }
    }
  • 验证码的控制器类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Slf4j
    @CrossOrigin
    @RestController
    public class CaptchaController {
    @Autowired
    private Producer producer;
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/captcha")
    public ResultModel getVerifyCode(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
    // 1. 生成验证码
    String text = producer.createText();
    log.info("code text: {}", text);
    // 2. TODO 放入 session/redis
    redisTemplate.opsForValue().set("kaptcha", text);
    // 3. 生成图片
    BufferedImage image = producer.createImage(text);
    FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
    ImageIO.write(image, "jpg", fos);
    String base64Img = Base64.encodeBase64String(fos.toByteArray());
    return ResultModel.success(HttpStatus.OK.value(), "验证码获取成功!", base64Img);
    }
    }

自定义全局异常

  • 验证码异常

    1
    2
    3
    4
    5
    6
    7
    8
    public class KaptchaNotMatchException extends AuthenticationException {
    public KaptchaNotMatchException(String msg, Throwable cause) {
    super(msg, cause);
    }
    public KaptchaNotMatchException(String msg) {
    super(msg);
    }
    }
  • 全局异常处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    package com.example.baizhisecurity.exception;

    import com.example.baizhisecurity.common.HttpStatus;
    import com.example.baizhisecurity.common.ResponseUtil;
    import com.example.baizhisecurity.common.ResultModel;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.javassist.NotFoundException;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.web.HttpRequestMethodNotSupportedException;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    @Slf4j
    @RestControllerAdvice
    public class GlobalExceptionHandle extends RuntimeException {

    @ExceptionHandler(value = {Exception.class})
    protected ResultModel handleAllException(Exception ex) {
    return ResultModel.error(HttpStatus.ERROR, ex.getMessage().toString());
    }

    @ExceptionHandler(value = {NotFoundException.class})
    protected ResultModel handleNotFound(Exception ex) {
    log.error("错误信息: {}", ex.getMessage().toString());
    return ResultModel.error(HttpStatus.NOT_FOUND, ex.getMessage().toString());
    }

    /**
    * 权限校验异常
    */
    @ExceptionHandler(AccessDeniedException.class)
    public ResultModel handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
    return ResultModel.error(HttpStatus.FORBIDDEN, e.getMessage());
    }

    /**
    * 请求方式不支持
    */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResultModel handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
    HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
    return ResultModel.error(HttpStatus.BAD_METHOD, e.getMessage().toString());
    }

    /**
    * 拦截未知的运行时异常
    */
    @ExceptionHandler(RuntimeException.class)
    public ResultModel handleRuntimeException(RuntimeException e, HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    log.error("请求地址'{}',发生未知异常.", requestURI, e);
    return ResultModel.error(e.getMessage());
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResultModel handleIllegalArgumentException(IllegalArgumentException ex) {
    return ResultModel.error(ex.getMessage());
    }

    /**
    * 自定义验证异常
    */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    log.error(e.getMessage(), e);
    String message = e.getBindingResult().getFieldError().getDefaultMessage();
    return ResultModel.error(message);
    }

    }

Mapper 定义

  • Mapper 定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Repository
    public interface UserMapper {
    User findUserByUserName(String username);

    List<Role> getRoleByUid(Integer uid);

    Integer updatePassword(String username, @Param("password") String newPassword);
    }

  • UserMapper 映射实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.baizhisecurity.mapper.UserMapper">
    <!-- User findUserByUserName(String username); -->
    <select id="findUserByUserName" resultType="user">
    select *
    from user
    where username = #{username}
    </select>

    <!-- List<Role> getRoleByUid(Integer uid); -->
    <select id="getRoleByUid" resultType="role">
    select r.id, r.name, r.name_zh
    from role r,
    user_role ur
    where r.id = ur.uid
    and ur.uid = #{uid}
    </select>


    <!-- Integer updatePassword(@Param("username") String username,@Param("password") String password);-->
    <update id="updatePassword">
    update `user`
    set password = #{password}
    where username = #{username}
    </update>
    </mapper>

  • MenuMapper.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.baizhisecurity.mapper.MenuMapper">
    <!-- List<Menu> getAllMenu(); -->
    <resultMap id="MenuResultMap" type="menu">
    <id property="id" column="id"/>
    <result property="pattern" column="pattern"/>
    <collection property="roles" ofType="Role">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="name_zh" property="nameZh"/>
    </collection>
    </resultMap>
    <select id="getAllMenu" resultMap="MenuResultMap">
    select m.*, r.id, r.name, r.name_zh
    from menu m
    left join menu_role mr on m.id = mr.mid
    left join role r on r.id = mr.rid
    </select>
    </mapper>

业务类实现

  • UserDetailsService

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    package com.example.baizhisecurity.service;

    import com.example.baizhisecurity.entity.Role;
    import com.example.baizhisecurity.entity.User;
    import com.example.baizhisecurity.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsPasswordService;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    import org.springframework.util.ObjectUtils;

    import java.util.List;

    @Service
    public class MyUserDetalService implements UserDetailsService, UserDetailsPasswordService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 1. 查询用户
    User user = userMapper.findUserByUserName(username);
    if (ObjectUtils.isEmpty(user)) {
    throw new UsernameNotFoundException("用户不存在");
    }
    // 2. 查询用户的权限信息
    // 查询权限信息
    List<Role> roles = userMapper.getRoleByUid(user.getId());
    user.setRoles(roles);
    return user;
    }

    /**
    * 自动密码升级解决方案 {推荐: 随着 SpringSecurity 版本的升级,密码的底层加密会实现自动升级}
    *
    * @param user
    * @param newPassword
    * @return
    */
    // 实现密码更新
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
    Integer updatePassword = userMapper.updatePassword(user.getUsername(), newPassword);
    if (updatePassword == 1) {
    ((User) user).setPassword(newPassword);
    }
    return user;
    }
    }

Token

  • 认证通过的回调中生成Token

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    /**
    * 自定义认证成功后的处理
    */
    @Slf4j
    @Component
    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private final RedisTemplate redisTemplate;
    // token 工具类
    private final TokenManager tokenManager;

    @Autowired
    public MyAuthenticationSuccessHandler(RedisTemplate redisTemplate, TokenManager tokenManager) {
    this.redisTemplate = redisTemplate;
    this.tokenManager = tokenManager;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    log.info("info: {}", SecurityContextHolder.getContext().getAuthentication().toString());
    // 生成 token
    User user = (User) authentication.getPrincipal();
    String username = user.getUsername();
    // 生成 token
    String token = tokenManager.createToken(username);
    // 存储到 redis, 键值: username:权限
    log.info("Authorities: {}", user.getAuthorities());
    redisTemplate.opsForValue().set(username, user.getAuthorities());
    // 返回 token 给前端页面
    Map<String, String> map = new HashMap<>();
    map.put("token", token);
    ResponseUtil.out(response, ResultModel.success(HttpStatus.SUCCESS, "认证成功", map));
    }
    }

  • 测试获取

    认证成功时生成Token

授权

  • 重写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    package com.example.baizhisecurity.security.metasource;

    import com.example.baizhisecurity.entity.Menu;
    import com.example.baizhisecurity.service.MenuService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.ConfigAttribute;
    import org.springframework.security.access.SecurityConfig;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;

    import java.util.Collection;
    import java.util.List;

    @Slf4j
    @Component
    public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    private final MenuService menuService;

    @Autowired
    public CustomSecurityMetadataSource(MenuService menuService) {
    this.menuService = menuService;
    }

    // 用于路径匹配
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
    * 构建元数据信息集合
    * /admin/** ROLE_ADMIN
    * /teacher/** ROLE_TEACHER
    * /student/** ROLE_TEACHER ROLE_USER
    *
    * @param object
    * @return
    * @throws IllegalArgumentException
    */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
    // 1. 获取当前请求对象
    String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
    // 2. 查询所有菜单
    List<Menu> allMenu = menuService.getAllMenu();
    for (Menu menu : allMenu) {
    if (antPathMatcher.match(menu.getPattern(), requestURI)) {
    String[] roles = menu.getRoles().stream().map(r -> r.getName()).toArray(String[]::new);
    log.info("roles: {}", roles);
    return SecurityConfig.createList(roles);
    }
    }
    return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
    return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
    return FilterInvocation.class.isAssignableFrom(clazz);
    }
    }

前端部分

  • 登录表单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    <template>
    <div class="login" v-cloak>
    <div class="left">
    <video autoplay="autoplay" loop="loop" muted oncanplay="true" src="@/assets/video/passport.mp4"></video>
    </div>
    <div class="right">
    <div class="box">
    <p>
    <strong> 登录 </strong>
    <span>没有账户? <router-link to="/register">免费注册</router-link>
    </span>
    </p>
    <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
    <el-form-item label="" prop="username">
    <el-input placeholder="请输入账号" v-model="ruleForm.username" type="text">
    <i slot="suffix" class="el-input__icon icon-jurassic_user"></i>
    </el-input>
    </el-form-item>
    <el-form-item label="" prop="password">
    <el-input v-model="ruleForm.password" ref="pwdRef" placeholder="请输入密码" :type="inputType">
    <i slot="suffix" class="el-input__icon icon-mima" @click="showPasswd"></i>
    </el-input>
    </el-form-item>
    <el-form-item label="" prop="kaptcha" class="code">
    <el-input placeholder="请输入验证码" v-model="ruleForm.kaptcha" type="text" style="width: 170px;">
    <i slot="suffix" class="el-input__icon icon-yanzhengma"></i>
    </el-input>
    <img :src="kaptchaCode" ref="captchaImg" alt="" title="点击刷新" @click="refreshCaptcha">
    </el-form-item>
    <el-button @click="loginHandle">登录</el-button>
    </el-form>
    </div>
    </div>
    </div>
    </template>

    <script>
    import { loginNetwork, refNewCode, getHello } from "@/network/user/user";
    export default {
    data() {
    return {
    ruleForm: {
    username: '', // 用户名
    password: '', // 密码
    kaptcha: '' // 验证码
    },
    kaptchaCode: "",
    showPassword: false, // 默认不显示密码
    rules: {
    username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    {
    min: 3,
    max: 15,
    message: '长度在 3 到 15 个字符',
    trigger: 'blur',
    },
    ],
    password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    {
    min: 3,
    max: 15,
    message: '长度在 3 到 15 个字符',
    trigger: 'blur',
    },
    ],
    kaptcha: [
    { required: true, message: '请输入验证码', trigger: 'blur' },
    {
    min: 3,
    max: 5,
    message: '长度在 4 个字符',
    trigger: 'blur',
    },
    ],
    },
    }
    },
    computed: {
    // 修改密码显示
    inputType() {
    return this.showPassword ? 'text' : 'password';
    },
    },
    created() {
    this.refreshCaptcha()
    },
    methods: {
    // 点击刷新验证码
    refreshCaptcha() {
    refNewCode().then(res => {
    if (res.code === 200) {
    // 解析 base64 图片资源 data:image/png;base64,
    this.kaptchaCode = "data:image/png;base64," + res.data
    this.$message.success(res.message || "刷新成功!")
    } else {
    this.$message.error(res.message || "验证码获取失败!")
    }
    })

    },
    // 点击显示验证码明文字符
    showPasswd() {
    this.showPassword = !this.showPassword;
    },
    // 点击登录事件
    loginHandle() {
    // 表单校验
    this.$refs.ruleForm.validate((valid) => {
    if (valid) {
    loginNetwork(this.ruleForm).then(res => {
    console.log("loginNetwork: ", res)
    // 判断 code
    if (res.code === 200) {
    this.$message.success(res.message)
    // TODO 页面跳转
    this.$router.push("/admin")
    getHello().then(res => {
    console.log("getHello: ", res)
    })
    } else {
    this.$message.error(res.message)
    }

    })
    }
    })
    }
    },
    }
    </script>

    <style lang="less" scoped>
    [v-cloak] {
    display: none;
    }

    .code {
    display: flex;
    justify-content: space-between;
    align-items: center;

    img {
    height: 40px;
    line-height: 40px;
    margin-left: 10px;
    vertical-align: middle;
    }
    }

    .icon-yanjing_xianshi {
    position: absolute;
    font-size: 14px;
    z-index: 1;
    right: 10px;
    color: #606266;
    font-family: iconfont;
    }

    .el-button:hover {
    background: #ffa459;
    }

    .icon-mima,
    .icon-yanzhengma,
    .icon-jurassic_user {
    font-family: iconfont;
    }

    .box p {
    position: relative;
    left: 80px;
    padding: 20px;

    strong {
    font-size: 32px;
    font-weight: 600;
    line-height: 40px;
    color: #121315;
    }

    span {
    display: block;
    margin-top: 8px;
    font-size: 14px;
    font-weight: 400;
    line-height: 22px;
    color: #767e89;
    }

    a {
    color: #fb9337;
    cursor: pointer;
    transition: color 0.3s;
    }
    }

    .right {
    position: relative;
    width: 50%;
    margin-left: 140px;
    box-sizing: border-box;

    .box {
    position: absolute;
    top: 300px;
    }

    .el-form {
    width: 100%;

    .el-input {
    width: 300px;
    }
    }
    }

    .el-button {
    position: relative;
    left: 100px;
    width: 300px;
    color: #fff;
    background-color: #fb9337;
    }

    .login {
    display: flex;
    justify-content: space-between;
    width: 100%;
    height: 100%;

    .left video {
    display: inline-block;
    width: 100%;
    height: 100vh;
    object-fit: cover;
    }
    }
    </style>
    • 渲染效果

      登录页面
      ilMcJk.png
  • 请求封装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // user.js
    import { request } from "../request";

    // 定义新的文件进行引用,单独管理文件
    export function loginNetwork(ruleForm) {
    return request({
    method: "post",
    url: "/login",
    // post
    data: { ...ruleForm },
    });
    }

    export function refNewCode() {
    return request({
    method: "get",
    url: "/captcha",
    });
    }

  • Vue CSRF 的配置

    • 下载插件

      1
      2
      # 下载 cookie 使用的插件
      npm install vue-cookie --save
    • 使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      // config.js
      import axios from "axios";
      import VueCookie from "vue-cookie";

      axios.defaults.xsrfHeaderName = "X-CSRF-TOKEN";
      axios.defaults.xsrfCookieName = "CSRF-TOKEN";
      axios.defaults.withCredentials = "true";

      export function request(config) {
      // 1.创建axios的实例
      const instance = axios.create({
      baseURL: "http://localhost:8080",
      timeout: 5000,
      });

      // 2.axios的拦截器
      // 2.1.请求拦截的作用
      instance.interceptors.request.use(
      (config) => {
      // 在发送请求之前做些什么
      // 获取 CSRF Token
      const csrfToken = VueCookie.get("XSRF-TOKEN");
      console.log("csrfToken: " + csrfToken);
      if (csrfToken) {
      // 在请求头中添加 CSRF Token
      config.headers["X-XSRF-TOKEN"] = csrfToken;
      }
      return config;
      },
      (err) => {
      // 对请求错误做些什么
      return Promise.reject(err);
      }
      );

      // 2.2.响应拦截
      instance.interceptors.response.use(
      (res) => {
      return res.data;
      },
      (err) => {
      console.log(err);
      }
      );

      // 3.发送真正的网络请求
      return instance(config);
      }

  • 发送请求认证测试

    表单测试
    il1WVN.gif

自定义SpringBoot异常页面

  • 配置

    1
    2
    3
    4
    server:
    error:
    include-message: always # 指定错误响应中始终包含错误消息的详细信息。
    include-binding-errors: always # 指定错误响应中始终包含与绑定错误相关的详细信息。 如果将它们的值设置为 never,则不会在错误响应中包含这些信息。
  • 定义控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.example.baizhisecurity.controller;

    import com.example.baizhisecurity.common.ResultModel;
    import org.springframework.boot.web.servlet.error.ErrorController;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class CustomErrorController implements ErrorController {

    @GetMapping("/error")
    public ResultModel handleNotFound() {
    return ResultModel.error(HttpStatus.NOT_FOUND.value(), "Not Found");
    }

    public String getErrorPath() {
    return "/error";
    }
    }
  • 访问测试

    效果测试
    i2uvBC.png

代码下载

特殊点说明

  • 项目整体采用的是前后端分离开发
  • 前后端分离后的特点是所有响应以JSON 格式显示
  • 在登录页面上,需要特别的注意自定义登录页面是针对传统的WEB 开发,而前后端分离是将登陆表单以JSON 格式显示的

项目测试

  • 测试获取验证码

    http://localhost:8080/captcha

    data 是图片数据的Base64 显示,前端是需要拼接的 POSTMAN 测试
  • 测试直接访问控制器数据

    未登陆时访问数据
    • 细节

      1. 这里需要注意,使用的时候需要在header 中添加CSRF 需要的键值

        第一步获取cookie 中关于CSRF 相关的键值
      2. 将上图中红色框中的值复制下来,添加到本次请求的header

        CSRF 配置
      3. 在添加好后,再次访问请求

        成功获得认证
      4. 下次访问时,需要删除header CSRF 的值,之后再次添加

      5. 疑问点难道每一次都需要访问一次失败的再添加cookie后才能访问成功吗?

        在前端使用的时候,是通过添加相关的配置获取的是cookie 的内容,所以访问时就已经实现了添加,所以不会出现访问失败一次的现象。

      6. 在项目使用中关闭csrf

GIT-版本回退不一致

  • 原因

    起初是对IDEA 进行了关闭,后续使用git bash 终端执行git reset --hard commitId,在git bash 中已经明确的看到版本回退,但是IDEA 是未发生变化的,在后续IDEA 中通过IDEA 的终端查看当前HEAD 指向,发现并未发生变化

  • 差异

    回退差异
  • 结论

    当你在使用某个编辑器,如果编辑器内部有终端尽可能使用编辑器提供的内部终端避免误导,过多的消耗时间