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 下怎么使用

待续……

Update

recaptcha.net 用国内 dns 解析的话,会是到 google 北京的服务器。
强制指定 DNS IP 即可:vi /etc/hosts,加入

203.208.40.2    www.recaptcha.net

即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值