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
即可