基于Redis实现登录功能思路详解(手机号+验证码)

 更新时间:2026年01月08日 11:06:28   作者:什么都不会的Tristan  
本文介绍了使用手机号和验证码登录的方式,验证码通过控制台输出,重点解释了UserServiceImpl实现类的结构,包括sendCode、login和createUserWithPhone方法,还介绍了拦截器框架,包括preHandle和拦截器链的配置,感兴趣的朋友跟随小编一起看看吧

本文使用的是 手机号+验证码 的登录方式,其中验证码是通过在控制台输出,并没有真的发送到手机上(太麻烦,主要目的还是学习使用Redis)

重点是看思路,而不是具体的代码实现

UserServiceImpl实现类

整体结构

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //...
    }
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //...
    }
    private User createUserWithPhone(String phone) {
        //...
    }
}

sendCode方法

这个是发送验证码的方法

public Result sendCode(String phone, HttpSession session) {
    // 1. 校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2. 如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3. 如果符合,生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 4. 保存验证码到redis
    stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
    // 5. 发送验证码
    log.debug("发送短信验证码成功,验证码:{}", code);
    // 6. 返回结果
    return Result.ok();
}

注:这里的RedisConstants是一个用来存放各种常量的类

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;
}

login方法

这里使用了MybatisPlus来操作数据库(User user = query().eq("phone", phone).one();),但是这个不是重点

public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1. 校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式错误!");
    }
    // 2. 从redis获取验证码并校验
    String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY +phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        // 3. 不一致,报错
        return Result.fail("验证码错误!");
    }
    // 4. 一致,根据手机号查询用户
    User user = query().eq("phone", phone).one();
    // 5. 判断用户是否存在
    if (user == null) {
        // 6. 不存在,创建新用户并保存
        user = createUserWithPhone(phone);
    }
    // 7. 保存用户信息到redis
    String token= UUID.randomUUID().toString(true);
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
            CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName, fieldValue)->fieldValue.toString()));
    stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
    stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
    return Result.ok(token);
}

createUserWithPhone方法

在login方法中调用了该方法

这里也使用了MybatisPlus来操作数据库(save(user);)

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

拦截器

整体框架

其实就是实现了HandlerInterceptor的两个方法

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //...
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

 UserHolder是ThreadLocal 持有类

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
    public static void saveUser(UserDTO user){
        tl.set(user);
    }
    public static UserDTO getUser(){
        return tl.get();
    }
    public static void removeUser(){
        tl.remove();
    }
}

preHandle方法

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.基于token获取redis中的用户
    String key = RedisConstants.LOGIN_USER_KEY + token;
    Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
    // 3.判断用户是否存在
    if (userMap.isEmpty()) {
        // 4.不存在,拦截
        response.setStatus(401);
        return false;
    }
    // 5.将查询到的Hash数据转换为UserDTO对象
    UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
    // 6.存在,保存用户信息到ThreadLocal
    UserHolder.saveUser(userDTO);
    // 7.刷新token有效期
    stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 8.放行
    return true;
}

注:authorization 是前端定义的用来传递token的key

配置类

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

整体思路

flowchart TD
subgraph A[发送验证码流程]
    A1["前端请求 发送验证码"] --> A2["校验手机号格式"]
    A2 -- 不合法 --> A3["返回错误  手机号格式错误"]
    A2 -- 合法 --> A4["生成6位验证码"]
    A4 --> A5["保存验证码到Redis"]
    A5 --> A6["返回成功"]
end
subgraph B[登录流程]
    B1["前端请求 登录"] --> B2["校验手机号格式"]
    B2 -- 不合法 --> B3["返回错误"]
    B2 -- 合法 --> B4["从Redis获取验证码"]
    B4 --> B5{"验证码是否正确"}
    B5 -- 否 --> B6["返回验证码错误"]
    B5 -- 是 --> B7["根据手机号查询用户"]
    B7 --> B8{"用户是否存在"}
    B8 -- 否 --> B9["创建新用户"]
    B8 -- 是 --> B10["使用已有用户"]
    B9 --> B11["生成Token"]
    B10 --> B11
    B11 --> B12["用户信息写入Redis"]
    B12 --> B13["返回Token"]
