Redis进阶学习02---Redis替代Session和Redis缓存
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis进阶学习02---Redis替代Session和Redis缓存相关的知识,希望对你有一定的参考价值。
Redis进阶学习02---Redis替代Session和Redis缓存
参考b站虎哥redis视频
本系列项目源码将会保存在gitee上面,仓库链接如下:
https://gitee.com/DaHuYuXiXi/redis-combat-project
基于Session登录流程
我们先来看一下基于Session实现登录的模板流程是什么样子的:
- 发送短信验证码
核心逻辑:
public Result sendCode(String phone, HttpSession session)
//1.校验手机号
if(RegexUtils.isPhoneInvalid(phone))
//2.如果不符合,返回错误信息
return Result.fail(getErrMsg("01",UserServiceImpl.class));
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到session
session.setAttribute("code",code);
//5.发送验证码
log.debug("发送短信验证码成功,code ",code);
//6.返回ok
return Result.ok();
- 短信验证码的登录和注册
@Override
public Result login(LoginFormDTO loginForm, HttpSession session)
//1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone))
//2.如果不符合,返回错误信息
return Result.fail(getErrMsg("01", UserServiceImpl.class));
//3.校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code))
//4.不一致报错
return Result.fail(getErrMsg("02", UserServiceImpl.class));
//5.一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
//6.判断用户是否存在
if (user == null)
//7.不存在,创建新用户并保存
user = createUserWithPhone(phone);
//8.保存用户信息到session中
session.setAttribute("user", user);
return Result.ok();
private User createUserWithPhone(String phone)
//1.创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
//2.保存用户
save(user);
return user;
- 校验登录状态
我们需要把验证功能放到拦截器中实现:
public class LoginInterceptor implements HandlerInterceptor
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
//1.获取session
HttpSession session = request.getSession();
//2.获取session中的用户
Object user = session.getAttribute("user");
//3.判断用户是否存在
if(user==null)
//4.不存在,拦截,返回401状态码
response.setStatus(401);
return false;
//5.存在,保存用户信息到ThreadLocal
UserHolder.saveUser(getUserDTO(user));
//6.放行
return true;
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception
//清空ThreadLocal
UserHolder.removeUser();
private UserDTO getUserDTO(Object user)
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
return userDTO;
保存用户信息到ThreadLocal可以确保当前请求从开始到结束这段时间,我们可以轻松从ThreadLocal中获取当前用户信息,而不需要每次用到的时候,还去查询一遍
本节项目完整代码,参考2.0版本
集群session共享问题
既然多台tomcat之间的session存在隔离问题,那么我们是否可以将session中存储的内容移动到redis中进行存放,即用redis代替session
基于Redis实现session共享
这里说一下: 登录成功后,会将用户保存到redis中,这和上面讲用户保存到session中的思想是一致的,都是一种缓存思想,防止每次都需要拦截器拦截请求时,都需要去数据库查找,而是直接通过token去redis中获取即可
注意,这里的token不是jwt的token,这里的token只是随机生成的一段字符串,我们无法通过解析这个字符串拿到用户信息,而是只能通过这个token作为key,去redis中获取到对应用户的信息。
个人想法:即便是jwt的token,因为一般不会在里面token中保存完整的用户信息,并且每次请求打进拦截器的时候,还是需要去解析token,并去数据库查一下,防止token伪造,但是这样太浪费性能了,可以考虑在登录成功后,将用户信息存入redis,并且规定过期时间,然后拦截器每次根据token去redis获取用户完整信息,如果成功获取,那么刷新token过期时间,否则,从数据库重新获取,然后再放入缓存中。
我们这里选用HASH来存储User对象的信息:
UserServiceImpl代码:
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService
private final StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session)
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone))
//2.如果不符合,返回错误信息
return Result.fail(getErrMsg("01", UserServiceImpl.class));
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,2, TimeUnit.MINUTES);
//5.发送验证码
log.debug("phone code ", code);
//6.返回ok
return Result.ok();
@Override
public Result login(LoginFormDTO loginForm, HttpSession session)
//1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone))
//2.如果不符合,返回错误信息
return Result.fail(getErrMsg("01", UserServiceImpl.class));
//3.从redis中获取验证码然后进行校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code))
//4.不一致报错
return Result.fail(getErrMsg("02", UserServiceImpl.class));
//5.一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
//6.判断用户是否存在
if (user == null)
//7.不存在,创建新用户并保存
user = createUserWithPhone(phone);
//7.保存用户信息到redis
//7.1 随机生成token,作为登录令牌
String token = generateToken();
//7.2 将User对象转换为Hash对象
Map map = beanToMap(BeanUtil.copyProperties(user, UserDTO.class));
String key=LOGIN_USER_KEY+token;
stringRedisTemplate.opsForHash().putAll(key,map);
//设置有效期
stringRedisTemplate.expire(key,LOGIN_USER_TTL,TimeUnit.MINUTES);
//7.3 存储
return Result.ok();
private Map<String, Object> beanToMap(UserDTO user)
return BeanUtil.beanToMap(user, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true)
//解决long转String报错的问题
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
private String generateToken()
return UUID.randomUUID().toString(true);
private User createUserWithPhone(String phone)
//1.创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
//2.保存用户
save(user);
return user;
LoginInterceptor 代码:
public class LoginInterceptor implements HandlerInterceptor
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;
token = LOGIN_USER_KEY + token;
//2.基于Token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(token);
//3.判断用户是否存在
if (userMap.isEmpty())
//不存在,拦截,返回401状态码
response.setStatus(401);
return false;
//5.map转换为userDto
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.用户信息保存到threadLocal
UserHolder.saveUser(userDTO);
//7.刷新token的有效期
stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception
UserHolder.removeUser();
解决状态登录刷新问题
上面的代码设计思路: 如果用户长时间都在请求不需要拦截的请求,那么token就不会被刷新,进而导致用户浏览浏览着,token就过期了
优化后:分离拦截器职责,用一个单独的拦截器拦截所有请求,每次都刷新token,另一个拦截器就负责需要登录的请求进行拦截即可
public class RefreshTokenInterceptor implements HandlerInterceptor
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(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;
token = LOGIN_USER_KEY + token;
//2.基于Token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(token);
//3.判断用户是否存在
if (userMap.isEmpty())
//不存在,拦截,返回401状态码
response.setStatus(401);
return false;
//5.map转换为userDto
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.用户信息保存到threadLocal
UserHolder.saveUser(userDTO);
//7.刷新token的有效期
stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception
UserHolder.removeUser();
public class LoginInterceptor implements HandlerInterceptor
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
//判断是否需要去拦截
if(UserHolder.getUser()==null)
response.setStatus(401);
return false;
return true;
RefreshTokenInterceptor 要先于LoginInterceptor 执行,否则LoginInterceptor 中无法中ThreadLocal中获取用户信息
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer
private final StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry)
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
)
//指定拦截器的执行顺序---数字越小,优先级越高
.order(2);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(1);
还有一点需要注意:如果用户信息被修改了,需要清空redis中的缓存信息,让用户重新进行登录
Redis02Redis基础:List相关操作