(*)Redis应用场景之验证码

  随着技术的不断发展,为了使用户拥有更好的体验,许多网站在登陆界面提供了使用手机验证码的登陆方式。这种功能基本使用Redis数据库实现,不仅提高了效率,还减少了维护量。下面我们通过一个简单的例子来了解一下Redis是怎么实现这种功能的。

需求:手机验证码功能

  该功能有以下3个要求:

  • 输入手机号,点击发送后随机生成6位数字验证码,2分钟内有效
  • 输入验证码,点击验证,返回成功或失败
  • 每个手机号每24小时内只会生成3次验证码

测试环境

  IDEA 2019.1+ Maven + Servlet + Jsp(Bootstarp实现) + Redis5.0

代码实现

添加依赖

  首先在pom.xml加入以下依赖:

<dependency>
      <!--Servlet相关-->
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
    </dependency>
    <!--Jedis-->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
    </dependency>

Jsp页面编写

  其次为jsp页面,该页面实现了一个简单的验证码页面,当用户填写手机号并点击发送验证码按钮后,页面出现120秒的倒计时,并将该表单提交以ajax方式提交给CodeSenderServlet

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>验证码</title>

    <script src="${pageContext.request.contextPath}/static/jquery-3.3.1.min.js"></script>
    <link rel="stylesheet" href="${pageContext.request.contextPath}/static/bootstrap.min.css">
    <script src="${pageContext.request.contextPath}/static/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

</head>
<body>
<div class="container">
    <div class="row">
        <div id="alertdiv" class="col-md-12">
            <form class="navbar-form navbar-left" role="search" id="codeform">
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="填写手机号" name="phone_number">
                    <button type="button" class="btn btn-default" id="sendCode">发送验证码</button>
                    <br>
                    <font id="countdown" color="red"></font>
                    <br>
                    <input type="text" class="form-control" placeholder="填写验证码" name="verify_code">
                    <button type="button" class="btn btn-default" id="verifyCode">确定</button>
                    <font id="result" color="green"></font>
                    <font id="error" color="red"></font>
                </div>
            </form>
        </div>
    </div>
</div>
</body>
<script type="text/javascript">
    var t = 120;//设计倒计时的时间
    var interval;

    function refer() {
        $("#countdown").text("请于" + t + "秒内填写验证码");//显示倒计时
        t--;//计数器递减
        if (t <= 0) {
            clearInterval(interval);
            $("#countdown").text("验证码已失效,请重新发送!");
        }
    }

    $(function () {
        $("#sendCode").click(function () {
            $.post("${pageContext.request.contextPath}/CodeSenderServlet", $("#codeform").serialize(), function (data) {
                if (data == "true") {
                    t = 120;
                    clearInterval(interval);
                    interval = setInterval("refer()", 1000);//启动1秒定时
                } else if (data == "limit") {
                    clearInterval(interval);
                    $("#countdown").text("单日发送超过次数! ");
                }
            });
        });

        $("#verifyCode").click(function () {
            $.post("${pageContext.request.contextPath}/CodeVerifyServlet", $("#codeform").serialize(), function (data) {
                if (data == "true") {
                    $("#result").attr("color", "green");
                    $("#result").text("验证成功,即将跳转到下一页面");
                    clearInterval(interval);
                    $("#countdown").text("");
                } else if (data == "false"){
                    $("#result").attr("color", "red");
                    $("#result").text("验证失败,请重新发送验证码");
                }
            })
        })
    })
</script>
</html>
效果图

Servlet

  CodeSenderServlet具体代码如下:

import redis.clients.jedis.Jedis;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Random;

/**
 * 获取验证码
 *
 * @author : wksky
 * @date : 2019-04-21 18:10
 */
