随着技术的不断发展,为了使用户拥有更好的体验,许多网站在登陆界面提供了使用手机验证码的登陆方式。这种功能基本使用Redis数据库实现,不仅提高了效率,还减少了维护量。下面我们通过一个简单的例子来了解一下Redis是怎么实现这种功能的。
需求:手机验证码功能
该功能有以下3个要求:
- 输入手机号,点击发送后随机生成6位数字验证码,2分钟内有效
- 输入验证码,点击验证,返回成功或失败
- 每个手机号每24小时内只会生成3次验证码
测试环境
IDEA 2019.1+ Maven + Servlet + Jsp(Bootstarp实现) + Redis5.0
代码实现
添加依赖
首先在pom.xml
加入以下依赖:
javax.servlet
javax.servlet-api
3.1.0
redis.clients
jedis
2.9.0
Jsp页面编写
其次为jsp页面,该页面实现了一个简单的验证码页面,当用户填写手机号并点击发送验证码
按钮后,页面出现120秒的倒计时,并将该表单提交以ajax
方式提交给CodeSenderServlet
:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
验证码
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
文件加入下面依赖:
com.aliyun
aliyun-java-sdk-core
4.1.0
之后写一个工具类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());
}
}
这样,我们点击按钮后手机就会收到验证码啦。