SpringSecurity多用户表登录

环境

  • Jdk-1.8
  • SpringBoot-2.7.3
  • MySQL-8.0.24
  • Redis-3.0.504-Windows10

边看关键代码,边注释细节,最后有完整代码地址

>folded SQL脚本
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
CREATE DATABASE /*!32312 IF NOT EXISTS*/`spring-security` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `spring-security`;

DROP TABLE IF EXISTS `base_admin`;

CREATE TABLE `base_admin` (
`id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`username` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
`password` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
`name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
`type` varchar(12) COLLATE utf8mb4_general_ci DEFAULT 'admin',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

/*Data for the table `base_admin` */

insert into `base_admin`(`id`,`username`,`password`,`name`,`type`,`create_time`) values

('1','1','$2a$10$tIzYl5VlcUrZnx4HKV8J0uOT7fDfN3PdN7m4qFu40.XmWBmc38LFa','admin-1','admin','2023-03-17 12:29:01'),

('2','2','$2a$10$tIzYl5VlcUrZnx4HKV8J0uOT7fDfN3PdN7m4qFu40.XmWBmc38LFa','admin-2','admin','2023-03-17 14:12:31');

/*Table structure for table `base_user` */

DROP TABLE IF EXISTS `base_user`;

CREATE TABLE `base_user` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`type` varchar(12) COLLATE utf8mb4_general_ci DEFAULT 'user',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

/*Data for the table `base_user` */

insert into `base_user`(`id`,`username`,`password`,`name`,`type`,`create_time`) values

('3','3','$2a$10$tIzYl5VlcUrZnx4HKV8J0uOT7fDfN3PdN7m4qFu40.XmWBmc38LFa','user-3','user','2023-03-17 12:29:11'),

('4','4','$2a$10$tIzYl5VlcUrZnx4HKV8J0uOT7fDfN3PdN7m4qFu40.XmWBmc38LFa','user-4','user','2023-03-17 14:11:57');
>folded pom.xml
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
<?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 http://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.3</version>
<relativePath/>
</parent>

<groupId>top.yuan67.webapp</groupId>
<artifactId>springboot-study</artifactId>
<version>2022.12.29</version>

<name>springboot-study</name>
<description>springboot周边生态框架学习</description>

<properties>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.8</java.version>
</properties>

<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.13-SNSAPSHOT</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.24</version>
<scope>runtime</scope>
</dependency>

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

<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.7</version>
</dependency>

<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.23</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>

</dependencies>

<build>

<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>

<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
>folded 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
package top.yuan67.webapp.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import top.yuan67.webapp.configuration.handler.CustomAccessDeniedHandler;
import top.yuan67.webapp.configuration.handler.CustomLogoutHandler;
import top.yuan67.webapp.configuration.handler.CustomLogoutSuccessHandler;
import top.yuan67.webapp.configuration.handler.UnAuthenticatedRequestHandler;

/**
* @Author: NieQiang
* @User: CAPTAIN
* @CreateTime: 2023-3-15
* @Desc: 描述信息
**/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

@Bean
public AuthenticationManager manager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}

String[] permitAll = {
"/oauth/loginAdmin",
"/oauth/loginUser"
};

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()//关闭csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//关闭session
.and()
.formLogin().disable()//关闭表单登录
.logout().addLogoutHandler(new CustomLogoutHandler())
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.and()
.exceptionHandling()
.authenticationEntryPoint(new UnAuthenticatedRequestHandler())
.accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
.authorizeRequests(auth->
auth.antMatchers(permitAll).permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
>folded 登录接口
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
package top.yuan67.webapp.controller;

import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.netty.util.internal.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import top.yuan67.webapp.constant.AuthorizationConstant;
import top.yuan67.webapp.entity.BaseAdmin;
import top.yuan67.webapp.entity.BaseUser;
import top.yuan67.webapp.exception.AccessTokenException;
import top.yuan67.webapp.service.BaseAdminService;
import top.yuan67.webapp.service.BaseUserService;
import top.yuan67.webapp.util.HTTPResponse;
import top.yuan67.webapp.util.MessageUtil;
import top.yuan67.webapp.util.TokenUtil;
import top.yuan67.webapp.vo.LoginAdminVO;

import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author CAPTAIN
*/
@RestController
@RequestMapping("/oauth")
public class BaseTokenController {
public static final Logger log = LoggerFactory.getLogger(BaseTokenController.class);

@Resource
private RedisTemplate redisTemplate;

@Resource
private TokenUtil tokenUtil;

@Resource
private BaseAdminService adminService;

@Resource
private BaseUserService userService;

@Resource
private BCryptPasswordEncoder encoder;

/**
* 管理员
* 用户名+密码
*
* @param login
* @return
*/
@PostMapping("/loginAdmin")
public HTTPResponse loginAdmin(@RequestBody LoginAdminVO login){
log.info("用户登录:{}", login);

Authentication authentication;
try {
DaoAuthenticationProvider adminProvider = new DaoAuthenticationProvider();
adminProvider.setUserDetailsService(adminService);
adminProvider.setPasswordEncoder(encoder);//设置密码加密方式
ProviderManager manager = new ProviderManager(adminProvider);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword());

authentication = manager.authenticate(token);
} catch (Exception e) {
return exception(login, e);
}
SecurityContextHolder.getContext()
.setAuthentication(UsernamePasswordAuthenticationToken
.authenticated(authentication.getPrincipal(), null, authentication.getAuthorities()));

return HTTPResponse.ok("登录成功", tokenUtil.login((BaseAdmin) authentication.getPrincipal()));
}


/**
* 普通用户
* 用户名+密码
* @param login
* @return
*/
@PostMapping("/loginUser")
public HTTPResponse loginUser(@RequestBody LoginAdminVO login) throws AccessTokenException {

Authentication authentication;
try{

DaoAuthenticationProvider userProvider = new DaoAuthenticationProvider();
userProvider.setUserDetailsService(userService);
userProvider.setPasswordEncoder(encoder);//设置密码加密方式
ProviderManager manager = new ProviderManager(userProvider);

UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword());

authentication = manager.authenticate(token);

}catch (Exception e){
return exception(login, e);
}

SecurityContextHolder.getContext()
.setAuthentication(UsernamePasswordAuthenticationToken
.authenticated(authentication.getPrincipal(), null, authentication.getAuthorities()));

return HTTPResponse.ok("登录成功", tokenUtil.login((BaseUser)authentication.getPrincipal()));
}


