前后端分离下的 SpringSecurity
前后端分离下的 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
<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
28spring:
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
64package 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;
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<>();
// 权限集合
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public boolean isAccountNonExpired() {
return accountNonExpired;
}
public boolean isAccountNonLocked() {
return accountNonLocked;
}
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
public boolean isEnabled() {
return enabled;
}
} -
角色实体
1
2
3
4
5
6
7
8
9
10
11
12
13
14package com.example.baizhisecurity.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
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
19package com.example.baizhisecurity.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
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
31package 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;
public class HelloController {
public ResultModel adminHello() {
return ResultModel.success(HttpStatus.OK.value(), "数据获取成功", "Hello SpringSecurity Admin!");
}
public ResultModel teacherHello() {
return ResultModel.success(HttpStatus.OK.value(), "数据获取成功", "Hello SpringSecurity ADMIN and Teacher!");
}
public ResultModel studentHello() {
return ResultModel.success(HttpStatus.OK.value(), "数据获取成功", "Hello SpringSecurity Student!");
}
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
15public 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
81package com.example.baizhisecurity.common;
import lombok.Data;
import org.springframework.http.HttpStatus;
import java.io.Serializable;
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
283package 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;
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;
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 交给工厂管理
*/
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
*/
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 自定义 AuthenticationManager 推荐
* 它的作用是管理用户认证的过程。
* 具体来说,它接收用户的登录请求并从Spring Security 进行用户认证。在进行用户认证的过程中,AuthenticationManager
* 首先根据用户名获取用户信息,
* 然后将给定的用户名和密码与用户信息进行比较,如果验证通过,则认为用户已经被认证。如果验证失败,则会抛出异常,表示用户认证失败。
*
* @param auth
* @throws Exception
*/
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetalService);
}
/**
* 放行的资源不经过过滤器安全链
* LOG: Will not secure Ant [pattern='/img/**']
*
* @param web
* @throws Exception
*/
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 | 用户登录后可访问
*/
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>() {
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
*/
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
/**
* 前后端分离记住我的实现
*
* @return MyRememberServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository)
*/
public RememberMeServices rememberMeServices() {
return new MyRememberServices(UUID.randomUUID().toString(), userDetailsService(), persistentTokenRepository());
}
}
前后端分离相关自定义实现
-
自定义授权异常处理
1
2
3
4
5
6
7
8
9
10/**
* 自定义授权异常处理
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
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
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
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
/**
* 自定义认证失败的处理
*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
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/**
* 自定义认证成功后的处理
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
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/**
* 自定义注销成功的处理
*/
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
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
101package 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
*/
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;
}
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
44package 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 的实现,这个类必须实现它的构造方法
*/
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
*/
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
27package 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 配置,运行跨域请求
*/
public class CorsConfig implements WebMvcConfigurer {
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
public class KaptchaConfig {
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
public class CaptchaController {
private Producer producer;
private StringRedisTemplate redisTemplate;
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
8public 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
79package 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;
public class GlobalExceptionHandle extends RuntimeException {
protected ResultModel handleAllException(Exception ex) {
return ResultModel.error(HttpStatus.ERROR, ex.getMessage().toString());
}
protected ResultModel handleNotFound(Exception ex) {
log.error("错误信息: {}", ex.getMessage().toString());
return ResultModel.error(HttpStatus.NOT_FOUND, ex.getMessage().toString());
}
/**
* 权限校验异常
*/
public ResultModel handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return ResultModel.error(HttpStatus.FORBIDDEN, e.getMessage());
}
/**
* 请求方式不支持
*/
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());
}
/**
* 拦截未知的运行时异常
*/
public ResultModel handleRuntimeException(RuntimeException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return ResultModel.error(e.getMessage());
}
public ResultModel handleIllegalArgumentException(IllegalArgumentException ex) {
return ResultModel.error(ex.getMessage());
}
/**
* 自定义验证异常
*/
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
public interface UserMapper {
User findUserByUserName(String username);
List<Role> getRoleByUid(Integer uid);
Integer updatePassword(String username, ; 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
<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
<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
52package 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;
public class MyUserDetalService implements UserDetailsService, UserDetailsPasswordService {
private UserMapper userMapper;
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
*/
// 实现密码更新
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
/**
* 自定义认证成功后的处理
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final RedisTemplate redisTemplate;
// token 工具类
private final TokenManager tokenManager;
public MyAuthenticationSuccessHandler(RedisTemplate redisTemplate, TokenManager tokenManager) {
this.redisTemplate = redisTemplate;
this.tokenManager = tokenManager;
}
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
66package 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;
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private final MenuService menuService;
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
*/
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;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
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>-
渲染效果
登录页面
-
-
请求封装
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);
}
-
-
发送请求认证测试
表单测试
自定义 SpringBoot 异常页面
-
配置
1
2
3
4server:
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
20package 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;
public class CustomErrorController implements ErrorController {
public ResultModel handleNotFound() {
return ResultModel.error(HttpStatus.NOT_FOUND.value(), "Not Found");
}
public String getErrorPath() {
return "/error";
}
} -
访问测试
效果测试
代码下载
特殊点说明
- 项目整体采用的是前后端分离开发
- 前后端分离后的特点是所有响应以
JSON
格式显示 - 在登录页面上,需要特别的注意自定义登录页面是针对传统的
WEB
开发, 而前后端分离是将登陆表单以 JSON
格式显示的
项目测试
-
测试获取验证码
data
是图片数据的 Base64
显示, 前端是需要拼接的 POSTMAN
测试 -
测试直接访问控制器数据
未登陆时访问数据 -
细节
-
这里需要注意,使用的时候需要在
header
中添加 CSRF
需要的键值 第一步获取 cookie
中关于 CSRF
相关的键值 -
将上图中
红色框
中的值复制下来,添加到本次请求的 header
中 CSRF
配置 -
在添加好后,
再次访问请求 成功获得认证 -
下次访问时,需要删除
header
中 CSRF
的值,之后再次添加 -
疑问点
难道每一次都需要访问一次失败的再添加
cookie 后才能访问成功吗? 在前端使用的时候,
是通过添加相关的配置获取的是 cookie
的内容,所以访问时就已经实现了添加,所以不会出现访问失败一次的现象。 -
在项目使用中关闭
csrf
-
-
GIT-版本回退不一致
-
原因
起初是对
IDEA
进行了关闭, 后续使用 git bash
终端执行 git reset --hard commitId
,在git bash
中已经明确的看到版本回退, 但是 IDEA
是未发生变化的, 在后续 IDEA
中通过 IDEA
的终端查看当前 HEAD
指向, 发现并未发生变化 -
差异
回退差异 -
结论
当你在使用某个编辑器,如果编辑器内部有终端尽可能使用编辑器提供的内部终端避免误导,
过多的消耗时间