当前位置: 代码网 > it编程>数据库>Redis > 基于Redis实现短信验证码登录功能

基于Redis实现短信验证码登录功能

2025年01月23日 Redis 我要评论
1 基于session实现短信验证码登录 /** * 发送验证码 */ @override public result sendcode(string phone,

1 基于session实现短信验证码登录

    /**
     * 发送验证码
     */
    @override
    public result sendcode(string phone, httpsession session) {
        // 1、判断手机号是否合法
        if (regexutils.isphoneinvalid(phone)) {
            return result.fail("手机号格式不正确");
        }
        // 2、手机号合法,生成验证码,并保存到session中
        string code = randomutil.randomnumbers(6);
        session.setattribute(systemconstants.verify_code, code);
        // 3、发送验证码
        log.info("验证码:{}", code);
        return result.ok();
    }
 
    /**
     * 用户登录
     */
    @override
    public result login(loginformdto loginform, httpsession session) {
        string phone = loginform.getphone();
        string code = loginform.getcode();
        // 1、判断手机号是否合法
        if (regexutils.isphoneinvalid(phone)) {
            return result.fail("手机号格式不正确");
        }
        // 2、判断验证码是否正确
        string sessioncode = (string) session.getattribute(login_code);
        if (code == null || !code.equals(sessioncode)) {
            return result.fail("验证码不正确");
        }
        // 3、判断手机号是否是已存在的用户
        user user = this.getone(new lambdaquerywrapper<user>()
                .eq(user::getpassword, phone));
        if (objects.isnull(user)) {
            // 用户不存在,需要注册
            user = createuserwithphone(phone);
        }
        // 4、保存用户信息到session中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
        session.setattribute(login_user, user);
        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));
        this.save(user);
        return user;
    }

2 配置登录拦截器

public class logininterceptor implements handlerinterceptor {
    /**
     * 前置拦截器,用于判断用户是否登录
     */
    @override
    public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
        httpsession session = request.getsession();
        // 1、判断用户是否存在
        user user = (user) session.getattribute(login_user);
        if (objects.isnull(user)){
            // 用户不存在,直接拦截
            response.setstatus(httpstatus.http_unauthorized);
            return false;
        }
        // 2、用户存在,则将用户信息保存到threadlocal中,方便后续逻辑处理
        // 比如:方便获取和使用用户信息,session获取用户信息是具有侵入性的
        threadlocalutls.saveuser(user);
 
        return handlerinterceptor.super.prehandle(request, response, handler);
    }
}

3 配置完拦截器还需将自定义拦截器添加到springmvc的拦截器列表中 才能生效

@configuration
public class webmvcconfig implements webmvcconfigurer {
 
    @override
    public void addinterceptors(interceptorregistry registry) {
        // 添加登录拦截器
        registry.addinterceptor(new logininterceptor())
                // 设置放行请求
                .excludepathpatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}
 

4 session集群共享问题

(1)什么是session集群共享问题

在分布式集群环境中,会话(session)共享是一个常见的挑战。默认情况下,web 应用程序的会话是保存在单个服务器上的,当请求不经过该服务器时,会话信息无法被访问。

(2) session集群共享问题造成哪些问题

 服务器之间无法实现会话状态的共享。比如:在当前这个服务器上用户已经完成了登录,session中存储了用户的信息,能够判断用户已登录,但是在另一个服务器的session中没有用户信息,无法调用显示没有登录的服务器上的服务

 (3)如何解决session集群共享问题

方案一:session拷贝(不推荐)

tomcat提供了session拷贝功能,通过配置tomcat可以实现session的拷贝,但是这会增加服务器的额外内存开销,同时会带来数据一致性问题

方案二:redis缓存(推荐)

redis缓存具有session存储一样的特点,基于内存、存储结构可以是key-value结构、数据共享

(4)redis缓存相较于传统session存储的优点

1 高性能和可伸缩性:redis 是一个内存数据库,具有快速的读写能力。相比于传统的 session 存储方式,将会话数据存储在 redis 中可以大大提高读写速度和处理能力。此外,redis 还支持集群和分片技术,可以实现水平扩展,处理大规模的并发请求。

2 可靠性和持久性:redis 提供了持久化机制,可以将内存中的数据定期或异步地写入磁盘,以保证数据的持久性。这样即使发生服务器崩溃或重启,会话数据也可以被恢复。

3 丰富的数据结构:redis 不仅仅是一个键值存储数据库,它还支持多种数据结构,如字符串、列表、哈希、集合和有序集合等。这些数据结构的灵活性使得可以更方便地存储和操作复杂的会话数据。

4 分布式缓存功能:redis 作为一个高效的缓存解决方案,可以用于缓存会话数据,减轻后端服务器的负载。与传统的 session 存储方式相比,使用 redis 缓存会话数据可以大幅提高系统的性能和可扩展性。

5 可用性和可部署性:redis 是一个强大而成熟的开源工具,有丰富的社区支持和活跃的开发者社区。它可以轻松地与各种编程语言和框架集成,并且可以在多个操作系统上运行。

ps:但是redis费钱,而且增加了系统的复杂度

5 基于redis实现短信验证码登录

6 hash 结构与 string 结构类型的比较

