阿里云人机验证和短信发送
背景
最近的工作中需要实现用户注册、登录功能,都是通过短信进行二次验证的。使用人机验证,就是用来防止登录或者注册的短信接口被刷。调研了好几个平台,包括阿里云、易盾、腾讯云,它们的实现都是使用webview打开网页的方式进行处理。
我能理解它们肯定主要是给网页前端使用的,用在手机应用内,确实可能不算很多。但是确实接入起来我觉得比较麻烦,要是有封装好的AndroidSDK或者iOSSDK,直接调用接口,自己弹出界面(不用做屏幕适配了,只管收回调),肯定是最好的。可惜~没有。
这里留存一下几个厂商的文档,万一后面要用呢。
阿里云:https://www.aliyun.com/product/security/captcha
易盾:https://dun.163.com/product/captcha
腾讯:https://cloud.tencent.com/document/product/1110/49810
技术选型
- 阿里云人机验证 – 拼图验证
- 阿里云短信
阿里云人机验证(验证码2.0)实现流程
阿里云文档
查看文档,了解下什么是验证码2.0,开通服务什么的按文档来就行。
Web和H5客户端接入文档
Android端接入文档
服务端接入文档
流程概述
用户在客户端点击获取手机验证码 -> 弹出人机验证(webview加载html) -> js代码初始化阿里云SDK,展示拼图验证 -> 用户完成拼图 -> 阿里云SDK回调验证参数(一长串) -> js传递给自己的服务端 -> 服务端调用阿里云接口验证(验证通过则发送短信,不通过则返回验证失败) -> 返回结果给客户端 -> 客户端根据验证结果关闭界面或者继续进行验证。
这里面其实有一些被盗用的风险,我在后面会提到。
验证码2.0的SDK接入(前端)
参考Web和H5客户端接入文档,实现一个html页面。在其中初始化阿里云SDK,并展示拼图验证。下面是代码,注释很详细,不多说。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> <title>阿里云验证码 V2 嵌入式接入</title>
<script> window.AliyunCaptchaConfig = { region: "cn", prefix: "xxxx", }; </script>
<script src="https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"></script> </head> <body> <div id="captcha-element"></div>
<script> var captchaInstance = null;
window.initAliyunCaptcha({ SceneId: "xxxx", mode: "embed", element: "#captcha-element", immediate: true, getInstance: function (ins) { captchaInstance = ins; }, captchaVerifyCallback: async function (captchaVerifyParam) { console.log("验证参数:", captchaVerifyParam); var result = { code: 200 };
return { captchaResult: result.code == 200, bizResult: result.code == 200, }; }, onBizResultCallback: function (bizResult) { }, slideStyle: { width: 360, height: 40 }, language: "cn", }); </script> </body> </html>
|
服务端验证
我的服务端使用的是springboot,直接依赖阿里云的JavaSDK,
1 2 3 4 5 6
| <!--阿里云人机验证SDK https: <dependency> <groupId>com.aliyun</groupId> <artifactId>captcha20230305</artifactId> <version>1.2.0</version> </dependency>
|
初始化阿里云客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Bean public com.aliyun.captcha20230305.Client captchaClient(){ log.debug("captchaClient start."); com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client(); com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config(); config.setCredential(credential); config.endpoint = "captcha.cn-shanghai.aliyuncs.com"; try { return new com.aliyun.captcha20230305.Client(config); } catch (Exception e) { log.error(e.getMessage(), e); throw new RuntimeException(e); } }
|
服务端拿着阿里云返回的captchaVerifyParam,请求阿里云验证,将结果返回给前端。
阿里云响应数据字段说明参见文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public int verifyAliCaptcha(String captchaVerifyParam, String sceneId) { com.aliyun.captcha20230305.models.VerifyIntelligentCaptchaRequest verifyIntelligentCaptchaRequest = new com.aliyun.captcha20230305.models.VerifyIntelligentCaptchaRequest() .setCaptchaVerifyParam(captchaVerifyParam) .setSceneId(sceneId); try { com.aliyun.captcha20230305.models.VerifyIntelligentCaptchaResponse resp = captchaClient.verifyIntelligentCaptchaWithOptions(verifyIntelligentCaptchaRequest, new com.aliyun.teautil.models.RuntimeOptions()); log.info(new com.google.gson.Gson().toJson(resp)); if (!resp.body.success){ log.error("captcha verify failed-1, body={}", resp.body); return 0; } if (!resp.body.result.verifyResult){ log.error("captcha verify failed-2, body={}", resp.body); return 0; }else{ return 1; } } catch (TeaException error) { log.error("verifyAliCaptcha TeaException "+error.getMessage()+" 诊断地址:"+error.getData().get("Recommend").toString()); } catch (Exception _error) { TeaException error = new TeaException(_error.getMessage(), _error); log.error("verifyAliCaptcha Exception " + error.getMessage()+" 诊断地址:"+error.getData().get("Recommend").toString()); } return 0; }
|
前端拿着结果,可以决定是否关闭拼图验证或验证通过。
阿里云短信发送
依赖阿里云SDK
1 2 3 4 5 6
| <!--阿里云发送短信 最新版本号:https: <dependency> <groupId>com.aliyun</groupId> <artifactId>dysmsapi20170525</artifactId> <version>4.5.0</version> </dependency>
|
初始化阿里云客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Bean public com.aliyun.dysmsapi20170525.Client smsClient(){ log.warn("smsClient start."); com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config() .setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")) .setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
config.endpoint = "dysmsapi.aliyuncs.com";
try { return new com.aliyun.dysmsapi20170525.Client(config); } catch (Exception e) { log.error(e.getMessage(), e); throw new RuntimeException(e); } }
|
发送短信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public boolean sendCaptcha(String phone, String captcha) { com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest() .setPhoneNumbers(phone) .setSignName("xx网络/公司") .setTemplateCode("模板id") .setTemplateParam("{\"code\":\"" + captcha +"\"}"); try { com.aliyun.dysmsapi20170525.models.SendSmsResponse sendSmsResponse = smsClient.sendSms(sendSmsRequest); if (sendSmsResponse.statusCode == 200 && sendSmsResponse.body.code.equals("OK")) { log.info(new com.google.gson.Gson().toJson(sendSmsResponse)); return true; }else{ log.error("sendCaptcha failed, body={}", sendSmsResponse.body); return false; } } catch (Exception e) { log.error("sendCaptcha Exception "+e.getMessage(),e); } return false; }
|
总结
1、上面的代码和流程已经能够支持我完成需求了,但是有一天我无意中看到阿里云的文档计费说明中提到计费是从完成哪一步计算的,所谓的V2版本和V3版本,也是不清不楚的,经过我问客服。
才知道如何区分V2还是V3版本:
1 2 3
| 版本识别:可通过客户端调用参数区分。V3架构使用 SceneId 及 success/fail 回调函数;V2架构则使用 captchaVerifyCallback 或 onBizResultCallback。
前端使用 captchaVerifyCallback 且服务端调用 verifyIntelligentCaptchaWithOptions 接口,属于 V2 架构。
|
从计费标准来说:我现在用的是拼图验证。从计费说明都写的拼图验证计算次数是步骤3.也就是我方服务器到阿里云验证的流程来计费的,所以V2和V3对于我来说没有区别,计费都是一样的,也就不用管加密不加密,场景id泄露不泄露的问题了。(问题引申)
2、人机验证被盗用的问题分析和处理
由于我们的场景id、身份标以及我们请求后端的接口代码都是在html中的,那么我们的整个验证流程可以说是完全裸奔,那么就存在我们的人机验证功能被盗用的可能。举例说明:
(1)小明通过获取html内容,阅读js代码,在它的网页上来使用人机验证功能(假设它搭了个盗版资源网站,用户登录时做人机验证),用着我们的场景id,用着我们的身份标,阿里云正常加载出来拼图验证码,正常回调验证参数。同时,小明在js中请求我们的服务端,因为它知道我们的接口加签方式以及我们的各种参数有效值,服务端完全分辨不出来这个请求是被盗用的,傻乎乎的到阿里云验证,并且返回验证结果。那么小明就白嫖了我们出钱买的人机验证服务了。
这个情况,小明的用户都是用浏览器来访问的,我们可以通过后端接口限制跨域来杜绝,只要是在浏览器中,就绕不过跨域的限制。
(2)小明就是想搞我们,它不通过浏览器访问,那么跨域的限制就解决不了,它通过各种方式,拿到了阿里云返回的有效验证参数,通过代码请求我们接口,我们也就区分不出来了它是被盗用的请求,还是傻乎乎的去阿里云验证,返回结果给它。
这时候只能上IP频次限制了,小明一个人手上的IP毕竟有限,翻不起大浪。
(3)最狠的方案,后端永远返回通过,阿里云那边的验证结果只有我们自己知道,前端拿到后端返回的结果(永远都是通过,直接就关闭拼图验证码了),用户收到短信了,就填呗,没收到就再次获取。这是兜底的方案,用户体验不好。但是却什么都不用做,就能解决问题,因为小明根本不敢用我们的验证,它永远都是通过!