
文章目录
- 完成任务
-
- [1. 集群 Session 共享问题](#1. 集群 Session 共享问题)
-
- [1.1 什么是 session 共享问题?](#1.1 什么是 session 共享问题?)
- [1.2 Session 替代方案 ------ Redis](#1.2 Session 替代方案 —— Redis)
- [2. Redis 代替 Session 实现登录](#2. Redis 代替 Session 实现登录)
- 总结
完成任务
1. 集群 Session 共享问题
1.1 什么是 session 共享问题?
session共享问题就是:多态 Tomcat 不共享 Session 存储空间,当请求不同 Tomcat 服务器时,会导致数据丢失的问题。
为了并发,对 Tomcat 做水平扩展,形成多个负载均衡的集群 。**当请求进入 Nginx,会进行负载均衡,在多台 Tomcat 之间做轮询。**每个 tomcat 有自己的 session 空间。
当第一次请求负载到第一台 tomcat 时,会将数据存储在该 tomcat 的 session 中,比如说验证码。
当第二次请求负载到另一台 tomcat 时,session 中没有存储验证码数据,此时,就会出现这种情况:明明验证码是正确的,但一直报错 "验证码不正确"。

1.2 Session 替代方案 ------ Redis
寻找能够替代 Session 的解决方案,应满足:
- 数据共享:Session 的问题就是数据共享问题
- 内存存储:Session 是基于内存存储的
- key、value结构:Session 中保存的数据是 key-value 结构的
------> Redis
2. Redis 代替 Session 实现登录
- 将生成的验证码保存到 Redis
- 登录注册时根据 key 获取 Redis 中的验证码,验证成功,将 token 作为 key ,返回用户数据
返回用户数据时,使用 token,而不使用每个用户的手机号 ,原因在于:要将 key 返回给前端,如果将手机作为 key 返回给前端,有信息泄露的风险。 - 携带 token 校验登录状态,根据能否从 Redis 中获取用户,判断是否登录。
发送验证码,利用 StringRedisTemplate 将验证码存入 Redis:
java
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 发送手机验证码
*/
@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.debug("发送短信验证码成功,验证码:{}", code);
// 6. 返回成功信息
return Result.ok();
}
登录:
(1)从 Redis 中获取验证码,进行校验
(2)将查询到的用户信息保存到 Redis,以 token 作为 key,并设置有效期
(3)将 token 返回给前端
java
/**
* 登录
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1. 校验手机号
if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
// 不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 2. 从Redis中获取验证码, 并校验验证码
String checkCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+loginForm.getPhone());
if (checkCode == null || !checkCode.equals(loginForm.getCode())) {
// 3. 不一致,报错
return Result.fail("验证码错误!");
}
// 4. 一致,根据手机号查询用户
User user = query().eq("phone", loginForm.getPhone()).one();
// 5. 判断用户是否存在
if (user == null) {
// 6. 不存在,创建新用户并保存
user = createUserWithPhone(loginForm.getPhone());
}
// 7. 保存用户信息到 Redis
// 7.1 随机生成 token,作为登录令牌
String token = UUID.randomUUID().toString(true); // UUID 使用的是 hutool 的实现
// 7.2 将 UserDTO 对象转为 HashMap 存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3 将 token 存入 Redis
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4 设置 token 有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8. 返回token
return Result.ok(token);
}
在登录拦截器中,校验登录状态,前端在请求头中携带 token:
java
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求头中的 token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 不存在,拦截,返回 401 状态码
response.setStatus(401);
return false;
}
// 2. 基于 token 获取 redis 中的用户信息
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3. 判断用户是否存在
if (userMap.isEmpty()) {
// 4. 不存在,拦截,返回 401 状态码
response.setStatus(401);
return false;
}
// 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;
}
总结
了解 Session 存在的共享问题,Redis 的出现解决这种问题。利用 Redis 完成登录功能。