【Redis应用】基于Redis实现短信登录

本章目的

  • **掌握session实现登录的流程**

  • **认识使用session实现登录的缺点**

  • **掌握引入redis实现登录的流程**


前置知识(基于session实现短信登录)

在学习redis实现登录功能前,我先学习一下基于session实现的登录功能,然后再来思考一下基于session来实现的登录功能会有什么缺点,为什么要使用redis来实现我们这个短信登录呢?

在解决整个问题之前我先学习了session是如何实现的短信登录。 那么我们根据上面的流程去实现一下这个后端的登录功能

controller层

  • 发送验证码
less 复制代码
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
    // 发送短信验证码并保存验证码
    return userInfoService.sendCode(phone,session);
}
  • 登录(进行验证码的校验)
less 复制代码
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    // 实现登录功能
    return userService.login(loginForm,session);
}
  • 校验
java 复制代码
@GetMapping("/me")
public Result me(){
    // 获取当前登录的用户并返回
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

Service层

  • 发送验证码
arduino 复制代码
Result sendCode(String phone, HttpSession session);

impl层

typescript 复制代码
@Override
public Result sendCode(String phone, HttpSession session) {
    // 判断手机号码是否正确
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果手机号不合法
        return Result.fail(SystemConstants.PHONE_INVALID);
    }
    // 如果手机号码正确,生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 将验证码保存至session
    session.setAttribute("code",code);
    // 发送验证码(短信的发送需要调用第三方平台,这里就不写了)
    log.debug("发送短信验证码成功,验证码:{}",code);
    return Result.ok();
}
  • 登录(进行验证码的校验)
scss 复制代码
Result login(LoginFormDTO loginForm, HttpSession session);

impl层

scss 复制代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    /**
     * 短信校验登录注册
     */
    // 校验手机号
    if (loginForm.getPhone() == null || RegexUtils.isPhoneInvalid(loginForm.getPhone())){
        return Result.fail(SystemConstants.PHONE_INVALID);
    }
    // 判断验证码是否正确
    Object code = session.getAttribute(SystemConstants.LOGIN_CODE);
    if (loginForm.getCode() == null || !code.toString().equals(loginForm.getCode())) {
        return Result.fail(SystemConstants.LOGIN_CODE_WRONG);
    }
    // 通过手机号获取用户信息
    User userInfo = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
    // 判断是否存在该用户
    if(userInfo == null){
        // 创建新用户,并将用户信息存入数据库
        userInfo = createUserWithPhone(loginForm.getPhone());
    }
    UserDTO userDTO = new UserDTO();
    BeanUtils.copyProperties(userInfo,userDTO);
    // 保存用户到session
    session.setAttribute(SystemConstants.USER_INFO,userDTO);
    return Result.ok();
}

private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    save(user);
    return user;
}

注意:这里说一下为什么使用UserDTO,我们的session是存放在浏览器的(基于内存),那么我们从数据库里查询出来的是完整的用户信息,如果我们将其存放在session的缺点:

  • 不安全,敏感信息直接显示在session(浏览器)
  • 占用内存 所以我们会自定义一个DTO,去隐藏我们的敏感信息。

拦截器实现校验功能

我们的校验是针对整个项目而言的,那我们的项目中存在很多的controller,这样我们每次发送请求的时候就都需要去重复写一个校验请求,如何更加简洁呢?其实只需要引入一个拦截器,用户发送请求,先经过拦截器进行判断,再将请求发送给controller。实现如下:

  1. 拦截器的实现流程
java 复制代码
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 userInfo = session.getAttribute(SystemConstants.USER_INFO);
        // 3.判断用户信息是否存在
        if (Objects.isNull(userInfo)) {
            // 3.2 不存在放行
            response.setStatus(401);
            return false;
        }
        // 3.1 存在保存到threadLocal
        BeanUtils.copyProperties(userInfo,userInfo);
        UserHolder.saveUser((UserDTO) userInfo);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
  1. 配置拦截器
typescript 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "voucher/**"
                );
    }
}

session的缺点(集群情况)

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同的tomcat服务时导致数据丢失的问题 解决的思想:让session可以共享(数据拷贝) --》 问题:时间延迟,数据丢失、 session的替代方案:

  • 数据共享
  • 内存存储
  • key,value结构 redis中满足上面的解决思想要求

