redis短信验证码实战

Redis实现短信验证码

现在app登录都有个验证码登录功能, 至少包含2个接口:获取验证码和登录。

后台服务需要缓存验证码、账号和该验证码的对应关系以及设置超过时间(一般是60秒)等; 这个验证码就需要中间件来存储。 Redis是按Key-Value方式存储数据, 它还提供了超时自动删除功能, 适合用在短信验证码需求上。
一、  点击“获取验证码”按钮的示例代码:

每个账号对应唯一的key,为了区分redis的key值, 要添加前缀或者后缀, 本例中是添加前缀login:token。
关键代码: stringRedisTemlate.opsForValue.set(login:token + phone, code, 2L,TimeUnit.MINUTES) , 2L表示超过20秒后自动删除该键值。
二、登录接口示例:

登录接口会比较入参验证码参数和缓存在中间件redis里的验证码, 一般app在传输密码字段时会加密; 所以服务端可以先解密或者加密密码明文。

 因为Redis提供了超时自动删除键值功能, 而且在多个tomcat节点可以访问同一个redis, 所以用Redis实现验证码功能。
PS: Redis还提供了分布式锁功能, 是现在互联网公司使用的主流中间件。

Redis | 实战篇 短信登录

前言:

主要完成了基于Session实现登录,解决集群的Session共享问题,从而实现了基于Redis来实现共享Session登录

1.1.发送短信验证码

步骤:

前端提交手机号

==》校验手机号

==》不符合返回错误信息,符合生成验证码

==》保存验证码到Session(应该保存手机号与验证码)

==》发送验证码

==》返回数据

解释:我们这个是以手机号为唯一标识,前端提交用户输入的手机号,后端校验手机号的格式,符合就生成一个随机6位的数字验证码,并保存到Session中(为了后续登录与注册时校验用户验证码是否填写正确)(当然这里应该保存手机号与验证码,不然会有一个错误)

1.2.短信验证码实现登录与注册

步骤:

前端提交手机号和验证码

==》校验手机号和验证码

==》不通过返回错误信息,通过根据手机号查询用户信息

==》判断用户是否存在

==》不存在,创建新用户,并且保存到数据库中

==》最终存在与不存在都将保存用户到Session中(方便后续校验登录状态)

==》结束

 @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //获取缓存数据
        Object catchPhone = session.getAttribute("phone");
        Object catchCode = session.getAttribute("code");
        //获取登录数据
        String code = loginForm.getCode();
        String phone = loginForm.getPhone();
        //校验
        if(!code.equals(catchCode) || !phone.equals(catchPhone)){
            return Result.fail("手机号或验证码错误");
        }
//        if (RegexUtils.isPhoneInvalid(phone)) {
//            return Result.fail("手机号格式错误");
//        }
//        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
//        if(!code.equals(cacheCode)){
//            return Result.fail("验证码错误");
//        }
        User user = query().eq("phone", phone).one();
        //判断用户是否存在
        if(user == null){
            //不存在
            User userNew = new User();
            userNew.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
            userNew.setPhone(phone);
            user = userNew;
            //保存数据库
            save(user);
        }
        //添加到Session
        UserDTO userDTO = new UserDTO();
        BeanUtil.copyProperties(user,userDTO);
//        String token = UUID.randomUUID().toString(true);
//        String tokenKey = LOGIN_USER_KEY + token;
//        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
//                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
//        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        session.setAttribute("user",userDTO);
//       stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok();
    }

解释: 前端提交用户输入的手机号与验证码,后端从Session中取出存入的手机号与验证码与用户提交的相对比,相同那么我们就通过手机号来从数据库中查询用户信息,信息为空,那么我们需要注册一个用户并将其存入数据库中,最终存在与不存在的(我们自己注册了一个用户)都会有一个用户信息,将用户信息存入Session中(这里还有个细节)(方便后续校验登录状态,判断用户是否登录)
 注意:

老师有一个错误,在发送短信验证码的功能实现时,老师只保存了验证码到Session中,那么等到校验验证码来实现登录与注册时,如果我将手机号修改了会怎么样,只要我手机号符合格式一样可以登录与注册,所以我们需要保持前后手机号的一致性,那么存入Session的数据应该是验证码与手机号,然后登录与注册时同时校验手机号与验证码是否一致

1.3.校验登陆状态

步骤:

前端请求携带Cokie

==》后端拦截器从Session中获取用户信息

==》判断用户是否存在

==》不存在不放行,存在放行

public class RefreshTokenInterceptor implements HandlerInterceptor {
 
