LongZ

积硅步,至千里

  • 主页
  • 随笔
  • 杂记
  • 算法
  • 归档
所有文章 关于我

LongZ

积硅步,至千里

  • 主页
  • 随笔
  • 杂记
  • 算法
  • 归档

基于Redis实现共享Session登录

2023-08-22
字数统计: 1.6k字   |   阅读时长: 6分

阅读数: 10次   

大家都知道,Tomcat服务器并不共享 Session ,当一个项目部署在一个集群时,假设第一次路由分配给到 tomcat01,Session会保存在这台服务器上,但是短时间内当第二次访问时(可以看作是无操作,再次刷新时),被分配到tomcat02上,这台服务器的tomcat就没有Session的,这时显示前端显示未登录,这样显然是不合理的。这就叫Tomcat服务器的Session混淆问题。

Session共享问题主要说的是多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务器时导致数据丢失的问题。那么久需要思考一个替代的方案了,而且这个替代方案应该满足以下三点:数据共享、内存存储、key:value结构。满足以上三点的,主流的服务器可以选择 Redis 服务器,首先时基于内存存储的,访问速度快;集群数据共享,可以解决session混淆问题;而且是key:value结构存储数据的,存取方便;并且具有各种缓存淘汰机制,不用过于担心性能问题。

Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式,可以省去我们自定义RedisTemplate的过程,可以直接调用:

1
2
@Autowired
private StringRedisTemplate stringRedisTemplate;

下面是基于StringRedisTemplate来实现的Session共享:

一般的登录登录页面可能会用到验证码来校验,这时也可以传入session来辅助登录。

下面是一个根据手机号来获取验证码登录的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public Result sendCode(String phone, HttpSession session) {
// 1. 先判断手机号有没有符合规范
if (RegexUtils.isPhoneInvalid(phone)) {
// 2. 如果不符合就返回错误信息
return Result.fail("手机号码无效格式!");
}
// 3. 如果手机号码符合符合,就生成一个验证码
String code = RandomUtil.randomNumbers(6);
// 4. 生成的验证码保存到 redis 里面(用手机号区分)
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5. 生成的验证码发送给用户端(模拟发送校验码,具体实际开发自行替换)
log.info("生成的验证码信息:{}", code);
// 6. 返回信息
return Result.ok();
}

注意:RegexUtils是自定义的工具,isPhoneInvalid方法是用来判断手机号是否符合规范,符合才能发送验证码;LOGIN_CODE_KEY和LOGIN_CODE_TTL静态变量都是自定义的值,防止多处编写出错,第一个是Redis的key的前缀,第二个是验证码过期时间。

登录的共享session的处理,并且用到了hutool工具包,代码如下所示:

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
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1. 先校验手机号格式
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合就返回错误信息
return Result.fail("手机号码无效格式!");
}
// 2. 校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)){
// 验证码错误
return Result.fail("验证码错误!");
}
// 3. 查询数据库,判断用户是否存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("phone",phone);
User user = userMapper.selectOne(queryWrapper);

// 4. 用户不存在则新建用户并保存
if (user == null){
user = createUserWithPhone(phone);
}
// 5. 把用户信息保存到 Redis 中
// 5.1 随机生成一个 token 作为令牌存储到 Redis 中
String token = UUID.randomUUID().toString(false);

// 5.2 将 User 对象转换为 HashMap 对象存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,
new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true) // 忽略空值参数
.setFieldValueEditor((k, v) -> v.toString())); // 处理非String类型转换失败问题
// 6. 把 token 保存到 redis 中
String keyToken = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(keyToken, userMap);
// 7. 设置过期时间
stringRedisTemplate.expire(keyToken, LOGIN_USER_TTL, TimeUnit.MINUTES);

// 8. 返回 token
return Result.ok(token);
}

注意:LoginFormDTO是只封装了手机号、验证码和密码三个字段,防止数据过多而导致有泄露的风险。这里使用UUID来作为token来替代session存储到redis服务器中,当用户登录的时候,就会尝试获取token,如果能够获取到,就直接登录并刷新token有效期,为空就创建存储并放行。

