benym的知识笔记 benym的知识笔记
🦮首页
  • Java

    • Java-基础
    • Java-集合
    • Java-多线程与并发
    • Java-JVM
    • Java-IO
  • Python

    • Python-基础
    • Python-机器学习
  • Kafka
  • Redis
  • MySQL
  • 分布式事务
  • Spring

    • SpringIOC
    • SpringAOP
🦌设计模式
  • 剑指Offer
  • LeetCode
  • 排序算法
🐧实践
  • Rpamis

    • Utils
    • Exception
    • Security
  • 归档
  • 标签
  • 目录
🦉里程碑
🐷关于
GitHub (opens new window)

benym

惟其艰难,才更显勇毅🍂惟其笃行,才弥足珍贵
🦮首页
  • Java

    • Java-基础
    • Java-集合
    • Java-多线程与并发
    • Java-JVM
    • Java-IO
  • Python

    • Python-基础
    • Python-机器学习
  • Kafka
  • Redis
  • MySQL
  • 分布式事务
  • Spring

    • SpringIOC
    • SpringAOP
🦌设计模式
  • 剑指Offer
  • LeetCode
  • 排序算法
🐧实践
  • Rpamis

    • Utils
    • Exception
    • Security
  • 归档
  • 标签
  • 目录
🦉里程碑
🐷关于
GitHub (opens new window)
  • Kafka

    • 概览
  • Redis

    • Redis实现共享Session
      • Redis实现共享Session
        • 代码实现
    • 自定义RedisTemplate
    • Redis哨兵
    • Redis持久化RDB
    • Redis持久化AOF
    • Redis分片集群
  • MySQL

    • MySQL索引原理及应用场景
  • 分布式事务

    • 事务的特性、CAP定理、BASE理论
    • 分布式事务XA、AT、TCC、SAGA
    • 分布式事务AT模式的脏写问题
    • 分布式事务TCC模式的空回滚和业务悬挂问题
  • 分布式与中间件
  • Redis
benym
2020-06-25
目录

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中获取

实现思路如下:

  1. 登陆页面提交用户名密码
  2. 登陆成功之后生成token。Token相当于原来的sessionId,字符串等,可以使用UUID
  3. 把用户信息保存到redis中。Key就是token,value就是userId
  4. 设置key的过期时间。模拟Session的过期时间
  5. 拦截器请求校验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
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

其中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
1
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
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

之后设置一个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
1
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
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

登陆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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
编辑 (opens new window)
#Redis#Java
上次更新: 2023/07/11, 17:06:57
概览
自定义RedisTemplate

← 概览 自定义RedisTemplate→

最近更新
01
SpringCache基本配置类
05-16
02
DSTransactional与Transactional事务混用死锁场景分析
03-04
03
Rpamis-security-原理解析
12-13
更多文章>
Theme by Vdoing | Copyright © 2018-2024 benym | MIT License
 |   |   | 
渝ICP备18012574号 | 渝公网安备50010902502537号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式