Spring Boot 下使用谷歌 reCAPTCHA v3

Posted sp42a

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot 下使用谷歌 reCAPTCHA v3相关的知识,希望对你有一定的参考价值。

JSP 时代,我写一个图片验证码组件《JSP 实用程序之简易图片验证码》,那是很老的技术,安全性很低,纯粹练手。

图片验证码(Captcha)这类应用,还是采用第三方提供的好,比较放心。于是我想起 Google 的不错,优点如下

  • 大厂出品,安全性高
  • reCAPTCHA v3 很牛逼,取消了用户互动交互,做到无感知验证
  • 免费

原来,我之前第一次用的时候,其实写过博文《免费使用 Google 防注册机验证》,不过就是偏向于前端的使用,今回我们看看怎么在 Spring Boot 下使用。

前期步骤参见旧文《免费使用 Google 防注册机验证》 即可。

配置类 GoolgeCaptachaConfig

配置 AppId 和密钥。

import com.ajaxjs.sdk_free.ClientAccessFullInfo;

/**
 * 谷歌验证码配置
 *
 * @author Frank Cheung<sp42@qq.com>
 */
public class GoolgeCaptachaConfig extends ClientAccessFullInfo 
    private Boolean enable;

    public Boolean isEnable() 
        return enable;
    

    public void setEnable(Boolean enable) 
        this.enable = enable;
    


/**
 * 客户端访问的基本两个字段: App Id、App 密钥
 * 
 * @author Frank Cheung<sp42@qq.com>
 *
 */
public abstract class ClientAccessFullInfo 
	/**
	 * App Id
	 */
	private String accessKeyId;

	/**
	 * App 密钥
	 */
	private String accessSecret;

	public String getAccessKeyId() 
		return accessKeyId;
	

	public void setAccessKeyId(String accessKeyId) 
		this.accessKeyId = accessKeyId;
	

	public String getAccessSecret() 
		return accessSecret;
	

	public void setAccessSecret(String accessSecret) 
		this.accessSecret = accessSecret;
	

注入:

/**
 * Captcha 配置
 * 
 * @return
 */
@Bean
GoolgeCaptachaConfig goolgeCaptachaConfig() 
	GoolgeCaptachaConfig g = new GoolgeCaptachaConfig();
	g.setEnable(true);
	g.setAccessKeyId("6LclfLM----------------------");
	g.setAccessSecret("6Lc-------------------------------");

	return g;

如果你想在 yml 中配置,也是可以的,

# 谷歌验证码
GoolgeCaptacha:
  accessKeyId: 6LclfLMZ-----------------
  accessSecret: 6LclfL-------------------

注入的改为:

@Value("$GoolgeCaptacha.accessKeyId")
private String goolgeCaptachaAccessKeyId;

@Value("$GoolgeCaptacha.accessSecret")
private String goolgeCaptachaAccessSecret;

/**
 * Captcha 配置
 * 
 * @return
 */
@Bean
GoolgeCaptachaConfig goolgeCaptachaConfig() 
	GoolgeCaptachaConfig g = new GoolgeCaptachaConfig();
	g.setEnable(true);
	g.setAccessKeyId(goolgeCaptachaAccessKeyId);
	g.setAccessSecret(goolgeCaptachaAccessSecret);

	return g;

之所以还保留 GoolgeCaptachaConfig,是因为 JSP 不知道怎么读取 yml,通过这个 bean 读取吧。

控制器的拦截器 GoogleCaptchaCheck/GoogleCaptchaMvcInterceptor

首先是前端页面,还是传统的 JSP 好用,

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"
	import="com.ajaxjs.util.spring.DiContextUtil, com.ajaxjs.security.google_captcha.GoolgeCaptachaConfig"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Google reCAPTCHA</title>
<%
	GoolgeCaptachaConfig g = DiContextUtil.getBean(GoolgeCaptachaConfig.class);
%>
<script
	src="https://www.recaptcha.net/recaptcha/api.js?render=<%=g.getAccessKeyId()%>"></script>
</head>
<body>
	<form>
		<input type="text" name="foo" />
		<button onclick="submitForm();return false;">提交</button>
	</form>
	<script type="text/javascript">
		function submitForm() 
            grecaptcha.ready(() => 
                grecaptcha.execute('<%=g.getAccessKeyId()%>',  action: 'submit' ).then((token) => 
                    // Add your logic to submit to your backend server here.
        			let xhr = new XMLHttpRequest();
        			xhr.open("POST", '/cms/msg', true);
        			xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");	// 发送合适的请求头信息
        			xhr.onload = function() 
        				console.log('请求完成')
        			;
        			
        			let value = document.querySelector('input[name=foo]').value;
        			xhr.send('foo=' + value +'&grecaptchaToken='+ token);
                );
            );
		
	</script>
</body>
</html>

在需要校验的 MVC 控制器某个方法中,加入这么一个注解 GoogleCaptchaCheck 就可以进行拦截。

控制器:

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
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.ResponseBody;

import com.ajaxjs.framework.BaseController;
import com.ajaxjs.security.google_captcha.GoogleCaptchaCheck;

