基于Redis实现共享session登录
在基于Redis实现共享session登录之前,先要明白什么是session共享问题?
session共享问题: 如何解决session共享问题
实现流程
生成验证码
- 获取用户手机号,根据正则表达式校验手机号
- 生成验证码
- 将验证码存入到redis中,存入redis时需要设置过期时间。
- 发送验证码,发送手机验证码需要调用第三方服务,此处仅控制台输出
public Result sendCode(String phone) {
//1.验证手机号是否符合规范
if (RegexUtils.isPhone(phone)){
return Result.fail("手机号码格式不正确");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.将验证码存入到redis中
stringRedisTemplate.opsForValue().set(RedisData.LOGIN_CODE_KEY+phone,code,RedisData.LOGIN_CODE_TTL,TimeUnit.MINUTES);
//4.发送验证码
log.info("发送手机验证码{}",code);
//5.返回
return Result.ok();
}
用户登录
- 获取信息,包括用户输入的手机号,用户输入的验证码,redis缓存的验证码
- 校验手机号码是否符合规范
- 校验验证码是否正确
- 根据用户手机号码查询用户,用户不存在就添加
- 生成token,生成token的方式很多,但是需要保证唯一性
- 数据转换,由于redis选择hash存储对象,此处使用工具类转换数据格式
- 存入redis,设置过期时间
- 将token返回
public Result login(LoginFormDTO loginForm) {
//1.获取信息
String phone = loginForm.getPhone();//获取手机号码
String code = loginForm.getCode();//用户输入的验证码
String catchCode = stringRedisTemplate.opsForValue().get(RedisData.LOGIN_CODE_KEY + phone);
//2.校验手机号码是否符合规范
if (RegexUtils.isPhone(phone)){
return Result.fail("手机号码格式错误");
}
//3.校验验证码是否正确
if (catchCode==null||!catchCode.equals(code)){
return Result.fail("验证码错误");
}
//4.根据手机号码查询用户
User user = query().eq("phone", phone).one();
//5.用户不存在就添加
if (user==null){
user=createUserByPhone(phone);
}
//6.生成token
String token = UUID.randomUUID().toString(false);
//7.将User--->UserDTO--->Map
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((name, value) -> value.toString()));
//8.将用户信息存入到redis中
String tokenKey=RedisData.LOGIN_USER_KEY+token;
stringRedisTemplate.opsForHash().putAll(tokenKey,map);
//9.设置过期时间
stringRedisTemplate.expire(tokenKey,RedisData.LOGIN_USER_TTL,TimeUnit.MINUTES);
//10.返回token
return Result.ok(token);
}
注意事项
- key的设计大致需要满足:
- key要具有唯一性,如果key重复可能造成数据覆盖
- key要方便携带,在用户登录成功后将token返回给前端后,前端会在每次的请求头中添加信息
- key具有安全性,返回的key会被保存在浏览器中,那么key就不能包含用户的隐私信息
- 保存在redis中的数据需要设置合理的过期时间,否则这些数据将会永久保存,随着时间的积累占用大量内存空间。
- redis存储数据数据结构的选择,选择合适的数据结构减少内存的浪费并且方便存取和操作
如何刷新用户信息存储时间
目前用户登录成功后会将用户信息保存到redis中,如果其他业务流程不做存储时间刷新,那么当到达指定的存储时间后,存储的用户信息就会过期,那么身份校验就不会通过。那么何时刷新用户信息存储时间?何处刷新比较合适?
其实在用户登录成功后,用户访问其他业务基本都会做用户身份校验。身份校验这个过程可以通过拦截器完成而刷新用户信息存储时间也可以在此时同时完成。
刷新用户信息存储时间
用户的请求对应一个线程,将获取到的用户信息保存到ThreadLocal中可以在此次请求中使用,又可以做到线程隔离。
RefreshTokenInterceptor的功能只是做存储时间刷新以及将用户信息保存到ThreadLocal中,并不做拦截。
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)){
return true;
}
//2.获取用户信息
String tokenKey=RedisData.LOGIN_USER_KEY+token;
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(tokenKey);
if (map.isEmpty()){
return true;
}
//3.将用户信息转换为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
//4.将用户信息保存到ThreadLocal中
UserThreadLocal.saveUser(userDTO);
//5.更新缓存时间
stringRedisTemplate.expire(tokenKey,RedisData.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户,防止内存泄漏
UserThreadLocal.removeUser();
}
}
用户身份校验
由于RefreshTokenInterceptor已经实现了存储时间的刷新,LoginInterceptor只需要做身份验证即可,而ThreadLocal存储对象的有无就代表用户是否登录。
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (UserThreadLocal.getUser()==null){
response.setStatus(401);
return false;
}
return true;
}
}
注册拦截器
注册拦截器时需要注意RefreshTokenInterceptor执行必须在LoginInterceptor执行之前。可以通过设置order的值来实现,至于拦截器器需要拦截那些请求根据实际情况设定。
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login"
).order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
}
}
最后关于拦截器的执行顺序可以参考此链接:
SpringMVC拦截器的执行顺序