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 | 实战篇 短信登录
前言:
主要完成了基于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
浙公网安备 33010602011771号