@Controller
@RequestMapping("/msg")
public class MsgController 
	@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE + ";charset=utf-8")
	@GoogleCaptchaCheck
	@ResponseBody
	public String create(@RequestParam String foo) 
		System.out.println(foo);

		return BaseController.jsonOk("创建 Msg 成功");
	

注解源码:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 防止 Captcha
 * 
 * @author Frank Cheung<sp42@qq.com>
 *
 */
@Target( ElementType.METHOD, ElementType.TYPE )
@Retention(RetentionPolicy.RUNTIME)
public @interface GoogleCaptchaCheck 

MVC 肯定得有个拦截器呀:

import java.lang.reflect.Method;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * 
 * @author sp42 frank@ajaxjs.com
 *
 */
public class GoogleCaptchaMvcInterceptor implements HandlerInterceptor 
	@Autowired
	private GoogleFilter googleFilter;

	@Override
	public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) 
		if (handler instanceof HandlerMethod) 
			HandlerMethod handlerMethod = (HandlerMethod) handler;
			Method method = handlerMethod.getMethod();

			if (method != null) 
				String httpMethod = req.getMethod();

				if (("POST".equals(httpMethod) || "PUT".equals(httpMethod)) && method.getAnnotation(GoogleCaptchaCheck.class) != null) 
					// 有注解,要检测
					System.out.println("开始检测");

					if (googleFilter.check(req))
						return true;

					return false;
				
			
		

		return true;
	

怎么注册这个拦截器呢?在 Spring 的 WebMvcConfigurer 中注入 GoogleCaptchaMvcInterceptor 的 bean,然后再注册 addInterceptors(InterceptorRegistry registry)

/**
 * 拦截器
 * 
 * @return
 */
@Bean
GoogleCaptchaMvcInterceptor googleCaptchaMvcInterceptor() 
	return new GoogleCaptchaMvcInterceptor();


/**
 * 加入拦截器
 */
@Override
public void addInterceptors(InterceptorRegistry registry) 
	registry.addInterceptor(googleCaptchaMvcInterceptor());
	super.addInterceptors(registry);

核心校验器 GoogleFilter

核心校验逻辑在 GoogleFilter

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

import com.ajaxjs.framework.BaseController;
import com.ajaxjs.net.http.Post;


/**
 * 校验核心
 * 
 * @author sp42 frank@ajaxjs.com
 *
 */
public class GoogleFilter 
	@Autowired
	private GoolgeCaptachaConfig cfg;

	/**
	 * 校验表单时候客户端传过来的 token 参数名
	 */
	public final static String PARAM_NAME = "grecaptchaToken";

	/**
	 * 谷歌校验 API
	 */
	private final static String SITE_VERIFY = "https://www.recaptcha.net/recaptcha/api/siteverify";

	/**
	 * 校验
	 *
	 * @return 是否通过验证,若为 true 表示通过,否则抛出异常
	 */
	public boolean check() 
		return check(BaseController.getRequest());
	

	/**
	 * 校验
	 *
	 * @param request 请求对象
	 * @return 是否通过验证,若为 true 表示通过,否则抛出异常
	 */
	public boolean check(HttpServletRequest request) 
		return check(request.getParameter(PARAM_NAME));
	

	/**
	 * 
	 * @param token
	 * @return
	 */
	public boolean check(String token) 
		if (!cfg.isEnable())
			return true;

		if (!StringUtils.hasText(token))
			throw new SecurityException("非法攻击!客户端缺少必要的参数");

		Map<String, Object> map = Post.api(SITE_VERIFY, String.format("secret=%s&response=%s", cfg.getAccessSecret(), token.trim()));

		if (map == null)
			throw new IllegalAccessError("谷歌验证码服务失效,请联系技术人员");

		if ((boolean) map.get("success")) // 判断用户输入的验证码是否通过
			if (map.get("score") != null) 
				// 评分0 到 1。1:确认为人类,0:确认为机器人
				double score = (double) map.get("score");

				if (score < 0.5)
					throw new SecurityException("验证码不通过,非法请求");
			

			return true;
		 else 
			if ("timeout-or-duplicate".equals(map.get("error-codes")))
				throw new NullPointerException("验证码已经过期,请刷新");

			throw new SecurityException("验证码不正确");
		
	

校验通过,结果如下

其他问题

隐藏 reCAPTCHA 图标

使用 reCAPTCHA,会在网站上提示出一个图标.。

如果需要隐藏,可以添加 CSS。

.grecaptcha-badge  
	display: none; 
 

依赖 js 过大

我的妈呀,300 多 k~

Vue SPA 下怎么使用

待续……

以上是关于Spring Boot 下使用谷歌 reCAPTCHA v3的主要内容,如果未能解决你的问题,请参考以下文章

调用链系列Zipkin搭建Spring-boot集承

带有 Google 的 OAuth2 - CORS 错误(Angular + Spring boot)[重复]

如何使用Spring Boot和嵌入式Tomcat配置此属性?

Spring Boot 返回字符串而不是视图

如何使用hibernate在spring boot中实现分页

spring boot系列01--快速构建spring boot项目