此篇文章为spring security系列的第一篇,着重讲解如何通过spring security完成企业级项目的权限控制,以及采用Redis的方式控制JWT的失效。
1. 什么是RBAC
RBAC(Role-Based Access Control )基于角色的权限控制,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。
2. JWT 和 Spring Security
spring security 授权主要分为两种,一种是security内部负责维护登录用户的session,一种则是采用JWT的方式,不管理session。关于JWT 和 Security的详细资料请小伙伴们自行查阅(相关网址推荐:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html)
此处就不在赘述,好了下面开始正文吧。
3.1 导入依赖
1 | <!-- spring security 和 jwt --> |
3. security 核心配置类:WebSecurityConfig
1 | @Configuration |
1. AuthenticationManagerBuilder auth
主要设置了security的加密方式,(BCryptPasswordEncoder
也是目前比较流行安全的一种加密方式,它比MD5效率更高),userDetailsService
则负责对用户名、密码的校验和授权。
2. HttpSecurity http
主要是对security核心过滤器链的配置,可配置登录、登出及异常等处理器,因为我们采用的是JWT的方式,因此禁用了security提供的登录和登出,配置了JWT的过滤器,以及RBAC校验的方式。
3. WebSecurity web
主要负责配置一些security放行的路径,文中通过customConfig读取在配置文件中设置的放行的URL。
4. 配置JWT 的认证过滤器JwtAuthenticationFilter
1 | /** |
此过滤器会拦截访问系统的所有的请求,因此需要放行所有被忽略的URL,包括登录和登出,并将校验通过的JWT的用户信息封装为authentication。
5. RBAC权限匹配器
1 | /** |
此方法看似很多,实则只做了一件事,就是把页面请求的URL和用户拥有的所有权限资源(URL)进行匹配。
6. UserDetailsService
查询数据库用户信息
1 | /** |
7. JWT的刷新和登录用户的注销
众所周知,JWT是无状态的,服务端无法通过解析JWT知道用户是否提前注销,因此借助了Redis的过期机制,来达到通知用户退出的目的。创建JWT时,会将生成的JWT以用户名为前缀存入Redis,退出时,清除Redis中此用户名的JWT,每次访问时解析JWT并判断Redis中是否还存在此用户名的JWT,若不存在,则表示此用户已注销。
JWT的续签,此处采用的是refresh_token的形式,及登录的时候创建两个JWT,一个token,一个refresh_token,refresh_token的过期时间设置比较长,token失效后,前端可调用refresh_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
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 /**
* jwt工具类
* @author daiyp
* @date 2018-9-26
*/
@EnableConfigurationProperties(JwtConfig.class)
@Configuration
@Slf4j
public class JwtUtil {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private JwtConfig jwtConfig;
/**
* 创建JWT
*
* @param authentication 用户认证信息
* @param rememberMe 记住我
* @return JWT
*/
public String createJWT(Authentication authentication, Boolean rememberMe, Boolean isRefresh) {
SecurityUser user = (SecurityUser) authentication.getPrincipal();
return createJWT(isRefresh, rememberMe, user.getId(), user.getUsername(), user.getRoles(), user.getMenus(), user.getAuthorities());
}
/**
* 创建JWT
*
* @param id 用户id
* @param subject 用户名
* @param roles 用户角色
* @param authorities 用户权限
* @return JWT
*/
public String createJWT(Boolean isRefresh,
Boolean rememberMe,
Long id,
String subject,
List<Role> roles,
List<MenuRight> menus,
Collection<? extends GrantedAuthority> authorities) {
Date now = new Date();
JwtBuilder builder = Jwts.builder()
.setId(id.toString())
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, jwtConfig.getSecret())
.claim("roles", roles)
// .claim("perms", menus)
.claim("authorities", authorities);
// 设置过期时间
Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
String redisKey;
if (isRefresh){
ttl *= 3;
redisKey = Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + subject;
}else{
redisKey = Constant.REDIS_JWT_TOKEN_KEY_PREFIX + subject;
}
if (ttl > 0) {
builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));
}
String jwt = builder.compact();
// 将生成的JWT保存至Redis
redisTemplate.opsForValue().set(redisKey, jwt, ttl, TimeUnit.MILLISECONDS);
return jwt;
}
/**
* 解析JWT
*
* @param jwt JWT
* @return {@link Claims}
*/
public Claims parseJWT(String jwt, Boolean isRefresh) {
try {
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret())
.parseClaimsJws(jwt)
.getBody();
String username = claims.getSubject();
String redisKey = (isRefresh ? Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX : Constant.REDIS_JWT_TOKEN_KEY_PREFIX)
+ username;
// 校验redis中的JWT是否存在
Long expire = redisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);
if (Objects.isNull(expire) || expire <= 0) {
throw new CustomException(ResultCode.TOKEN_EXPIRED);
}
// 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期
String redisToken = (String) redisTemplate.opsForValue().get(redisKey);
if (!StrUtil.equals(jwt, redisToken)) {
throw new CustomException(ResultCode.TOKEN_OUT_OF_CTRL);
}
return claims;
} catch (ExpiredJwtException e) {
log.error("Token 已过期");
throw new CustomException(ResultCode.TOKEN_EXPIRED);
} catch (UnsupportedJwtException e) {
log.error("不支持的 Token");
throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
} catch (MalformedJwtException e) {
log.error("Token 无效");
throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
} catch (IllegalArgumentException e) {
log.error("Token 参数不存在");
throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
}
}
/**
* 设置JWT过期
*
* @param request 请求
*/
public void invalidateJWT(HttpServletRequest request) {
String jwt = getJwtFromRequest(request);
String username = getUsernameFromJWT(jwt, false);
// 从redis中清除JWT
redisTemplate.delete(Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + username);
redisTemplate.delete(Constant.REDIS_JWT_TOKEN_KEY_PREFIX + username);
}
/**
* 从 request 的 header 中获取 JWT
*
* @param request 请求
* @return JWT
*/
public String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
/**
* 根据 jwt 获取用户名
*
* @param jwt JWT
* @return 用户名
*/
public String getUsernameFromJWT(String jwt, Boolean isRefresh) {
Claims claims = parseJWT(jwt, isRefresh);
return claims.getSubject();
}
public Map<String, String> refreshJWT(String token) {
Claims claims = parseJWT(token, true);
// 获取签发时间
Date lastTime = claims.getExpiration();
// 1. 判断refreshToken是否过期
if (!new Date().before(lastTime)){
throw new CustomException(ResultCode.TOKEN_EXPIRED);
}
// 2. 在redis中删除之前的token和refreshToken
String username = claims.getSubject();
// redisTemplate.delete(Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + username);
// redisTemplate.delete(Constant.REDIS_JWT_TOKEN_KEY_PREFIX + username);
// 3. 创建新的token和refreshToken并存入redis
String jwtToken = createJWT(false, false, Long.parseLong(claims.getId()), username,
(List<Role>) claims.get("roles"), null, (Collection<? extends GrantedAuthority>) claims.get("authorities"));
String refreshJwtToken = createJWT(true, false, Long.parseLong(claims.getId()), username,
(List<Role>) claims.get("roles"), null, (Collection<? extends GrantedAuthority>) claims.get("authorities"));
Map<String, String> map = new HashMap<>();
map.put("token", jwtToken);
map.put("refreshToken", refreshJwtToken);
return map;
}
/**
*
* 功能:生成 jwt token<br/>
* @param name 实例名
* @param param 需要保存的参数
* @param secret 秘钥
* @param expirationtime 过期时间(5分钟 5*60*1000)
* @return
*
*/
public static String sign(String name, Map<String,Object> param, String secret, Long expirationtime){
String JWT = Jwts.builder()
.setClaims(param)
.setSubject(name)
.setExpiration(new Date(System.currentTimeMillis() + expirationtime))
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
return JWT;
}
/**
*
* 功能:解密 jwt<br/>
* @param JWT token字符串
* @param secret 秘钥
* @return
* @exception
*
*/
public static Claims verify(String JWT, String secret){
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(JWT)
.getBody();
return claims;
}
public static Object getValueFromToken(String jwt,String key, String secret){
return verify(jwt, secret).get(key);
}
}
登录和登出的方法
1 | /** |
7. 效果测试
8. 数据库和源码
上面只是项目的部分核心代码,完整代码和数据库已托管到Github上,请访问源码链接自行下载,觉得有用的话,记得star哦,有什么问题欢迎大家通过issues或者邮件进行交流。