@WebServlet("/CodeSenderServlet")
public class CodeSendServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //从表单中获取电话号码
        String phoneNumber = req.getParameter("phone_number");
        //获取指定的电话号码发送的验证码次数
        Jedis jedis = null;
        try {
            jedis = new Jedis("127.0.0.1", 6379);
            /**
             * 设置Redis的两个键
             * codeKey:该手机号对应的验证码
             * countKey:该手机号验证码的获取次数
             */
            String codeKey = phoneNumber + ":code";
            String countKey = phoneNumber + ":count";

            //对次数进行判断
            String count = jedis.get(countKey);

            //没有发送过验证码
            if (count == null) {
                //生成验证码
                StringBuilder code = new StringBuilder();
                for (int i = 0; i < 6; i++) {
                    code.append(new Random().nextInt(10));
                }
                //在缓存数据库中增加验证码
                jedis.setex(codeKey, 120, code.toString());
                //设置次数重置时间并累加验证码发送次数
                jedis.setex(countKey, 24 * 60 * 60, "1");
                //返回成功的消息
                resp.getWriter().print("true");
            } else if (Integer.valueOf(count) < 3) {//发送次数小于3次
                //生成验证码
                StringBuilder code = new StringBuilder();
                for (int i = 0; i < 6; i++) {
                    code.append(new Random().nextInt(10));
                }
                //在缓存数据库中增加验证码
                jedis.setex(codeKey, 120, code.toString());
                //验证码发送次数+1
                jedis.incr(countKey);
                //返回成功的消息
                resp.getWriter().print("true");
            } else {//发送次数过多返回失败的消息
                resp.getWriter().print("limit");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

  当我们点击发送验证码后,回去Redis数据库查询该电话号码相关信息,根据代码里的逻辑去生成验证码,次数超过3此则不会再生成验证码,该验证码有过期时间。
  之后当手机收到验证码(此处未模拟,需自己去Redis中查看相关验证码)后,将其填写并点击确定,页面又会通过ajax的方式提交给CodeVerifyServlet去进行判断,其代码如下:

import redis.clients.jedis.Jedis;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 校验验证码
 *
 * @author : wksky
 * @date : 2019-04-21 18:10
 */
@WebServlet("/CodeVerifyServlet")
public class CodeVerifyServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取用户填写的电话号码
        String phoneNumber = req.getParameter("phone_number");
        //获取用户填写的验证码
        String verifyCode = req.getParameter("verify_code");
        //Redis中的验证码
        String codeKey = phoneNumber + ":code";

        Jedis jedis = null;
        try {
            jedis = new Jedis("127.0.0.1", 6379);
            //获取redis中的验证码
            String redisCode = jedis.get(codeKey);
            //对获取结果进行校验
            if (verifyCode == null || !verifyCode.equals(redisCode)) {
                resp.getWriter().print("false");
            } else if (verifyCode.equals(redisCode)) {
                resp.getWriter().write("true");
            }
        } catch (Exception e) {

        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

测试

  测试过程如图:



  Redis中的验证码:

127.0.0.1:6379> get 13666666:code
"577737"
127.0.0.1:6379> get 13666666:count
"1"
127.0.0.1:6379> ttl 13666666:code
(integer) 14
127.0.0.1:6379> ttl 13666666:code
(integer) 13
127.0.0.1:6379> ttl 13666666:count
(integer) 86290

  可以看到该验证码随机生成的数字,该验证码生成次数及过期时间

优化:使用阿里云的短信SDK

  前面的代码并没有向指定的手机号发送验证码,所以我们可以通过使用阿里云云通信中的短信服务来提供该服务,在pom.xml文件加入下面依赖:

    <!--阿里云短信sdk-->
    <dependency>
      <groupId>com.aliyun</groupId>
      <artifactId>aliyun-java-sdk-core</artifactId>
      <version>4.1.0</version>
    </dependency>

  之后写一个工具类SendSmsUtil

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;

/**
 * SendSms接口是短信发送接口,支持在一次请求中向多个不同的手机号码发送同样内容的短信。
 *
 * @author : wksky
 * @date : 2019-07-23 14:18
 */
public class SendSmsUtil {
    public static void sendSms(String phoneNumbers, String code) {
        //地区(可不填使用默认值)及AK
        DefaultProfile profile = DefaultProfile.getProfile("default", "请输入阿里云提供的ID", "请输入阿里云提供的密码");
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        request.setMethod(MethodType.POST);
        request.setDomain("dysmsapi.aliyuncs.com");
        request.setVersion("2017-05-25");
        //系统规定参数。取值:SendSms。
        request.setAction("SendSms");
        //接受验证码的手机号
        request.putQueryParameter("PhoneNumbers", phoneNumbers);
        //短信签名名称。请在控制台签名管理页面签名名称一列查看。
        request.putQueryParameter("SignName", "请输入自己的短信签名");
        //短信模板ID。请在控制台模板管理页面模板CODE一列查看。
        request.putQueryParameter("TemplateCode", "请输入自己的短信模版ID");
        //短信模板变量对应的实际值,JSON格式。如:{"code":"1111"}
        request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");
        try {
            CommonResponse response = client.getCommonResponse(request);
            response.getData();
            System.out.println(response.getData());
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
    }
}

  最后将前面的CodeSendServlet中的逻辑代码修改并优化:

import ...

@WebServlet("/CodeSenderServlet")
public class CodeSendServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //从表单中获取电话号码
        String phoneNumber = req.getParameter("phone_number");
        //获取指定的电话号码发送的验证码次数
        Jedis jedis = null;
        try {
            jedis = new Jedis("127.0.0.1", 6379);
            /**
             * 设置Redis的两个键
             * codeKey:该手机号对应的验证码
             * countKey:该手机号验证码的获取次数
             */
            String codeKey = phoneNumber + ":code";
            String countKey = phoneNumber + ":count";

            //对次数进行判断
            String count = jedis.get(countKey);

            //没有发送过验证码
            if (count == null) {
                generateCodeAndSend(phoneNumber, jedis, codeKey);
                //设置次数重置时间并累加验证码发送次数
                jedis.setex(countKey, 24 * 60 * 60, "1");
                //返回成功的消息
                resp.getWriter().print("true");
            } else if (Integer.valueOf(count) < 3) {//发送次数小于3次
                generateCodeAndSend(phoneNumber, jedis, codeKey);
                //验证码发送次数+1
                jedis.incr(countKey);
                //返回成功的消息
                resp.getWriter().print("true");
            } else {//发送次数过多返回失败的消息
                resp.getWriter().print("limit");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 验证码的操作
     *
     * @param phoneNumber 手机号
     * @param jedis       redis
     * @param codeKey     手机号对应的验证码
     */
    private void generateCodeAndSend(String phoneNumber, Jedis jedis, String codeKey) {
        //生成验证码
        StringBuilder code = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            code.append(new Random().nextInt(10));
        }
        //在缓存数据库中增加验证码
        jedis.setex(codeKey, 120, code.toString());
        //发送验证码到指定手机号
        SendSmsUtil.sendSms(phoneNumber, code.toString());
    }
}

  这样,我们点击按钮后手机就会收到验证码啦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335