  • string 数据结构是以 json 字符串的形式保存,更加直观,操作也更加简单,但是 json 结构会有很多非必须的内存开销,比如双引号、大括号,内存占用比 hash 更高
  • hash 数据结构是以 hash 表的形式保存,可以对单个字段进行crud,更加灵活

7 redis替代session需要考虑的问题

(1)选择合适的数据结构,了解 hash 比 string 的区别

(2)选择合适的key,为key设置一个业务前缀,方便区分和分组,为key拼接一个uuid,避免key冲突防止数据覆盖

(3)选择合适的存储粒度,对于验证码这类数据,一般设置ttl为3min即可,防止大量缓存数据的堆积,而对于用户信息这类数据可以稍微设置长一点,比如30min,防止频繁对redis进行io操作

8 基于redis短信验证登录

    /**
     * 发送验证码
     *
     * @param phone
     * @param session
     * @return
     */
    @override
    public result sendcode(string phone, httpsession session) {
        // 1、判断手机号是否合法
        if (regexutils.isphoneinvalid(phone)) {
            return result.fail("手机号格式不正确");
        }
        // 2、手机号合法,生成验证码,并保存到redis中
        string code = randomutil.randomnumbers(6);
        stringredistemplate.opsforvalue().set(login_code_key + phone, code,
                redisconstants.login_code_ttl, timeunit.minutes);
        // 3、发送验证码
        log.info("验证码:{}", code);
        return result.ok();
    }
 
    /**
     * 用户登录
     *
     * @param loginform
     * @param session
     * @return
     */
    @override
    public result login(loginformdto loginform, httpsession session) {
        string phone = loginform.getphone();
        string code = loginform.getcode();
        // 1、判断手机号是否合法
        if (regexutils.isphoneinvalid(phone)) {
            return result.fail("手机号格式不正确");
        }
        // 2、判断验证码是否正确
        string rediscode = stringredistemplate.opsforvalue().get(login_code_key + phone);
        if (code == null || !code.equals(rediscode)) {
            return result.fail("验证码不正确");
        }
        // 3、判断手机号是否是已存在的用户
        user user = this.getone(new lambdaquerywrapper<user>()
                .eq(user::getphone, phone));
        if (objects.isnull(user)) {
            // 用户不存在,需要注册
            user = createuserwithphone(phone);
        }
        // 4、保存用户信息到redis中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
        userdto userdto = beanutil.copyproperties(user, userdto.class);
        // 将对象中字段全部转成string类型,stringredistemplate只能存字符串类型的数据
        map<string, object> usermap = beanutil.beantomap(userdto, new hashmap<>(),
                copyoptions.create().setignorenullvalue(true).
                        setfieldvalueeditor((fieldname, fieldvalue) -> fieldvalue.tostring()));
        string token = uuid.randomuuid().tostring(true);
        string tokenkey = login_user_key + token;
        stringredistemplate.opsforhash().putall(tokenkey, usermap);
        stringredistemplate.expire(tokenkey, login_user_ttl, timeunit.minutes);
 
        return result.ok(token);
    }
 
    /**
     * 根据手机号创建用户并保存
     *
     * @param phone
     * @return
     */
    private user createuserwithphone(string phone) {
        user user = new user();
        user.setphone(phone);
        user.setnickname(systemconstants.user_nick_name_prefix + randomutil.randomstring(10));
        this.save(user);
        return user;
    }

9 配置登录拦截器

单独配置一个拦截器用户刷新redis中的token:在基于session实现短信验证码登录时,我们只配置了一个拦截器,这里需要另外再配置一个拦截器专门用于刷新存入redis中的 token,因为我们现在改用redis了,为了防止用户在操作网站时突然由于redis中的 token 过期,导致直接退出网站,严重影响用户体验。那为什么不把刷新的操作放到一个拦截器中呢,因为之前的那个拦截器只是用来拦截一些需要进行登录校验的请求,对于哪些不需要登录校验的请求是不会走拦截器的,刷新操作显然是要针对所有请求比较合理,所以单独创建一个拦截器拦截一切请求,刷新redis中的key

 登录拦截器:

public class logininterceptor implements handlerinterceptor {
 
