2

【Redis场景1】用户登录注册 - xbhog

 1 year ago
source link: https://www.cnblogs.com/xbhog/p/16973027.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

细节回顾:

关于cookiesession不熟悉的朋友;

建议阅读该博客https://www.cnblogs.com/ityouknow/p/10856177.html

执行流程:

img

在单体模式下,一般采用这种模式来存储,传递、认证用户登录、注册等信息;

如果浏览器禁用Cookies,****如何保障整个机制的正常运转。

  1. url拼接或者POST请求:每个请求都携带SessionID
  2. Token机制:在用户登录或者注册的时候,与用户信息绑定一个随机字符串,用于用户状态管理

在分布式下的Session问题:

img

为了支撑更大的流量,后台往往需要在多台服务器中部署,那如果用户在 A 服务器登录了,第二次请求跑到服务 B 就会出现登录失效问题如何解决?

  • Nginx ip_hash 策略,服务端使用 Nginx 代理,每个请求按访问 IP hash分配,这样来自同一 IP 固定访问一个后台服务器,避免了在服务器 A 创建 Session,第二次分发到服务器 B 的现象。
  • Session 复制,任何一个服务器上的 Session 发生改变(增删改),该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点。
  • 共享 Session,服务端无状态话,将用户的 Session 等信息使用缓存中间件来统一管理,保障分发到每一个服务器的响应结果都一致。

第一种策略(Nginx ip_hash 策略)可以看该博客:https://www.cnblogs.com/xbhog/p/16929786.html

我们主要实现第三种:通过缓存中间件来统一管理。

解决痛点:

在原来场景中存在的Session不互通的问题(Session数据拷贝),该解决方式有以下问题:

  1. 每台服务器中都有完整的一份session数据,服务器压力过大。
  2. session拷贝数据时,可能会出现延迟

所以我们需要采用的中间件需要有以下特征(Redis):

  1. 基于内存读取
  2. 满足数据存储格式(KEY:VALUE)

实现场景:

该场景实现流程:以下分析结合部分代码(聚焦于redis的实现);

完整后端代码可在Github中获取:https://github.com/xbhog/hm-dianping

img

开发流程:

【获取验证码流程】前端根据手机号提交获取验证码请求:触发sendCode方法:

  1. 校验手机号合法性
  2. 生成验证码
  3. 保存验证码到redis
  4. 发送验证码(模拟实现,未调用第三方平台)

在第3步的实现如下:

stringRedisTemplate.opsForValue().set(PHONE_CODE_KEY+phone,code,2L,TimeUnit.MINUTES);

使用的stringRedisTemplate继承于RedisTemplate,局限:key和value必须是String类型:

public class StringRedisTemplate extends RedisTemplate<String, String> {
......
}

设置验证码Key值:phone:code:,code为随机6位字符串。

设置key的过期时间。

【登录功能】前端根据手机号和验证码发送登录功能(如上图):触发login方法:

  1. 校验手机号合法性

  2. 校验验证码合法性:从redis中获取

  3. 数据库通过手机号查询用户

    1. 不存在:创建用户
    2. 存在:执行后续逻辑(4)
  4. UUID生成随机TOKEN

  5. 将用户信息存入Redis中,设置过期时间

  6. 返回前端Token

第2步的实现如下:

String redisCode = stringRedisTemplate.opsForValue().get(PHONE_CODE_KEY + loginForm.getPhone());
if(StringUtils.isBlank(loginForm.getCode()) || !loginForm.getCode().equals(redisCode)){
    return Result.fail("验证码错误");
}

第4、5步的实现如下:

//随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
//保存到redis中
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userBeanToMap);
//设置过期时间
stringRedisTemplate.expire(tokenKey,30L, TimeUnit.MINUTES);

设置用户KEY"login:token:",这里redis的使用的数据结构是Map,方便对单一字段操作。

使用了hashmap结构,需要单独对tokenKey设置过期时间(30m);

场景问题:

Redis Key续期问题:

在设置token的时候,在redis给的过期时间是30分钟,这里就有个问题,用户在30分钟内,结束请求,那没有问题,但只要用户的在线时间超过30分钟,redis删除token,直接给用户强制下线了;这个实在是不符合实际场景。

解决方式:

在请求的过程总给加入一层拦截器,用来刷新Token的存活时间。

子问题:

对于拦截器,我们不能拦截所有的路径,比如获取验证码请求,用户登录,首页等;

    1. 用户在所拦截的范围内:则可执行刷新Token操作
    2. 用户不在拦截的范围内:无法执行刷新Token操作

子问题解决:

增加两层拦截器,第一层拦截全部请求路径,第二层拦截器基于第一层信息,对未登录的请求进行拦截。

需要设置两层拦截器的优先级,

order():指定要使用执行器顺序。默认值为0(最高)

@Configuration
public class MybatisConfig implements WebMvcConfigurer {

    @Resource
    private 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(1);
        registry.addInterceptor(new RefreshTokeInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}
img

【拦截器1实现】所有路径拦截,刷新登录Token令牌存活时间。

  1. 获取token

  2. 查询redis用户

    1. 存在,不拦截(执行3)
    2. 不存在,拦截
  3. 用户信息保存到threadLocal

  4. 刷新Token有效期

相关代码:

/**
 * @author xbhog
 * @describe: 拦截器实现:校验用户登录状态
 * @date 2022/12/7
 */
public class RefreshTokeInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("authorization");
        //如果页面没有登录,则没有token,直接放行给下一个拦截器
        if(StringUtils.isEmpty(token)){
            return true;
        }
        String tokenKey = LOGIN_USER_KEY + token;
        Map<Object, Object> userRedis = stringRedisTemplate.opsForHash().entries(tokenKey);
        if(userRedis.isEmpty()){
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userRedis, new UserDTO(), false);
        //用户存在,放到threadLocal
        UserHolder.saveUser(userDTO);
        //登录续期
        stringRedisTemplate.expire(tokenKey,30L, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

【拦截器2实现】需要登录的路径拦截;

实现代码:

/**
 * @author xbhog
 * @describe: 拦截器实现:校验用户登录状态
 * @date 2022/12/7
 */
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }
}

输入及参考

cookie和session

斐波那契散列和hashMap实践


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK