在SpringBoot和Vue3中使用Google的reCaptchaV3

发布时间 2023-12-16 13:48:57作者: JessieLin

Google的RecaptchaV3是一种对用户很友好的验证码模型,接下来从0开始介绍RecaptchaV的使用。
首先是需要去google的官网申请reCaptchaV3(google账号和访问方式请自备,这里不描述)
https://www.google.com/recaptcha/about/
点击右上角的getStarted(顶上那个Admin Console可以进入管理后台)

然后创建项目,名字随便起

选择v3版本然后确定,之后记得保存页面上两个密钥,一个是siteKey(在网站上用),一个后端的key(后端向google请求)

点击设置图标进入设置

添加自己的域名,如果需要在本地测试,记得添加localhost。生产环境请使用新的项目,勿添加localhost。

然后记得滑到底下点保存!不然不会记录,我就被这个卡了一会。
此时google部分的申请就已经完成。
之后是前后端集成。
这里先简单描述下reCaptchaV3工作流程:
1、前端导入google提供的js,这个js会自动分析用户的行为来判断是否机器人,调用该js的方法(需要使用上面的siteKey)会得到一个token,把这个token手动发送给后端
2、后端拿到token后,把该token和后端用的key发给google的服务器来得到分析的结果,结构大致如下:

{
  "success": true|false,      // whether this request was a valid reCAPTCHA token for your site
  "score": number             // the score for this request (0.0 - 1.0)
  "action": string            // the action name for this request (important to verify)
  "challenge_ts": timestamp,  // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
  "hostname": string,         // the hostname of the site where the reCAPTCHA was solved
  "error-codes": [...]        // optional
}

其中success表示请求成功或者失败,score是一个0-1之间的小数,越接近1代表越可能是真人,反之则可能是机器人或脚本
3、根据上面的score得分情况,决定用户的请求可以继续,或被拒绝。
详细的流程,在google的文档中已经有详尽描述
https://developers.google.com/recaptcha/docs/v3?hl=zh-cn

在Vue3中集成:
(注意:本人使用TS环境)
使用vue-recaptcha-v3来进行集成,以下是官网
https://www.npmjs.com/package/vue-recaptcha-v3?activeTab=readme
https://github.com/AurityLab/vue-recaptcha-v3
1、引入
npm install vue-recaptcha-v3
2、配置
main,.ts中添加如下内容(注意顺序)

//导入
import { VueReCaptcha, useReCaptcha } from 'vue-recaptcha-v3'
//创建app和其他内容
app.use(VueReCaptcha,{siteKey:'这里填你自己的',loaderOptions:{
    useRecaptchaNet: true,
    autoHideBadge: false
}})

这里的loaderoptions是配置项,可以为空,具体配置如下

Name Description Type Default value
useRecaptchaNet Due to limitations in certain countries it's required to use recaptcha.net instead of google.com. boolean false
useEnterprise Uses the enterprise version of the recaptcha api and handles the differences in the response. boolean false
autoHideBadge Will automatically hide the reCAPTCHA badge. Warning: The usage is only allowed if you follow the offical guide for hiding the badge from Google (see here) boolean false
renderParameters Will add the given parameters to the reCAPTCHA script. The given object will be converted into a query string and will then be added to the URL. Object {}
explicitRenderParameters Will set the parameters to the explicit rendering. See here Object {}

其中的useRecaptchaNet会替换Google.com为recaptcha.net 以方便国内访问,请国内的小伙伴务必配置这个。
在Java中进行配置:注意修改包名,另外使用了Okhttp,请引入Okhttp的依赖或改为你用的(如HttpClient)

 <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.10.0</version>
        </dependency>
package com.jessie.expressdeliverysystem.config;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jessie.expressdeliverysystem.controller.UserController;
import com.jessie.expressdeliverysystem.utils.OkRestClient;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.jessie.expressdeliverysystem.controller.UserController;


/**
 * 校验核心,做了一些修改
 * @author JessieLin
 * @author sp42 frank@ajaxjs.com
 * 
 */
@Component
public class GoogleFilter {
    @Autowired
    GoogleCaptchaConfig 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(UserController.getRequest());
//    }

    /**
     * 校验
     *
     * @param request 请求对象
     * @return 是否通过验证,若为 true 表示通过,否则抛出异常
     */
    public boolean check(HttpServletRequest request)  {
        Boolean res=false;
        try{
            res= check(request.getParameter(PARAM_NAME));
        }catch (IOException e){
            e.printStackTrace();
        }
        return res;

    }

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

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

        Map<String,String> param=new HashMap<>();
        param.put("secret", cfg.getAccessSecret());
        param.put("response",token);
        OkHttpClient client = new OkHttpClient();
        RequestBody body = new FormBody.Builder()
                .add("secret", cfg.getAccessSecret())
                .add("response",token)
                .build();
        Request req = new Request.Builder()
                .url(SITE_VERIFY)
                .post(body)
                .build();
        //同步请求
        Call call = client.newCall(req);
        Response response = call.execute();
        System.out.println("返回码:"+response.code());
        String s=response.body().string();
//        System.out.println(s);//?空的?
//        Map<String, Object> map = Post.api(SITE_VERIFY, String.format("secret=%s&response=%s", cfg.getAccessSecret(), token.trim()));
        JSONObject jsonObject= JSON.parseObject(s);


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

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

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

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


配置文件、

package com.jessie.expressdeliverysystem.config;

import lombok.Getter;

@Getter
public abstract class ClientAccessFullInfo {
    /**
     * App Id
     */
    private String accessKeyId;

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

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

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

}
package com.jessie.expressdeliverysystem.config;



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

    public Boolean isEnable() {
        return enable;
    }

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




以上部分即为校验的核心,可以通过编写拦截器的形式来触发拦截

package com.jessie.expressdeliverysystem.config;

import java.io.IOException;
import java.lang.reflect.Method;

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

import com.alibaba.fastjson.JSONObject;
import com.jessie.expressdeliverysystem.domain.Result;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 *
 * @author sp42 frank@ajaxjs.com
 *
 */
@Component
@Slf4j
public class GoogleCaptchaMvcInterceptor implements HandlerInterceptor {

    @Autowired
    GoogleFilter googleFilter;

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            String httpMethod = req.getMethod();
            if (("POST".equals(httpMethod) || "PUT".equals(httpMethod)) && method.getAnnotation(GoogleCaptchaCheck.class) != null) {
                // 有注解,要检测
                log.info("正在进行reCaptcha检测");
                boolean result=googleFilter.check(req);
                if(!result){
                    try{
                        ServletOutputStream outputStream = resp.getOutputStream();
                        resp.setContentType("application/json;charset=UTF-8");
                        outputStream.write(Result.error("人机验证无法通过,请重试").toString().getBytes("UTF-8"));
                    }catch (IOException e){
                        e.printStackTrace();
                    }


                }
                return result;
            }
        }

        return true;
    }
}

package com.jessie.expressdeliverysystem.config;

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 {
}

最后记得在WebMvcConfig中注册拦截器

@Autowired
    GoogleCaptchaMvcInterceptor googleCaptchaMvcInterceptor;
@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(googleCaptchaMvcInterceptor);//这里必须是托管给spring的
        WebMvcConfigurer.super.addInterceptors(registry);
    }

然后就配置完成了,在POST方法上加上@GoogleCaptchaCheck 即可触发拦截器

    @PostMapping("/login")
    @GoogleCaptchaCheck
    public Result Login(User user) {
       /*
          在这里处理登录的逻辑
       */
    }

前端请求

import { useReCaptcha } from "vue-recaptcha-v3";
const recaptcha1=useReCaptcha()
const recaptcha = async () => {
      console.log("验证码正在验证。。。")
      await recaptcha1?.recaptchaLoaded()
      const token = await recaptcha1?.executeRecaptcha('login')
      console.log(token)
      gRecaptchaToken.value=token as string
      return gRecaptchaToken.value;
    }

调用时:

 async loginHandle() {
      const captchaToken= await this.recaptcha() //这里调用recaptcha的方法
      let param = new FormData();
      param.append("username", username);
      param.append("password", password);
      param.append("role", role);
      param.append("gRecaptchaToken",captchaToken);//然后将返回的token作为参数发给后端
      service.post("/api/user/login", param)
        .then((res) => {
          console.log(res);
          if (res.data.code != 200) {
            ElMessage.error(res.data.msg)
            return;
          }
          console.log(res.data.data);
          sessionStorage.setItem("token", res.data.data.token);
          service.defaults.headers.common["token"] = res.data.data.tokenValue;
          //做页面跳转
          router.push("/home")
        })
        .catch((err) => {
          ElMessage.error(err)
          console.log(err)
        })

    },

好了,这样就完成了一次调用,在后端中若正确接收到参数,会得到如下信息

返回码:200
{
  "success": true,
  "challenge_ts": "2023-12-16T03:33:26Z",
  "hostname": "localhost",
  "score": 0.9,
  "action": "login"
}

根据score进行判断即可。

该代码后续还有值得完善的地方,等后面有空再说吧。有需要的小伙伴看一下也都知道要怎样修改比较好了。
参考文章:
https://blog.csdn.net/zhangxin09/article/details/123672905
https://blog.51cto.com/jackiehao/7384025
https://juejin.cn/post/6944966415217033247