• 在SpringBoot App中使用GoogleReCaptcha3过滤非法的请求


    现在的应用中对于登录,注册,短信验证码。。。这些场景来说,验证码真的是必不可少。随着技术的发展,也使得验证码从当初的图形验证码,发展到今天的滑块,倒立文字点击,数学计算,手势滑动,拼图,刮图。。。等等各种花样,总之一个目的,阻止机器人的访问

    验证码这玩意儿,确实给用户带来了很不好的体验,很多应用的验证码确实玄乎的很让人让人抓狂。

    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

    控制台可以查看一些验证信息,例如请求数量之类的,很简单,自己看就懂。

    文档

    https://developers.google.com/recaptcha/intro

    更多的细节,可以看看官方文档

    创建一个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; 
    } 
    

    最后

    就是这么简单,为了通用。可以抽象出一个验证拦截器,再定义一个注解。通过注解描述接口最低允许的评分。在拦截器中获取到注解值,进行判断和校验。这样通用性和灵活性就提高了很多。


    原文:https://springboot.io/t/topic/2365

  • 相关阅读:
    假期周进度报告02
    假期周进度报告01
    浪潮之巅阅读笔记6
    浪潮之巅阅读笔记5
    浪潮之巅阅读笔记4
    科技创新平台年报系统利益相关者分析
    浪潮之巅阅读笔记3
    浪潮之巅阅读笔记2
    浪潮之巅阅读笔记1
    Linux Redis 重启数据丢失解决方案,Linux重启后Redis数据丢失解决方
  • 原文地址:https://www.cnblogs.com/kevinblandy/p/13443200.html
Copyright © 2020-2023  润新知