    private final StringRedisTemplate stringRedisTemplate;
 
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头token
        String token = request.getHeader("authorization");
        //判断是否为空
        if (StrUtil.isBlank(token)) {
 
            return true;
        }
//        HttpSession session = request.getSession();
        //设置key
        String key = LOGIN_USER_KEY + token;
        //获取Redis中的数据
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //判断是否为空
        if(userMap.isEmpty()){
            return true;
        }
        //获取user
        UserDTO user = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
        //存入线程中
        UserHolder.saveUser( user);
        stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

注意:使用拦截器时,前端需要我们返回一些基础数据给它渲染用户信息,而我们之前存入Session的数据是整个用户的数据(包含密码,手机号),这些信息不适合暴露出去,所以对应之前存入Session时的数据需要做一些修改,只需要存一些基础用户信息即可(且存入Session的信息过多也是不好的)
因此我们需要隐藏用户的敏感信息(存入一些需展示的信息即可)

2.集群的Session共享问题

问题介绍:Session的数据一般存储到服务端或Redis中(手动存),而客户端只保存了一个SessionID(通过Cokie传递)(而且每个客户端的SessionID不同),那么当需要访问多个服务端时,Session数据并不共享,就会出现问题

解决:

方案一:服务器之间进行Session的拷贝(内存浪费,有延迟)

方案二:使用Redis存(Redis是存入内存的,访问速度快,多个服务器可以同时访问不会造成内存浪费)

3.基于Redis实现共享Session登录

  @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }
        //2.生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3.保存验证码
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//        session.setAttribute("code",code);
//        session.setAttribute("phone",phone);
        //4.返回验证码
        log.debug("当前验证码:{}",code);
        //5.返回
        return Result.ok();
    }
 
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
//        //获取缓存数据
//        Object catchPhone = session.getAttribute("phone");
//        Object catchCode = session.getAttribute("code");
        //获取登录数据
        String code = loginForm.getCode();
        String phone = loginForm.getPhone();
        //校验
//        if(!code.equals(catchCode) || !phone.equals(catchPhone)){
//            return Result.fail("手机号或验证码错误");
//        }
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if(!code.equals(cacheCode)){
            return Result.fail("验证码错误");
        }
        User user = query().eq("phone", phone).one();
        //判断用户是否存在
        if(user == null){
            //不存在
            User userNew = new User();
            userNew.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
            userNew.setPhone(phone);
            user = userNew;
            //保存数据库
            save(user);
        }
        //添加到Session
        UserDTO userDTO = new UserDTO();
        BeanUtil.copyProperties(user,userDTO);
        String token = UUID.randomUUID().toString(true);
        String tokenKey = LOGIN_USER_KEY + token;
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
//        session.setAttribute("user",userDTO);
       stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok(token);
    }
public class RefreshTokenInterceptor implements HandlerInterceptor {
 
    private final StringRedisTemplate stringRedisTemplate;
 
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头token
        String token = request.getHeader("authorization");
        //判断是否为空
        if (StrUtil.isBlank(token)) {
 
            return true;
        }
//        HttpSession session = request.getSession();
        //设置key
        String key = LOGIN_USER_KEY + token;
        //获取Redis中的数据
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //判断是否为空
        if(userMap.isEmpty()){
            return true;
        }
        //获取user
        UserDTO user = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
        //存入线程中
        UserHolder.saveUser( user);
        stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
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;
    }
 
 
}

解释:其实最终我们只需要修改存入Session的部分,改为存入Redis即可

注意:

1.由于先前Session是用户访问一次(就是进行一次操作)就会更新登录凭证的过期时间(防止登录失效),那么我们也需要实现该功能,一般想到是在拦截器中放行之前更新时间,但是由于之前实现的拦截器有特定的拦截路径,那么没有被拦截的路径我们也需要进行更新,所以我们可以在加一个拦截器专门来更新时间(第一个拦截器更新时间,并进行存用户,第二个拦截器进行判断用户是否存在(它有特定的拦截路径),不存在不放行)
2.由于使用的是StringRedisTemplate,它要求key与value都必须为String类型,所以我们需要将数据转换成String类型再存入Redis中

3.注意拦截器的顺序,一般先添加的拦截器先执行(你也可以设置优先级order)

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    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 RefreshTokenInterceptor(stringRedisTemplate)).order(0).addPathPatterns("/**");
    }
}

案例3:Redis充当短信验证码,设置过期时间

 

package com.shujia.jinjie;

import org.bouncycastle.crypto.modes.EAXBlockCipher;
import redis.clients.jedis.Jedis;

import java.io.*;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/*
    Redis充当短信验证码,设置过期时间

    1、创建一个User类,作为读取mysql用户数据的实体类 getAllUsers()
    2、创建一些工具类:
        CodeTool 主要是生成验证码,发送验证码,将验证码放到redis中
        MySqlTool 主要是获取与数据库的连接对象
        redisTool 主要是获取与单个redis服务的连接对象



 */
