从构建分布式秒杀系统聊聊验证码
前言
为了拦截大部分请求,秒杀案例前端引入了验证码。淘宝上很多人吐槽,等输入完秒杀活动结束了,对,结束了...... 当然了,验证码的真正作用是,有效拦截刷单操作,让羊毛党空手而归。
验证码
那么到底什么是验证码呢?验证码作为一种人机识别手段,其终极目的,就是区分正常人和机器的操作。我们常见的互联网注册、登录、发帖、领优惠券、投票等等应用场景,都有被机器刷造成各类损失的风险。
目前常见的验证码形式多为图片验证码,即数字、字母、文字、图片物体等形式的传统字符验证码。这类验证码看似简单易操作,但实际用户体验较差(参见12306网站),且随着ocr技术和打码平台的利用,图片比较容易被破解,被破解之后就形同虚设。
这里我们使用腾讯的智能人机安全验证码,告别传统验证码的单点防御,十道安全栅栏打造立体全面的安全验证,将黑产拒之门外。
场景
下面我们来瞅瞅验证码轻松解决了那些场景安全问题:
- 登录注册,为你防护撞库攻击、阻止注册机批量注册
- 活动秒杀,有效拦截刷单操作,让羊毛党空手而归
- 点赞发帖,有效解决广告屠版、恶意灌水、刷票问题
- 数据保护,防止自动机、爬虫盗取网页内容和数据
申请
申请地址:https://007.qq.com/product.html
在线体验:https://007.qq.com/online.html
只要一个qq就可以免费申请,对于一般的企业oa系统或者个人博客网站,验证码免费套餐足够了已经,具备以下特点:
- 2000次/小时安全防护
- 支持免验证+分级验证
- 三分钟快速接入
- 全功能配置后台
- 支持https
- 阈值内流量无广告
2000次/小时的安全防护,一般很少达到如此效果,当然了即时超出阈值,顶多也就是多个广告而已。
接入
快读接入:https://007.qq.com/quick-start.html
接入与帮助提供了多种客户端和服务端的接入案例,这里我们使用我们秒杀案例中最熟悉的java语言来接入。
前端
引入js:
<script src="https://ssl.captcha.qq.com/tcaptcha.js"></script>
页面元素:
<!--点击此元素会自动激活验证码,不一定是button,其他标签也可以--> <!--id : 元素的id(必须)--> <!--data-appid : appid(必须)--> <!--data-cbfn : 回调函数名(必须)--> <!--data-biz-state : 业务自定义透传参数(可选)--> <button id="tencentcaptcha" data-appid="*********" data-cbfn="callback">验证</button>
js回调:
<script type="text/javascript"> window.callback = function(res){ console.log(res) // res(未通过验证)= {ret: 1, ticket: null} // res(验证成功) = {ret: 0, ticket: "string", randstr: "string"} if(res.ret === 0){ startseckill(res) } } //后台验证ticket,并进入秒杀队列 function startseckill(res){ $.ajax({ url : "startseckill", type : 'post', data : {'ticket' : res.ticket,'randstr':res.randstr}, success : function(result) { //验证是否通过,提示用户 } }); } </script>
后端
@api(tags = "秒杀商品") @restcontroller @requestmapping("/seckillpage") public class seckillpagecontroller { @autowired private activemqsender activemqsender; //自定义工具类 @autowired private httpclient httpclient; //这里自行配置参数 @value("${qq.captcha.url}") private string url; @value("${qq.captcha.aid}") private string aid; @value("${qq.captcha.appsecretkey}") private string appsecretkey; @requestmapping("/startseckill") public result startseckill(string ticket,string randstr,httpservletrequest request) { httpmethod method =httpmethod.post; multivaluemap<string, string> params= new linkedmultivaluemap<string, string>(); params.add("aid", aid); params.add("appsecretkey", appsecretkey); params.add("ticket", ticket); params.add("randstr", randstr); params.add("userip", iputils.getipaddr(request)); string msg = httpclient.client(url,method,params); /** * response: 1:验证成功,0:验证失败,100:appsecretkey参数校验错误[required] * evil_level:[0,100],恶意等级[optional] * err_msg:验证错误信息[optional] */ //{"response":"1","evil_level":"0","err_msg":"ok"} jsonobject json = jsonobject.parseobject(msg); string response = (string) json.get("response"); if("1".equals(response)){ //进入队列、假数据而已 destination destination = new activemqqueue("seckill.queue"); activemqsender.sendchannelmess(destination,1000+";"+1); return result.ok(); }else{ return result.error("验证失败"); } } }
自定义请求工具类 httpclient:
@service public class httpclient { public string client(string url, httpmethod method, multivaluemap<string, string> params){ resttemplate client = new resttemplate(); httpheaders headers = new httpheaders(); // 请勿轻易改变此提交方式,大部分的情况下,提交方式都是表单提交 headers.setcontenttype(mediatype.application_form_urlencoded); httpentity<multivaluemap<string, string>> requestentity = new httpentity<multivaluemap<string, string>>(params, headers); // 执行http请求 responseentity<string> response = client.exchange(url, httpmethod.post, requestentity, string.class); return response.getbody(); } }
获取ip地址工具类 iputils :
/** * ip地址 */ public class iputils { private static logger logger = loggerfactory.getlogger(iputils.class); /** * 获取ip地址 * 使用nginx等反向代理软件, 则不能通过request.getremoteaddr()获取ip地址 * 如果使用了多级反向代理的话,x-forwarded-for的值并不止一个,而是一串ip地址,x-forwarded-for中第一个非unknown的有效ip字符串,则为真实ip地址 */ public static string getipaddr(httpservletrequest request) { string ip = null; try { ip = request.getheader("x-forwarded-for"); if (stringutils.isempty(ip) || "unknown".equalsignorecase(ip)) { ip = request.getheader("proxy-client-ip"); } if (stringutils.isempty(ip) || ip.length() == 0 || "unknown".equalsignorecase(ip)) { ip = request.getheader("wl-proxy-client-ip"); } if (stringutils.isempty(ip) || "unknown".equalsignorecase(ip)) { ip = request.getheader("http_client_ip"); } if (stringutils.isempty(ip) || "unknown".equalsignorecase(ip)) { ip = request.getheader("http_x_forwarded_for"); } if (stringutils.isempty(ip) || "unknown".equalsignorecase(ip)) { ip = request.getremoteaddr(); } } catch (exception e) { logger.error("iputils error ", e); } // 使用代理,则获取第一个ip地址 if (stringutils.isempty(ip) && ip.length() > 15) { if (ip.indexof(",") > 0) { ip = ip.substring(0, ip.indexof(",")); } } return ip; } }
案例效果图
启动项目访问:http://localhost:8080/seckill/1000.shtml
定制接入
在系统登录的时候,我们需要先校验用户名以及密码,然后调用验证码操作,这里就需要我们定制接入了。
<!-- 项目中使用了vue --> <div class="log_btn" @click="login" >登录</div>
login: function () { //这里校验用户名以及密码 // 直接生成一个验证码对象 var captcha = new tencentcaptcha('2001344788', function(res) { if(res.ret === 0){//回调成功 var data = {'username':username,'password':password,'ticket':res.ticket,'randstr':res.randstr} $.ajax({ type: "post", url: "sys/logincaptcha", data: data, datatype: "json", success: function(result){ //校验是否成功 } }); } }); captcha.show(); // 显示验证码 },
后台监控
腾讯后台还提供了简单实用的数据监控,如下:
小结
总体来说,系统接入人机验证码还是很方便的,并没有技术难点,难点已经被提供商封装,我们只需要简单的调用即可。
秒杀案例:
演示案例(点击生成按钮):
上一篇: 关于函数式接口