    /**
     * 前置拦截器,用于判断用户是否登录
     */
    @override
    public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
        // 判断当前用户是否已登录
        if (threadlocalutls.getuser() == null){
            // 当前用户未登录,直接拦截
            response.setstatus(httpstatus.http_unauthorized);
            return false;
        }
        // 用户存在,直接放行
        return true;
    }
}

刷新token的拦截器

public class refreshtokeninterceptor implements handlerinterceptor {
 
    // new出来的对象是无法直接注入ioc容器的(logininterceptor是直接new出来的)
    // 所以这里需要再配置类中注入,然后通过构造器传入到当前类中
    private stringredistemplate stringredistemplate;
 
    public refreshtokeninterceptor(stringredistemplate stringredistemplate) {
        this.stringredistemplate = stringredistemplate;
    }
 
    @override
    public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
        // 1、获取token,并判断token是否存在
        string token = request.getheader("authorization");
        if (strutil.isblank(token)){
            // token不存在,说明当前用户未登录,不需要刷新直接放行
            return true;
        }
        // 2、判断用户是否存在
        string tokenkey = login_user_key + token;
        map<object, object> usermap = stringredistemplate.opsforhash().entries(tokenkey);
        if (usermap.isempty()){
            // 用户不存在,说明当前用户未登录,不需要刷新直接放行
            return true;
        }
        // 3、用户存在,则将用户信息保存到threadlocal中,方便后续逻辑处理,比如:方便获取和使用用户信息,redis获取用户信息是具有侵入性的
        userdto userdto = beanutil.fillbeanwithmap(usermap, new userdto(), false);
        threadlocalutls.saveuser(beanutil.copyproperties(usermap, userdto.class));
        // 4、刷新token有效期
        stringredistemplate.expire(token, login_user_ttl, timeunit.minutes);
        return true;
    }
}

将自定义的拦截器添加到springmvc的拦截器表中,使其生效:

@configuration
public class webmvcconfig implements webmvcconfigurer {
 
    // new出来的对象是无法直接注入ioc容器的(logininterceptor是直接new出来的)
    // 所以这里需要再配置类中注入,然后通过构造器传入到当前类中
    @resource
    private stringredistemplate stringredistemplate;
 
    @override
    public void addinterceptors(interceptorregistry registry) {
        // 添加登录拦截器
        registry.addinterceptor(new logininterceptor())
                // 设置放行请求
                .excludepathpatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1); // 优先级默认都是0,值越大优先级越低
        // 添加刷新token的拦截器
        registry.addinterceptor(new refreshtokeninterceptor(stringredistemplate)).addpathpatterns("/**").order(0);
    }
}
  • refreshtokeninterceptor 先执行,主要用来检查 token 的有效性并刷新 token 的有效期,同时将用户信息存入 threadlocal
  • logininterceptor 后执行,验证 threadlocal 中是否有用户信息,以确认用户是否登录。

以上就是基于redis实现短信验证码登录功能的详细内容,更多关于redis短信验证码登录的资料请关注代码网其它相关文章!

(0)

相关文章:

  • 深入理解Redis大key的危害及解决方案

    一、背景redis作为后端开发中的一个常用组件,在开发过程中承担着非常重要的作用。在其实际使用过程中,我们常常会面临一些技术挑战,其中常见的问题就包括大key问题。当某些数据量较大…

    2025年01月19日 数据库
  • Redis存储的列表分页和检索的实现方法

    Redis存储的列表分页和检索的实现方法

    一、redis 列表的基本操作在实现分页和检索之前,先回顾一下 redis 列表的常用命令:lpush key value: 在列表左侧插入一个元素。rpush... [阅读全文]
  • Redis使用SETNX命令实现分布式锁

    Redis使用SETNX命令实现分布式锁

    什么是分布式锁分布式锁是一种用于在分布式系统中控制多个节点对共享资源进行访问的机制。在分布式系统中,由于多个节点可能同时访问和修改同一个资源,因此需要一种方法来... [阅读全文]
  • Redis主从复制的原理分析

    Redis主从复制的原理分析

    redis主从复制的原理主从复制概述在现代分布式系统中,redis作为一款高性能的内存数据库,其主从复制功能是确保数据高可用性和扩展性的关键技术之一。通过主从复... [阅读全文]
  • Redis缓存异常之缓存雪崩问题解读

    缓存异常:缓存雪崩、击穿、穿透当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据。缓存击穿,缓存更数据库中都没有应用要访问的数据。1.缓存雪崩1.1了解缓存雪崩是指大量的应…

    2025年01月16日 数据库
  • Redis哨兵机制的使用详解

    一.哨兵机制基本解读主库发生故障了,如何不间断的服务?哨兵模式:有效的解决主从库自动切换的关键机制在redis中如果从库发生故障了,客户端可以继续向主库和其他从库发消息,进行相关操…

    2025年01月16日 数据库

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com