@GetMapping("/findCurrentUserInfo")
public Map<String, Object> findCurrentUserInfo(){
String token = redisTemplate.opsForHash()
.get(tokenUtil.accessTokenKey(), AuthorizationConstant.USER_INFO).toString();
return JSONObject.parseObject(token, Map.class);
}

/**
* 刷新TOKEN
* <b>这里用map返回是因为当需要返回刷新的token时前端不弹出提示框</b>
* @param refreshToken
* @return
*/
@PostMapping("/refreshToken/admin")
public Map<String, Object> refreshTokenAdmin(String refreshToken) {
return refreshToken(refreshToken, "admin");
}
@PostMapping("/refreshToken/user")
public Map<String, Object> refreshTokenUser(String refreshToken) {
return refreshToken(refreshToken, "user");
}

private Map<String, Object> refreshToken(String refreshToken, String type){
log.info("refreshToken:{}", refreshToken);

Map<String, Object> map = new LinkedHashMap<>();
if (tokenUtil.isNotEmpty(refreshToken)) {//如果请求头中有token
boolean verify;
try {
//令牌校验
verify = JWTUtil.verify(refreshToken, AuthorizationConstant.REFRESH_TOKEN_SECRET.getBytes());

log.info("verify:{}", verify);
} catch (Exception e) {
log.error("token无法解析:{}", e);
throw new RuntimeException(MessageUtil.get("令牌解析异常"));
}
if (verify) {//校验token是否正确,是否本机生成的token
try {
JWT jwt = JWTUtil.parseToken(refreshToken);
String key = tokenUtil.refreshTokenKey(refreshToken);
boolean b = redisTemplate.hasKey(key);
if(!b){
map.put("status", 500);
map.put("message", MessageUtil.get("刷新令牌过期"));
return map;
}
String header = jwt.getHeader(AuthorizationConstant.HEADER).toString();
if (!header.equalsIgnoreCase(AuthorizationConstant.HEADER_REFRESH_TOKEN)) {
map.put("status", 500);
map.put("message", MessageUtil.get("令牌解析异常"));
return map;
}

/**
* 这里在token是可正常解析的基础上用前端传进来的token解析得到用户ID,然后去redis获取token是为了就算是前端保存的token是过期的
* 只要传进来可以正常解析,就可以正常访问本地系统的接口,服务端刷新token,刷新后的不用返回给前端
*/
String redisRefreshToken = redisTemplate.opsForHash().get(key, AuthorizationConstant.TOKEN).toString();//redis中的token
String redisAdmin =
redisTemplate.opsForHash().get(key, AuthorizationConstant.USER_INFO).toString();//redis中的用户信息

//ACCESS_TOKEN过期时间
long exp = redisTemplate.getExpire(key);
//当前时间
long now = System.currentTimeMillis() / 1000;
log.info("key:{}, exp:{}, now:{}, cha:{}", key, exp, now, exp - now);
if (exp - now < 0) {//令牌过期===判断查到的刷新token是否过期
log.info("刷新令牌彻底过期exp:{}, now:{}, exp - now={}", exp, now, (exp - now));
map.put("status", 500);
map.put("message", MessageUtil.get("令牌过期!请重新登录"));
return map;
}
if (!redisRefreshToken.equals(refreshToken)) {//redis中的token
map.put("status", 500);
map.put("message", MessageUtil.get("令牌解析异常"));
return map;
}

log.info("刷新令牌还未过期,但距离过期还有{}分钟,所以刷新token", (exp - now) / 60);
if(type.equalsIgnoreCase("user")){
BaseUser user = JSON.parseObject(redisAdmin, BaseUser.class);
return tokenUtil.login(user);
}
if(type.equalsIgnoreCase("admin")){
BaseAdmin admin = JSON.parseObject(redisAdmin, BaseAdmin.class);
return tokenUtil.login(admin);
}
} catch (Exception e) {
log.warn("用户身份过期!:{}", e);
map.put("status", 500);
map.put("message", MessageUtil.get("用户身份过期!请重新登录"));
map.put("data", MessageUtil.get("用户身份不存在"));
return map;
}
}else{
map.put("status", 500);
map.put("message", MessageUtil.get("令牌解析异常"));
return map;
}
}
map.put("status", 500);
map.put("message", MessageUtil.get("关键参数不能为空"));
return map;
}

private HTTPResponse exception(LoginAdminVO login, Exception e) {

if (e instanceof BadCredentialsException) {
log.error("e====={}", e);
log.info("用户名或密码错误:username:{}, password:{}", login.getUsername(), login.getPassword());
return HTTPResponse.error("用户名或密码错误");
}
return HTTPResponse.error(e.getMessage());
}
}

【Github地址】分支为【spring-security】

若国内访问不方便可以用gitee导入,然后再重gitee克隆到本地

Git 仓库 URL
1
https://github.com/yuan67-top/springboot-study.git

评论