基于Redis实现共享session登录

基于redis实现登录业务更改

scss 复制代码
@Autowired
private RedisTemplate redisTemplate;

@Override
public Result sendCode(String phone, HttpSession session) {
    // 判断手机号码是否正确
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果手机号不合法
        return Result.fail(SystemConstants.PHONE_INVALID);
    }
    // 如果手机号码正确,生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 将验证码保存至session
    // session.setAttribute(SystemConstants.LOGIN_CODE,code);

    // 将验证码保存至redis
    redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY+phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
    // 发送验证码
    log.debug("发送短信验证码成功,验证码:{}",code);
    return Result.ok();
}

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    /**
     * 短信校验登录注册
     */
    // 校验手机号
    if (loginForm.getPhone() == null || RegexUtils.isPhoneInvalid(loginForm.getPhone())){
        return Result.fail(SystemConstants.PHONE_INVALID);
    }
    // 判断验证码是否正确
    // Object code = session.getAttribute(SystemConstants.LOGIN_CODE);

    // 获取redis中的验证码进行校验
    Object code = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY+loginForm.getPhone());

    if (loginForm.getCode() == null || !code.toString().equals(loginForm.getCode())) {
        return Result.fail(SystemConstants.LOGIN_CODE_WRONG);
    }
    // 通过手机号获取用户信息
    User userInfo = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
    // 判断是否存在该用户
    if(userInfo == null){
        // 创建新用户,并将用户信息存入数据库
        userInfo = createUserWithPhone(loginForm.getPhone());
    }

    UserDTO userDTO = new UserDTO();
    BeanUtils.copyProperties(userInfo,userDTO);

    // 保存用户到session
    // session.setAttribute(SystemConstants.USER_INFO,userDTO);

    // 7.保存用户信息到redis
    // 7.1 生成一个随机token,作为登录令牌
    String token = UUID.randomUUID().toString();

    // 7.2 将userInfo对象转为hash存储
    Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
            CopyOptions.create().setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));
    // 7.3 保存数据到redis
    String key = RedisConstants.LOGIN_USER_KEY + token;
    stringRedisTemplate.opsForHash().putAll(key,map);
    stringRedisTemplate.expire(token,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);
    // 返回token
    return Result.ok(token);
}

private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    save(user);
    return user;
}

拦截器:

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {
    
    private RedisTemplate redisTemplate;

    public LoginInterceptor(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            response.setStatus(401);
            return false;
        }
        // 2.获取redis中的用户信息
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object,Object> userMap = redisTemplate.opsForHash().entries(key);
        // 3.判断用户信息是否存在
        if (userMap.isEmpty()) {
            // 3.2 不存在放行
            response.setStatus(401);
            return false;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 3.1 存在保存到threadLocal
        UserHolder.saveUser(userDTO);
        // 刷新redis过期时间
        redisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

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

优化拦截器

上面就成功实现了我们的一个基于redis实现登录功能,但是上面有一个bug,我们知道,我们的拦截器是针对我们的一个需要登录的路径做拦截,顾名思义就是针对需要登录的路径才做redis对token过期时间的刷新,那么这就会造成用户如果长期访问的是无需登录的页面,也会造成token过期 ,这是不友好的。怎么办呢?我们可以多加一个拦截器,进行拦截器的分工

RefreshTokenInterceptor拦截器

用于拦截一切请求

java 复制代码
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate redisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @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.获取redis中的用户信息
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object,Object> userMap = redisTemplate.opsForHash().entries(key);
        // 3.判断用户信息是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 3.1 存在保存到threadLocal
        UserHolder.saveUser(userDTO);
        // 刷新redis过期时间
        redisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

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

LoginInterceptor拦截器

用于拦截需要登录的路径

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断是否需要做拦截
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            response.setStatus(401);
            return false;
        }

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
typescript 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "voucher/**"
                ).order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate))
                .addPathPatterns("/**").order(0);
    }
}

这里有多个拦截器,那么我们可以使用order来指定拦截器的一个执行顺序,order值越小,优先级越高

好啦这样我们的一个基于redis实现的短信登录就实现啦!!!

相关推荐
IT大白鼠2 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz2 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙2 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除2 天前
redis--018
redis
两锭子2 天前
Redis基础
redis
烟沙九洲2 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索2 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞3 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路3 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog3 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis