Redis实现共享Session
# Redis实现共享Session
提示
本文时间为2020-06-25,涉及技术可能已过时
Session共享,一般有一个这样的场景。以往单机的项目,数据请求都是在一个服务器上,session保存在这个服务器上自然是没有问题的。但是如果项目需要部署在多台服务器上的时候,session就会存在共享的问题
举一个例子:
假如现在有两台服务器同时运行,分别是ServerA和ServerB。
假设第一次请求(登陆请求)被分配到了A进行处理,A收到请求之后会生成一个sessionId并保存到内存中,然后返回给用户(浏览器),浏览器会把sessionId保存到cookie中,第一次请求完成。如果之后每一次请求还是由A来进行处理,那么一切正常。如果A服务器挂掉了,这时候请求被分配到B,B拿到的sessionId是由A生成的,两边就对不上了。于是用户会发现,需要重新登陆了。
那么这样的问题应该怎么样去解决呢?
首先,问题的根源出在sessionId无法共享上,想要把sessionId共享,一个简单的思路就是把sessionId保存到数据库中(这里选择redis),这样验证的时候就不再从当前服务器获取sessionId了,而改为了从redis中获取
实现思路如下:
- 登陆页面提交用户名密码
- 登陆成功之后生成token。Token相当于原来的sessionId,字符串等,可以使用UUID
- 把用户信息保存到redis中。Key就是token,value就是userId
- 设置key的过期时间。模拟Session的过期时间
- 拦截器请求校验sessionId
# 代码实现
登陆成功,生成sessionId存入redis
@Service
public class UserServiceImpl implements UserService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private RedisService redisService;
@Override
public LoginRespVo login(LoginReqVo vo) {
SysUser sysUser = sysUserMapper.selectByUsername(vo.getUsername());
if(sysUser==null){
throw new BusinessException(4001005,"不存在该用户,请先注册");
}
if(sysUser.getStatus()==2){
throw new BusinessException(4001006,"该帐号已被禁用");
}
if(!PasswordUtils.matches(sysUser.getSalt(),vo.getPassword(),sysUser.getPassword())){
throw new BusinessException(4001007,"用户名密码不匹配");
}
String token = UUID.randomUUID().toString();
LoginRespVo respVo = new LoginRespVo();
respVo.setUserId(sysUser.getId());
respVo.setToken(token);
redisService.set(token,sysUser.getId(),60, TimeUnit.MINUTES);
redisService.set(sysUser.getId(),token,60,TimeUnit.MINUTES);
return respVo;
}
}
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
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
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
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
其中LoginRespVo类为
package com.cqupt.lesson.vo.req;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class LoginReqVo {
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
SessionInterceptor拦截器校验sessionId,拦截器实现HandlerInterceptor的preHandle方法,可以获取从头部获取token或者从cookie获取token,之后进行token的验证
package com.cqupt.lesson.interceptor;
import com.cqupt.lesson.exception.BusinessException;
import com.cqupt.lesson.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
throw new BusinessException(4001002,"用户凭证不能为空");
}else {
if(!redisService.hasKey(token)){
throw new BusinessException(4001002,"用户凭证无效");
}
String userId = (String) redisService.get(token);
if(redisService.hasKey(userId)&&!token.equals(redisService.get(userId))){
throw new BusinessException(4001002,"帐号已在异地登陆");
}
}
return true;
}
}
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
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
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
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
之后设置一个web拦截器,并注入自定义的拦截器
package com.cqupt.lesson.config;
import com.cqupt.lesson.interceptor.TokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebAppConfig implements WebMvcConfigurer {
@Bean
public TokenInterceptor tokenInterceptor(){
return new TokenInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor()).addPathPatterns("/api/**").excludePathPatterns("/api/user/login","/api/user/register","/api/user/code/*");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
登陆Service代码实现
@Service
public class UserServiceImpl implements UserService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private RedisService redisService;
@Override
public LoginRespVo login(LoginReqVo vo) {
SysUser sysUser = sysUserMapper.selectByUsername(vo.getUsername());
if(sysUser==null){
throw new BusinessException(4001005,"不存在该用户,请先注册");
}
if(sysUser.getStatus()==2){
throw new BusinessException(4001006,"该帐号已被禁用");
}
if(!PasswordUtils.matches(sysUser.getSalt(),vo.getPassword(),sysUser.getPassword())){
throw new BusinessException(4001007,"用户名密码不匹配");
}
String token = UUID.randomUUID().toString();
LoginRespVo respVo = new LoginRespVo();
respVo.setUserId(sysUser.getId());
respVo.setToken(token);
redisService.set(token,sysUser.getId(),60, TimeUnit.MINUTES);
redisService.set(sysUser.getId(),token,60,TimeUnit.MINUTES);
return respVo;
}
}
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
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
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
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
登陆Controller实现
@RestController
@RequestMapping("/api")
@Api(tags = "用户模块",description = "用户模块相关接口")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/user/login")
@ApiModelProperty(value = "用户登陆接口")
public LoginRespVo login(@RequestBody LoginReqVo vo){
return userService.login(vo);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
编辑 (opens new window)
上次更新: 2023/07/11, 17:06:57
- 01
- SpringCache基本配置类05-16
- 03
- Rpamis-security-原理解析12-13