# 1.
引入依赖:
1 | <dependency> |
一些必要的配置:
新建一个配置类 WebSecurityConfig
继承 WebSecurityConfigurerAdapter
重写 configure
方法。(重要)
- 是 SpringSecurity 的核心
1 |
|
新建一个 controller 用来测试登录
1 | /** |
启动项目:终端会有这么一段日志
1 | Using generated security password: f429b724-db54-4a56-ae82-7ebb63f22d69 |
表示:没有设置用户信息,给出了一个默认用户及密码,默认用户
user
登录之后,默认会跳转到 Index 页面,但是目前没有这个页面,所以会报错。
暂不处理。
访问: http://localhost:8080/user-info
返回结果:
1 | {"credentials":null,"details":null,"authenticated":false,"authorities":null,"principal":null,"name":"not login!"} |
可以通过 http://localhost:8080/logount
退出登录
之后在访问 user-info 接口,发现不在包含用户信息
# 2.
实际使用中 没有登录的用户是不能访问接口的
修改 WebSecurityConfig
1 | @Configuration |
重启项目
此时,访问: http://localhost:8080/user-info
发现会直接跳转到登录页面
# 增加一些细节
# 依赖
1 | <dependencies> |
# 配置
1 | server: |
在
com.li
新建utils
包,新建JwtUtils
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
public class JwtUtils {
//算法密钥
private String jwtSecretKey;
// 过期时间
private long expiration;
/**
* 创建jwt
* @param userInfo 用户信息
* @param authList 用户权限列表
* @return 返回jwt(JSON WEB TOKEN)
*/
public String createToken(String userInfo, List<String> authList) {
//创建时间
Date currentTime = new Date();
//过期时间,5分钟后过期
Date expireTime = new Date(currentTime.getTime() + expiration);
//jwt 的header信息
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("type", "JWT");
headerClaims.put("alg", "HS256");
//创建jwt
return JWT.create()
.withHeader(headerClaims) // 头部
.withIssuedAt(currentTime) //已注册声明:签发日期,发行日期
.withExpiresAt(expireTime) //已注册声明 过期时间
.withIssuer("thomas") //已注册声明,签发人
.withClaim("userInfo", userInfo) //私有声明,可以自己定义
.withClaim("authList", authList) //私有声明,可以自定义
.sign(Algorithm.HMAC256(jwtSecretKey)); // 签名,使用HS256算法签名,并使用密钥
// HS256是一种对称算法,这意味着只有一个密钥,在双方之间共享。 使用相同的密钥生成签名并对其进行验证。 应特别注意钥匙是否保密。
}
/**
* 验证jwt的签名,简称验签
*
* @param token 需要验签的jwt
* @return 验签结果
*/
public boolean verifyToken(String token) {
//获取验签类对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签,如果不报错,则说明jwt是合法的,而且也没有过期
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return true;
} catch (JWTVerificationException e) {
//如果报错说明jwt 为非法的,或者已过期(已过期也属于非法的)
log.error("验签失败:{}", token);
}
return false;
}
/**
* 获取用户id
* * @param token jwt
* @return 用户id
*/ public String getUserInfo(String token) {
//创建jwt验签对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//获取payload中userInfo的值,并返回
return decodedJWT.getClaim("userInfo").asString();
} catch (JWTVerificationException e) {
log.error("getUserInfo error", e);
}
return null;
}
/**
* 获取用户权限
*
* @param token
* @return
*/
public List<String> getUserAuth(String token) {
//创建jwt验签对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//获取payload中的自定义数据authList(权限列表),并返回
return decodedJWT.getClaim("authList").asList(String.class);
} catch (JWTVerificationException e) {
log.error("getUserAuth error", e);
}
return null;
}
}在
com.li
新建filter
包,新建SaySomethingJWTFilter
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/**
* @author zhanghuapeng
* @date 2024/2/22
* @desc 一次性请求过滤器
*/
public class SaySomethingJWTFilter extends OncePerRequestFilter {
private ObjectMapper objectMapper;
private StringRedisTemplate stringRedisTemplate;
private JwtUtils jwtUtils;
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求uri
String requestURI = request.getRequestURI();
// 如果是登录页面,放行
if (requestURI.equals("/login")) {
filterChain.doFilter(request, response);
return;
}
//获取请求头中的Authorization
String authorization = request.getHeader("Authorization");
//如果Authorization为空,那么不允许用户访问,直接返回
if (!StringUtils.hasText(authorization)) {
printFront(response, "没有登录!");
return;
}
//Authorization 去掉头部的Bearer 信息,获取token值
String jwtToken = authorization.replace("Bearer ", "");
//验签
boolean verifyTokenResult = jwtUtils.verifyToken(jwtToken);
//验签不成功
if (!verifyTokenResult) {
printFront(response, "jwtToken 已过期");
return;
}
//从payload中获取userInfo
String userInfo = jwtUtils.getUserInfo(jwtToken);
//从payload中获取授权列表
List<String> userAuth = jwtUtils.getUserAuth(jwtToken);
//创建登录用户
SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);
SecurityUser securityUser = new SecurityUser(sysUser);
//设置权限
List<SimpleGrantedAuthority> authList = userAuth.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
securityUser.setAuthorities(authList);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToke = new UsernamePasswordAuthenticationToken(securityUser
, null, authList);
//通过安全上下文设置认证信息
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToke);
//继续访问相应的rul等
filterChain.doFilter(request, response);
}
private void printFront(HttpServletResponse response, String message) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
HttpResult httpResult = new HttpResult();
httpResult.setCode(-1);
httpResult.setMsg(message);
writer.print(objectMapper.writeValueAsString(httpResult));
writer.flush();
}
}调整
SecurityConfig
, 将过滤器添加到配置中1
2
3
4
5
6
7
8
9
private SaySTokenFilter saySTokenFilter;
protected void configure(HttpSecurity http) throws Exception {
// 增加配置
http.addFilterBefore(saySomethingJWTFilter, UsernamePasswordAuthenticationFilter.class);
// ...原来的配置
}
# 调试
1 | # 不携带token访问http://localhost:8080/user-info |
- 在
com.li.config
, 新建SaySAuthenticationSuccessHandler
# 设置权限
在 loadUserByUsername 中获取权限,并设置到 SecurityUser 中
1 | // com.li.service.impl.UserServiceImpl |
在 SaySAuthenticationSuccessHandler.onAuthenticationSuccess 中,生成 Token 时,可以将权限信息一起放入 Token 中。
1 | List<String> authList = new ArrayList<>(); |
# 注销处理
Jwt 本质上是一个字符串,无法手动将其过期,也就是说,即使手动退出登录,对于 Token 来说,还是一个有效的 Token,可以通过接入 Redis 来解决这一问题
登录成功时,将 Token 写入 Redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// SaySAuthenticationSuccessHandler
// 设置过期时间
private long expiration;
// 引入StringRedisTemplate
private StringRedisTemplate stringRedisTemplate;
// 在创建Token之后,将Token存到Redis中
onAuthenticationSuccess(){
// 创建Token
String token = saySJwtUtils.createToken(userInfo, authList);
// 写入Redis
stringRedisTemplate.opsForValue().set("login_token:" + token, objectMapper.writeValueAsString(authentication), expiration, TimeUnit.MILLISECONDS);
}校验 Token 时,先验签,再去 Redis 中判断 Token 是否还存在
- 如果验签成功,但是 Redis 中不存在,说明 Token 被手动过期了
1
2
3
4
5
6
7
8
9
10doFilterInternal(){
...
// 从Redis获取token并校验
String tokenInRedis = stringRedisTemplate.opsForValue().get("login_token:" + jwtToken);
if (!StringUtils.hasText(tokenInRedis)) {
printFront(response, "用户已退出,请重新登录");
return;
}
...
}
在 com.li.config
,新建 SaysLogoutSuccessHandler
1 |
|
调整 SecurityConfig
1 |
|
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks