Redis实现短信登录

前言:如果我们通过Session来实现短信登录的话,因为Session在集群中无法做到数据共享,如果部署了多台Tomcat服务器,会导致信息之间不互通,可能会造成刚登陆完,访问其他页面的时候,结果还需要继续去登录操作,这种情况对用户的体验感是非常不好的。如下图:

在这里插入图片描述

虽然可以通过在多头Tomcat中复制Session的数据,但是这样会造成资源的浪费,每一台Tomcat服务器中,都存在相同的session数据。

所有我们可以基于Redis代替Session实现短信验证功能

1.设计Key

首先我们要思考一下利用redis来存储数据,那么到底使用哪种结构呢?由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图,如果使用String,同学们注意他的value,用多占用一点空间,如果使用哈希,则他的value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以啦。如下图:

在这里插入图片描述

下面我们采用第一种方式:使用String来存入数据

Redis的String结构,就是一个简单的key,value键值对的方式,但是关于key的处理,session他是每个用户都有自己的session,但是redis的key是共享的,就不能使用code了,所以我们可以使用一个动态的值作为其Key.

  • key要具有唯一性
  • key要方便携带

因为是短信验证,所以我们可以使用用户的手机号作为Key,但是用户的手机号是比较敏感的一个信息,因此我们使用UUID等构造一个随机字符串,然后拼接上手机号,生成一个token作为redis的Key.

2.基本流程

文字描述

1)获取到验证码,保存到Redis中

2)前端传递手机号和验证码 进行登录校验,因为上一步我们保存到Redis的验证码的Key为Phone,所以这里我们可以通过传递的手机号来获取Redis中的验证码,然后与用户的验证码进行比较

3)根据手机号查找数据库中有无该用户

4)存在用户,则随机生成一个token作为Key,将用户信息保存到Redis中(注意设置有效期 防止内存爆掉)因为我们采用的String序列化Value,所以需要使用fastjson手动序列化 (这里主要是为了保持用户的登录状态)将随机生成的token返回到前端。

5)注册新用户 执行如上操作

6)前端每次发送请求,携带token,然后我们实现拦截器进行拦截

7)拦截器中主要就是获取到token, 然后去缓存中查找用户信息 若能查到则说明用户为登录状态。

下面是具体的流程图

在这里插入图片描述

3.代码实现

业务层login方法

    @Override
    public Result login(LoginFormDTO loginForm) {
        User user = new User();
        //1.校验手机号
        String phone = loginForm.getPhone();
        //2.校验验证码
        String code = loginForm.getCode();
        //获取验证码
        String userCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        if(!code.equals(userCode)){
            //验证码错误
            return Result.fail("验证码错误!");
        }
        //3.根据手机号查询用户
        List<User> userList = userMapper.getByPhone(phone);
        if(userList == null || userList.isEmpty()){
            //4.不存在则创建新用户 保存到数据库
            user.setCreateTime(LocalDateTime.now());
            user.setPhone(phone);
            user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(6));
            userMapper.insert(user);
        }else{
            user = userList.get(0);
        }
        //随机生成一个token
        String token = UUID.randomUUID().toString();
        //把userDTO保存到Redis中 保持登录状态
        UserDTO userDTO = new UserDTO();
        BeanUtil.copyProperties(user,userDTO);
        //序列化:
        String jsonString = JSON.toJSONString(userDTO);
        //保存用户信息到redis
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForValue().set(tokenKey,jsonString);
        //设置token有效期
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL,TimeUnit.SECONDS);
        //返回token到客户端如何
        return Result.ok(token);

    }

如果我们只设计一个拦截器,假如说用户登录后,一直访问未进行token校验的请求,例如热搜等内容,这会导致我们的用户登录状态挂掉,这对用户的体验感非常不好。因此计划设计两个拦截器,一个拦截器用来拦截所有请求,同时刷新令牌,刷新用户登录状态,一个拦截器只需要用来判断当前线程是否存放了用户信息即可。下面为代码:

RefreshTokenInterceptor.java

package com.hmdp.utils;

import com.alibaba.fastjson.JSON;
import com.hmdp.dto.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

@Slf4j
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;
    //这里如果想使用到该bean 需要使用构造器 然后在WebMvcConfig里面 传递
    //因为WebMvcConfig为配置类,可以注入Bean.
    //这里不加@Component注解 是因为该拦截器只做这一个功能 就是拦截请求 不会在其他地方使用 所有为了避免资源浪费 占用IOC容器 所有通过这种方法解决
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("authorization");
        if(token == null){
            //这里直接放行 是为了防止Redis报空指针异常
            return true;
        }
        //基于token从redis中获取用户
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        String str = stringRedisTemplate.opsForValue().get(tokenKey);
        //反序列化
        UserDTO userDTO = JSON.parseObject(str, UserDTO.class);
        //刷新有效期
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS );
        //保存到当前线程变量中
        UserHolder.saveUser(userDTO);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清理当前线程 缓存
        UserHolder.removeUser();
    }
}

LoginInterceptor.java

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import com.hmdp.dto.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

@Slf4j
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;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清理当前线程 缓存
        UserHolder.removeUser();
    }
}

4.总结

这样我们就利用Redis实现了短信验证功能,相对于Session来说解决了Session在集群中数据不共享的问题,同时可以维持用户的登录状态。

  • 优点
    • 解决集群共享问题: 完美解决了传统 Session 在多台服务器负载均衡时无法共享的问题。
    • 可控性强: 服务端可以主动管理登录状态,例如强制用户下线、刷新 Token 有效期、限制登录设备数等。
    • 安全性高: 敏感的用户信息存储在服务端,Token 本身不携带任何信息,即使被截获,只要服务端的 Token 未过期且未被主动注销,风险相对可控(但需防范重放攻击)。
    • 实现相对简单: 逻辑清晰,与传统的 Session 模式思路一致,只是存储介质变了。
  • 缺点
    • 依赖 Redis: 系统架构增加了对 Redis 的强依赖,Redis 的性能和可用性直接影响登录功能。
    • 网络开销: 每次校验都需要查询 Redis,相比无状态方案增加了网络 I/O 开销。
    • 扩展性瓶颈: 虽然 Redis 性能极高,但在极端高并发场景下,它可能成为性能瓶颈。
  • 适用场景: 对登录状态管理要求高、需要强控制(如金融、后台管理系统)、系统规模适中或已有稳定 Redis 集群的项目。

5.扩展

JWT 令牌 校验(无状态)

核心思想:用户登录成功后,服务端生成一个包含用户身份信息(Payload)并经过签名(Signature)的 Token,将其完全交给客户端(如存储在 localStorage 或 Cookie 中)。后续请求,客户端携带此 Token,服务端只需验证其签名和有效期即可,无需查询数据库或缓存。服务端不存储任何与该 Token 相关的会话状态。Token 本身包含了验证用户身份所需的全部信息(经过加密签名保证不被篡改)。这种方法大家肯定比较了解的,它同样可以实现不同域名或不同微服务之间传递和验证。

  • 优点
    • 真正的无状态: 服务端完全不存储会话,极大地简化了服务端逻辑,非常适合微服务架构和水平扩展。
    • 减少数据库/缓存压力: 校验 Token 无需访问 Redis 或数据库,减轻了后端存储压力。
    • 跨域/跨服务友好: Token 可以轻松在不同域名或不同微服务之间传递和验证。
  • 缺点
    • Token 一旦签发,无法主动失效: 这是 JWT 最大的痛点。在 Token 有效期内,即使用户修改了密码或管理员强制其下线,也无法使已签发的 Token 失效,只能等它自然过期。这带来了安全风险。
    • Token 体积较大: 相比 Session ID 或简单的 UUID Token,JWT 因为包含了用户信息,体积更大,会增加网络传输开销。
    • 敏感信息暴露风险: 虽然 Payload 可以加密,但通常只做 Base64 编码,理论上用户可以解码看到里面的信息(如用户ID、角色),因此不应在 Payload 中存放密码等极度敏感信息。
    • 续签逻辑复杂: 为了平衡安全性和用户体验,常采用“短期 Token + 长期 Refresh Token”的方案,但这增加了实现的复杂度。

适用场景: 微服务架构、移动端 App、需要良好跨域能力的前后端分离项目、对服务端无状态要求极高的场景。

Redis + JWT 令牌技术 (混合方案)

核心思想: 结合前两者的优点,规避各自的缺点。依然使用 JWT 作为客户端持有的令牌,但将 JWT 的唯一标识(如 jti claim)或整个 Token 本身存储在 Redis 中,并设置与 JWT 相同或略长的有效期。校验时,除了验证 JWT 的签名和有效期,还需检查该 Token 是否存在于 Redis 中。 这是一种“伪无状态”或“有状态的无状态”方案。它利用了 JWT 的格式和客户端存储的优势,但通过 Redis 引入了服务端状态管理。

  • 优点
    • 保留 JWT 优势: 客户端持有 Token,服务端校验逻辑依然相对简单。
    • 解决 JWT 无法主动失效的问题: 通过在 Redis 中删除或标记某个 Token(或其 ID),可以立即让该 Token 失效,实现强制下线、单点登录互踢等功能。
    • 灵活性高: 可以在 Redis 中存储额外的会话信息,如登录设备、IP 地址等,用于安全审计。
  • 缺点
    • 失去“无状态”核心优势: 依然需要依赖 Redis,每次校验都需要一次 Redis 查询,引入了网络开销和对 Redis 的依赖。
    • 架构复杂度增加: 相比纯 JWT,需要维护 Redis 中的 Token 状态。
  • 适用场景: 这是目前非常主流且推荐的方案。它在保持 JWT 便利性的同时,通过 Redis 解决了其致命的安全缺陷,适用于绝大多数对安全性和灵活性都有要求的现代 Web 应用和 App。

总结:

流程:用户通过验证码登录成功后,将JWT令牌保存在Redis中,然后下放JWT令牌,前端每次发送请求都携带该Token,拦截器拦截到请求后,对JWT令牌进行解密,同时会检查Redis中是否存在该Token

这种方法是比较推荐的,不仅解决了JWT无法主动失效的问题,同时解决了Redis实现需要将token保存在客户端本地等缺点。

您的一个小小点赞,对我便是莫大的鼓励。感谢支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值