现在的应用中对于登录,注册,短信验证码。。。这些场景来说,验证码真的是必不可少。随着技术的发展,也使得验证码从当初的图形验证码,发展到今天的滑块,倒立文字点击,数学计算,手势滑动,拼图,刮图。。。等等各种花样,总之一个目的,阻止机器人的访问。
验证码这玩意儿,确实给用户带来了很不好的体验,很多应用的验证码确实玄乎的很让人让人抓狂。
ReCaptcha
这是谷歌的一个验证码程序,它免费,强大,很多世界级别的应用都在使用它(靠谱)。
它现在有2个比较流行的版本!!
ReCaptcha 2,它长这样
这玩意儿,有时候挺让人烦的,一看到这种验证码,愁得慌。
ReCaptcha 3
没法展示给你看… 是的,没图,没摁钮。这是最高级的一个版本,不骚扰用户。而是偷偷的读取一些客户端的环境数据(具体是啥我也不知道),提交给服务器,最后服务器给出一个数字评分。0 - 1。0 肯定是机器人,1肯定不是。应用需要通过这个评分来决定当前请求是否合法。
这也是这篇文章,要接入的版本,下面,从注册开始,演示一个接入ReCaptcha 3的案例。
注册
不要问为什么打不开下面的这些地址,我也不知道。
注册地址 https://www.google.com/recaptcha/admin/create
很简单,按照自己的需求填写名字,应用的访问域名。即可。
注册成功
有2个密钥,需要记住。第一个叫做前端密钥,会暴露在客户端。第二个叫做后端密钥,用于和远程服务器通信,不能暴露给客户端。
管理控制台
https://www.google.com/recaptcha/admin
控制台可以查看一些验证信息,例如请求数量之类的,很简单,自己看就懂。
文档
更多的细节,可以看看官方文档
创建一个SpringBoot应用
可以从 http://start.springboot.io/ 创建
创建过程就省略了,不需要任何第三方的依赖,基本的SpringBoot依赖就行。
把key配置在yml中
recaptcha:
client-key: "6LdvzboZAAAAALrLVyjabnd5xfb06izncgK0JXCt"
server-key: "6LdvzboZAAAAAE1mLwijdq9noxVUbWRBmqm-4Ava"
配置在yml中,程序可以灵活读取
Web客户端集成
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ReCaptcha V3</title>
<!-- render参数值就是前端密钥 -->
<script src="https://www.recaptcha.net/recaptcha/api.js?render=6LdvzboZAAAAALrLVyjabnd5xfb06izncgK0JXCt"></script>
</head>
<body>
<input name="name" placeholder="输入名字" id="name"/>
<button id="button">提交</button>
</body>
<script type="text/javascript">
// 前端密钥
const CLIENT_KEY = "6LdvzboZAAAAALrLVyjabnd5xfb06izncgK0JXCt";
// grecaptcha.ready => 验证码初始化成功后后回调
grecaptcha.ready(() => {
console.log('验证码初始化ok');
});
// 表单提交
document.querySelector('#button').addEventListener('click', () => {
// grecaptcha.execute => 生成Token
/**
第一个参数就是前端key
第二个参数是一个对象,action属性是一个自定义的“场景名称”。一个APP可以有N个验证场景,在后台可以查看不同场景下的验证数据。
第三方个参数是回调方法,Token就是形参,一般在这个方法里面发起请求
**/
grecaptcha.execute(CLIENT_KEY, {action: 'test'}).then((token) => {
// 构建请求体
const body = new URLSearchParams();
body.set('token', token); // 验证码回调token
body.set('name', document.querySelector('#name').value); // 表单数据
// 发起请求
fetch('/test', {
method: 'POST',
body: body
}).then(resp => {
if (resp.ok){
resp.json().then(message => {
if (message.success){
document.querySelector('#name').value = '';
} else {
alert('人机验证失败');
}
})
}
});
});
});
</script>
</html>
如果是模版引擎渲染,可以把配置文件中的前端key渲染到页面(这里直接在前端写死的)。
重点的几个东西
js库的加载
<script src="https://www.recaptcha.net/recaptcha/api.js?render={客户端key}"></script>
验证码初始化完成的回调,这不是必须的
grecaptcha.ready(() => {
console.log('验证码初始化ok');
});
通过执行execute方法,获取到Token
grecaptcha.execute("{客户端key}", {action: '{action}'}).then((token) => {
// 拿到token后,提交数据给服务器
});
服务器端的验证
验证的步骤
1. 使用Http客户端对远程服务器发起POST请求,有三个参数
https://www.recaptcha.net/recaptcha/api/siteverify
名称 | 说明 | 是否必须 |
---|---|---|
secret | 后端key | 是 |
response | 客户端生成的Token | 是 |
remoteip | 客户端ip | 否 |
2. 服务器响应
{
"success": true|false, // 此请求是否是站点的有效reCAPTCHA令牌
"score": number // 此请求的分数(0.0-1.0),人机判断的参考值。1 是人类,0是机器。
"action": string // 定义的验证场景
"challenge_ts": timestamp, // 加载的时间戳(ISO格式yyyy-MM-dd'T'HH:MM:ssZZ)
"hostname": string, // 使用reCAPTCHA的站点的主机名
"error-codes": [...] // 可选的错误代码
}
错误代码的说明
Error code | 说明 |
---|---|
missing-input-secret | secret参数丢失 |
invalid-input-secret | secret参数无效或格式错误 |
missing-input-response | 缺少响应参数 |
invalid-input-response | 响应参数无效或格式错误 |
timeout-or-duplicate | 响应不再有效:太旧或以前使用过。 |
3. 自己根据评分判断是否要放行
完整的代码
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@RestController
@RequestMapping("/test")
public class TestController {
private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
// 需要自己先把RestTemplate注册到IOC
@Autowired
private RestTemplate restTemplate;
// 从配置文件中读取到后端key
@Value("${recaptcha.server-key}")
private String serverKey;
// 请求地址
private static final String SITEVE_RIFY = "https://www.recaptcha.net/recaptcha/api/siteverify";
@PostMapping
public Object test (HttpServletRequest request,
@RequestParam("name") String name,
@RequestParam("token") String token) {
LOGGER.info("name={}, token={}", name, token);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
MultiValueMap<String, Object> requestBody = new LinkedMultiValueMap<>();
requestBody.add("secret", this.serverKey); // 服务端key
requestBody.add("response", token); // 客户端提交的token
requestBody.add("remoteip", request.getRemoteAddr()); // 客户的ip地址,不是必须的参数。
ResponseEntity<String> responseEntity = restTemplate.postForEntity(SITEVE_RIFY, new HttpEntity<>(requestBody, httpHeaders), String.class);
JsonObject jsonObject = JsonParser.parseString(responseEntity.getBody()).getAsJsonObject();
LOGGER.info("recaptcha response={}", jsonObject);
// 是否执行成功
if (!jsonObject.get("success").getAsBoolean()){
// 在失败的情况下,获取到异常状态码
JsonArray errorCodes = jsonObject.get("error-codes").getAsJsonArray();
LOGGER.error("recaptcha error={}", errorCodes);
return Collections.singletonMap("success", Boolean.FALSE);
}
// 评分
double score = jsonObject.get("score").getAsDouble();
if (score < 0.5) {
// 如果低于0.5分,服务不接受该请求
return Collections.singletonMap("success", Boolean.FALSE);
}
return Collections.singletonMap("success", Boolean.TRUE);
}
}
一次执行日志
i.s.web.controller.TestController : name=qwdqw, token=03AGdBq26J8YBZT6VuzU27VyuOk-KmKxN-UB6ETQ_MKOuDyea8upMmMBsX6H3TZ5NNK_VwgvJpxEJVppdmHNERK2d4Eo_w-YpxCi0TJmTyWJLRXD7279DScPOLxuRbj0nH_pTyYJw7OCf9o06gOeBQUqF7bCI_I4rakW4LvQSXd5d2jyFBdOf-FET6vqYzOYB93LyOsKcZdMci9YxIJ-9p8x_gm9YetFvyzQBt5il7iDHEqeLAd7HfLSh6UVOeDtHDncbkIgKWitHv4DuEO8_O8Pm7Fz6Sdc_GoAJgPeYAHkZs5vMvPqwv6H7hUKhh8RI-zCm3cYKe6nYK3Fc7Mc1Xr5bRnJqrSgrJkLBva4v2y-gSffm7E8GmtFgE9Kgr1iaualNUVzmYAiTx
i.s.web.controller.TestController : recaptcha response={"success":true,"challenge_ts":"2020-08-05T13:50:45Z","hostname":"localhost","score":0.9,"action":"test"}
隐私条款图标
使用这个版本的验证码,会在屏幕的右下角显示一个小图标。毕竟用了人家的东西,建议保留。
你非要隐藏它,可以添加一个css。
.grecaptcha-badge {
display: none;
}
最后
就是这么简单,为了通用。可以抽象出一个验证拦截器,再定义一个注解。通过注解描述接口最低允许的评分。在拦截器中获取到注解值,进行判断和校验。这样通用性和灵活性就提高了很多。