public class AnliDemo1 {
    private static final List<User> ALL_USERS = getAllUsers();
    private static final Jedis REDIS_CONN = RedisTool.getRedisConnection();

    public static void main(String[] args) {
        init();
    }

    public static void init() {
        System.out.println("=================== 欢迎登录***婚姻介绍所 ====================");
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入您要做的操作:(1.登录 2.注册)");
        if (sc.hasNextInt()) {
            switch (sc.nextInt()) {
                case 1:
                    System.out.println();
                    System.out.println();
                    login();
                    break;
                case 2:
                    System.out.println();
                    System.out.println();
                    register();
                    break;
                default:
                    System.out.println();
                    System.out.println();
                    System.out.println("没有该选项!");
                    break;
            }
        } else {
            System.out.println("您输入的类型有误!");
        }

    }

    public static void login() {
        System.out.println("请选择登录的方式:【1.密码登录 2.手机验证码登录】");
        Scanner sc = new Scanner(System.in);
        int choice = sc.nextInt();
        if(choice==1){
            System.out.println("欢迎登录!");
            System.out.println("请输入您的用户名:");
            String name = sc.next();

            User u = null;
            for (User user : ALL_USERS) {
                if (name.equals(user.getName())) {
                    u = user;
                    break;
                }
            }

            // 从map集合中判断用户是否存在
            if (u!=null) {
                System.out.println("请输入用户的密码:");
                String pwd = sc.next();
                if (pwd.equals(u.getPassword())) {
                    System.out.println("登录成功!!");
                } else {
                    System.out.println("登录失败!密码不正确!");
                }
            } else {
                System.out.println("该用户还未注册!");
            }
        }else {
            System.out.println("欢迎登录!");
            System.out.println("请输入手机号:");
            String phoneNum = sc.next();

            User u = null;
            for (User user : ALL_USERS) {
                if (phoneNum.equals(user.getPhoneNumber())) {
                    u = user;
                    break;
                }
            }
            // 从map集合中判断用户是否存在
            if (u!=null) {
                System.out.println("正在发送验证码,请稍后~");
                //调用api发送验证码
                String status = CodeTool.sendCode(u);
                if("OK".equals(status)){
                    while (true){
                        System.out.println("请输入收到的验证码:");
                        String yzm = sc.next();
                        String redisRes = REDIS_CONN.get(u.getName());
                        if(redisRes!=null && redisRes.equals(yzm)){
                            System.out.println("登录成功!!");
                            break;
                        }else {
                            System.out.println("验证码输入错误!!!");
                        }
                    }
                }
            } else {
                System.out.println("该手机号还未注册!");
            }
        }
    }

    public static void register() {
        BufferedWriter bw = null;
        try {
            Scanner sc = new Scanner(System.in);
            System.out.println("欢迎注册!");
            System.out.println("请设置您的用户名:");
            String name = sc.next();
            User u = null;
            for (User user : ALL_USERS) {
                if (name.equals(user.getName())) {
                    u = user;
                    break;
                }
            }

            // 从map集合中判断用户是否存在
            if (u!=null) {
                System.out.println("该用户名已被使用!!");
            } else {
                System.out.println("请设置您的账户密码:");
                String pwd = sc.next();
                System.out.println("请设置您的账户邮箱:");
                String email = sc.next();
                System.out.println("请设置您的账户手机号:");
                String phoneNum = sc.next();
                System.out.println("请设置您的账户地址:");
                String address = sc.next();
                //将用户名和密码写入到文件中【追加写】
                bw = new BufferedWriter(new FileWriter("src/shujia/day14/users.txt", true));
                bw.write(name + "|" + pwd+"|"+email+"|"+phoneNum+"|"+address);
                bw.newLine();
                bw.flush();

                System.out.println("用户注册成功!!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bw != null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static List<User> getAllUsers() {

        ArrayList<User> userList = new ArrayList<>();

        try {
            //获取与数据库的连接对象
            Connection conn = MySqlTool.getConnection();
            //查询所有的用户数据
            Statement statement = conn.createStatement();

            ResultSet resultSet = statement.executeQuery("select name,password,email,phoneNumber from users");

            while (resultSet.next()){
                String name = resultSet.getString("name");
                String password = resultSet.getString("password");
                String email = resultSet.getString("email");
                String phoneNumber = resultSet.getString("phoneNumber");

                User user = new User(name, password, email, phoneNumber);
                userList.add(user);

            }

        }catch (Exception e){
            e.printStackTrace();
        }


        return userList;

    }
}

参考:

https://blog.csdn.net/fanTuanye/article/details/147861303

https://blog.csdn.net/2402_88700528/article/details/147779334

https://www.cnblogs.com/wyh-study/p/18739644

posted on 2025-05-25 23:52  SZ_文彬  阅读(93)  评论(0)    收藏  举报