对于token有效期的刷新,可以自定义一个拦截器,用来刷新session的有效期,防止用户在使用过程中session过期的问题:

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
/**
* token 刷新拦截器,拦截一切路径
*/
public class RefreshInterceptor implements HandlerInterceptor {

private StringRedisTemplate stringRedisTemplate;

public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

// 基于 Redis 实现登录
// 1. 获取请求头中的 token
String token = request.getHeader("authorization");
if (StrUtil.isEmpty(token)) {
// 不管有没有登录,一直放行
return true;
}
// 2. 基于 Token 来获取 redis 中的用户信息
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3. 判断用户是否存在
if (userMap.isEmpty()) {
// 这里也是一直放行
return true;
}
// 5. 将查询到的Hash用户转换成 UserDto 对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6. 用户存在,存到 ThreadLocal中
UserHolder.saveUser(userDTO);
// 7. 刷新 token 有效期
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8. 放行
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 获取完用户信息后及时销毁,防止信息泄漏
UserHolder.removeUser();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 登录拦截器,只要判断有无用户即可,无就拦截(其他东西给刷新拦截器做)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 基于 ThreadLocal 来判断有没有用户,判断拦截
if (UserHolder.getUser() == null) {
// 没有用户,拦截
response.setStatus(401);
return false;
}
// 2. 有用户,则放行
return true;
}

注意:UserHolder是基于ThreadLocal来实现的,在同一个线程内,方法区内存是共享的,所以可以获得这个线程的信息。需要自定义拦截的路径,如果配置多个拦截器,要给拦截器指定执行顺序,order方法可以给自定义的拦截器加权重,权重越小执行优先级越高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

public static void saveUser(UserDTO user){
tl.set(user);
}

public static UserDTO getUser(){
return tl.get();
}

public static void removeUser(){
tl.remove();
}
}

以上是基于Redis实现Session共享的问题的一种解决方案,还有更好的替代方案,比如在处理拦截的时候,可以使用更专业的工具,如spring getaway 可以很好地在微服务架构中应用;比如生成token的算法,可以选择雪花算法等。

本文作者: Long HY
本文链接: https://longzas.github.io/2023/08/22/%E5%9F%BA%E4%BA%8ERedis%E5%AE%9E%E7%8E%B0%E5%85%B1%E4%BA%ABSession%E7%99%BB%E5%BD%95/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!
  • Redis缓存
  • Redis
Redis缓存穿透问题及解决方法
MySQL管理中一些好用的工具
0 comments
Anonymous
Markdown is supported

Be the first person to leave a comment!

© 2023 Long HY
总访问量:1759次 | 总访客:1664人
  • 所有文章
  • 关于我

tag:

  • 杂记
  • 随笔
  • Redis高并发
  • Redis缓存
  • Java多线程
  • 算法
  • 二分查找法

    2023-10-04

    #算法

  • 哲学家吃饭问题

    2023-10-02

    #Java多线程

  • 基于Redisson优化秒杀业务

    2023-09-02

    #Redis高并发

  • Lua脚本优化分布式锁原子性问题

    2023-08-29

    #Redis高并发

  • Redis分布式锁

    2023-08-26

    #Redis高并发

  • Redis高并发场景:商品秒杀活动

    2023-08-26

    #Redis高并发

  • Redis缓存工具类封装

    2023-08-25

    #随笔

  • Redis 缓存击穿问题及解决方法

    2023-08-24

    #Redis缓存

  • Redis缓存穿透问题及解决方法

    2023-08-23

    #Redis缓存

  • 基于Redis实现共享Session登录

    2023-08-22

    #Redis缓存

  • MySQL管理中一些好用的工具

    2023-08-21

    #随笔

  • 序列化过程中版本号不一致的问题

    2023-08-20

    #随笔

  • 往虚拟机的MySQL数据库插入大量数据的一种解决方案

    2023-08-18

    #随笔

  • 对System.out.println()这个打印语句的认识

    2023-08-16

    #随笔

  • Github访问速度慢的一个解决方案

    2023-08-15

    #杂记

今夜无事,勾栏听曲

积硅步,至千里。

     共同努力!

邮箱地址:longhy1008@qq.com