end
subgraph C[请求拦截流程]
    C1["请求到达拦截器"] --> C2["从请求头获取Token"]
    C2 --> C3{"Token是否存在"}
    C3 -- 否 --> C4["返回401"]
    C3 -- 是 --> C5["从Redis获取用户信息"]
    C5 --> C6{"用户是否存在"}
    C6 -- 否 --> C4
    C6 -- 是 --> C7["保存用户到ThreadLocal"]
    C7 --> C8["刷新Token有效期"]
    C8 --> C9["放行请求"]
end
subgraph D[请求结束]
    D1["请求完成"] --> D2["清理ThreadLocal"]
end
B13 --> C1
C9 --> D1

复制到未命名绘图 - draw.io中用mermaid格式文件创建流程图

优化

目前之后访问被拦截的页面才会刷新有效期,所以这里我们需要优化一下

方式是采用拦截器链,即再加一个拦截器来拦截全部页面,以此来更新有效期

RefreshTokenInterceptor

@Slf4j
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {
    @Autowired
    private 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.基于token获取redis中的用户
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的Hash数据转换为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

LoginInterceptor

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断是否需要拦截(ThreadLocal中是否有用户)
        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();
    }
}

配置类

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Autowired
    private RefreshTokenInterceptor refreshTokenInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1);
        // 刷新token拦截器
        registry.addInterceptor(refreshTokenInterceptor)
                .addPathPatterns("/**").order(0);
    }
}

注:order方法是用来设置哪一个拦截器在前,哪一个在后;规则:数字小的在前,数字大的在后

到此这篇关于基于Redis实现登录功能的文章就介绍到这了,更多相关redis登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Redis分布式锁方案设计之防止订单重复提交或支付

    Redis分布式锁方案设计之防止订单重复提交或支付

    这篇文章主要为大家介绍了Redis分布式锁之防止订单重复提交或支付方案设计示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • 分布式爬虫处理Redis里的数据操作步骤

    分布式爬虫处理Redis里的数据操作步骤

    这篇文章主要介绍了分布式爬虫处理Redis里的数据操作步骤,数据分别存入mongodb和mysql数据库,具体内容详情及实例代码大家参考下本文
    2018-03-03
  • Redis内存碎片率调优处理方式

    Redis内存碎片率调优处理方式

    Redis集群因内存碎片率超过1.5触发告警,分析发现内因与外因导致内存碎片,内因为操作系统内存分配机制,外因为Redis操作特性,使用Redis内置内存碎片清理机制可有效降低碎片率,但需注意可能影响性能,建议使用MEMORY命令诊断内存使用情况,合理配置参数以优化性能
    2024-09-09
  • 在项目中使用redis做缓存的一些思路

    在项目中使用redis做缓存的一些思路

    这篇文章主要介绍了在项目中使用redis做缓存的一些思路,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • 详解redis desktop manager安装及连接方式

    详解redis desktop manager安装及连接方式

    这篇文章主要介绍了redis desktop manager安装及连接方式,本文图文并茂给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-09-09
  • Redis 数值范围查询(Numeric Range Queries)的实现

    Redis 数值范围查询(Numeric Range Queries)的实现

    本文主要介绍了Redis 数值范围查询(Numeric Range Queries)的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-11-11
  • springboot项目redis缓存异常实战案例详解(提供解决方案)

    springboot项目redis缓存异常实战案例详解(提供解决方案)

    redis基本上是高并发场景上会用到的一个高性能的key-value数据库,属于nosql类型,一般用作于缓存,一般是结合数据库一块使用的,但是在使用的过程中可能会出现异常的问题,这篇文章主要介绍了springboot项目redis缓存异常实战案例详解(提供解决方案),需要的朋友可以参考下
    2025-05-05
  • redis淘汰策略的几种实现

    redis淘汰策略的几种实现

    redis内存数据数据集大小升到一定大的时候,就会实行数据淘汰策略,本文主要介绍了redis淘汰策略的几种实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-05-05
  • Redis使用Bitmap的方法实现

    Redis使用Bitmap的方法实现

    本文主要介绍了Redis使用Bitmap的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01
  • redis lua脚本解决高并发下秒杀场景

    redis lua脚本解决高并发下秒杀场景

    这篇文章主要为大家介绍了redis lua脚本解决高并发下秒杀场景,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10

最新评论