react使用谷歌人机验证

发布于:2024-05-04 ⋅ 阅读:(30) ⋅ 点赞:(0)

在项目中,需要对请求验证,防止被爆破,这里使用的是谷歌的recaptcha-v3。

1.申请谷歌人机验证的api

申请链接,申请完后需要将两个谷歌颁发的key分别写入前,后端的配置环境中,后面会使用.

 2.前端部分

 前端使用的是vite+CRA框架,先放出配置.

vite全局环境配置(vite.config.js)(环境配置记录,验证请看下面).
import { defineConfig, loadEnv } from 'vite'
import ViteTestConfig from './config/vite.test.config.js'
import ViteBaseConfig from './config/vite.base.config.js'


// 策略模式做一个动态的配置
const envResolver = {
  "build": () => {
    console.log("生产环境")
    return ({ ...ViteTestConfig, ...ViteBaseConfig })
  },
  "serve": () => {
    console.log("开发环境")

    // 另一种写法
    // return Object.assign({}, ViteBaseConfig, ViteDevConfig)
    return ({ ...ViteTestConfig, ...ViteBaseConfig })
  }

}


// https://vitejs.dev/config/
export default defineConfig(
  ({ command, mode }) => {
    const env = loadEnv(mode, process.cwd(), "")
    console.log("env : ", env)

    // 根据不同的环境使用不同的配置文件,注意这个地方的写法,非常的奇特
    return envResolver[command]()
  }
)

这里使用动态配置vite的环境,vite的config文件,当前使用的是本地开发环境使用的是vite.test.config.js,其他环境按这样引用就好了,环境配置比如host,prot,dir等等都在自己需要的环境config中配置就行了.

同时还需要配置环境变量,环境变量使用.env文件,这里我创建了个env文件夹然后将环境变量都放进去了,需要在vite的环境配置中告诉其环境变量文件位置.

import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
  plugins: [react()],
  envDir: 'src/env',
})

同时也分各种不同的环境变量配置,格式为:.env.(环境名称) 例如:..env.development

而.env则是默认的环境变量,也可以理解为全局变量,不管你加不加载都会读取其中的变量.

而vite中的变量命名必须以 VITE_ 开头,否则不会读取

例子:

VITE_TEST_BASE_URL="http://127.0.0.1:8080/api"

在react中读取变量(react数据共享)

react讲究一个纯函数,所以变量声明只能在其组件中使用,这就有一个问题,当我们使用主题或者存储用户token的时候就不能在所有组件中共享,如果一个个传递下去非常麻烦,所以需要使用到react提供的useContext.

1.创建一个context载体组件

// AppContext.js
import React from 'react'

const AppContext = React.createContext()

export default AppContext

2.在父组件中使用

function App() {
// 主题模式
  const [styleMode, setStyleMode] = React.useState('light')


//改变页面主题
  const changeMode = () => {
    const newMode = styleMode === 'dark' ? 'light' : 'dark'
    setStyleMode(newMode)
    localStorage.setItem('theme', newMode)
  }

   //主题
  const customTheme = createTheme({
    palette: {
      mode: styleMode,
    },
  })

<ThemeProvider theme={customTheme}>
        <AppContext.Provider
          value={{ mode: styleMode, changeMode }}>
          {ElementRouter}
        </AppContext.Provider>
    </ThemeProvider>
}

使用该方式可以将变量在被包裹的组件中使用,同时也可以传递函数,对父组件中的数据进行修改。

3.子组件调用

直接使用useContext(载体组件名)即可获得变量

//获取全局变量

const { mode, changeMode } = useContext(AppContext)

//获取环境变量

const variable= import.meta.env.定义的环境变量名

在前端引入recaptcha v3

这里使用封装好的组件引入,npm安装

npm install react-google-recaptcha-v3

v2的话需要自己去找一下引入方式,此方法只适合v3.

引入后在需要引入谷歌验证的父组件上对子组件进行包裹,这里我直接放在APP组件上,直接全部使用了.

function App{

 return (
    <ThemeProvider theme={customTheme}>
      <GoogleReCaptchaProvider
        reCaptchaKey={import.meta.env.VITE_GOOGLE_CAPCHAT_KEY}
        //国内谷歌验证
        useRecaptchaNet={true}>
        <AppContext.Provider
          value={{ mode: styleMode, baseUrl: baseUrl, changeMode }}>
          {ElementRouter}
        </AppContext.Provider>
      </GoogleReCaptchaProvider>
    </ThemeProvider>
  )
}

key就是你申请到的前端key(client),useRecaptchaNet一定要开,不开的话会去访问谷歌的verifty,局域网内是无法访问的.

使用的话v3使用的是一个无感验证,不需要用户去进行图片或者人机验证点击,所以直接封装一个验证逻辑后放入代码中就好了(比如提交表单的时候或者点击某些按钮的时候等等)

import { requestSlimpePost } from "./RequestUtils"

async function checkRobot (executeRecaptcha, baseUrl) {
  //人机验证不可用情况
  if (!executeRecaptcha) {
    alert('人机验证不可用,请检查网络!')
    return false
  }

  //获得验证token
  const capChatToken = await executeRecaptcha()

  //获取token失败
  if (!capChatToken) {
    alert('人机验证失败!')
    return false
  }

  //封装成json数据
  const data = {
    capChatToken: capChatToken,
  }
  const jsonData = JSON.stringify(data, null, 2)


  //获取返回数据
  const res = await requestSlimpePost(baseUrl + '/verify/recapchat', jsonData)
  if (res.msg != 'ok' || res.code != 200) {
    alert('人机验证失败!')
    console.log(res)
    return false
  }

  return true
}

export { checkRobot }

其中requestSlimpePost是我自己封装的请求工具,就使用fetch就不过多介绍了.

 const { executeRecaptcha } = useGoogleReCaptcha()
  const { baseUrl } = React.useContext(AppContext)

  const handleSubmit = async (event) => {
    event.preventDefault()

    //获取当前登录数据
    const userInputData = new FormData(event.currentTarget)
    const userName = userInputData.get('userName')
    const passwd = userInputData.get('passwd')

    if (!userName) {
      alert('用户名不能为空')
      return
    }

    if (!passwd) {
      alert('密码不能为空')
      return
    }

    const notRobot = await checkRobot(executeRecaptcha, baseUrl)
    if (!notRobot) return

  }

先通过封装的包中获取executeRecaptcha这个对象,然后通过调用其中函数向谷歌验证申请验证令牌,拿到后再通过后端携带该令牌去进行评测.

3.后端部分

在前端获取验证令牌后,需要向后端申请验证,后端则向谷歌申请该次人机验证的评分,并进行逻辑操作.

/**
 * @author Vermouth
 * @ClassName: ReCapChatManager
 * @Description: 人机校验管理器
 * @Date 2024/4/10 11:05
 * @Version: V1.0
 */
@Component
public class ReCapChatManager {
    // 请求地址
    private static final String SITEVE_RIFY = "https://www.recaptcha.net/recaptcha/api/siteverify";
    private static OkHttpClient httpClient = new OkHttpClient();

    @Autowired
    private ReCapIpManager reCapIpManager;

    // 从配置文件中读取到后端key
    @Value("${recaptcha.server-key}")
    private String serverKey;


    /**
     * 人机校验
     *
     * @param request 请求包装类
     * @return true / false
     * @throws IOException
     */
    public boolean robotCheck(HttpServletRequest request) throws IOException {

        //请求的IP
        String remoteAddr = RequestUtils.getClientIp(request);
        String requestJson = StringUtils.bufferToString(request.getReader());
        JsonObject requestObj = JsonParser.parseString(requestJson).getAsJsonObject();

        //异常的请求也驳回
        if (null == requestObj) {
            this.addFreezeIp(remoteAddr);
            return false;
        }

        //请求体设置
        RequestBody formBody = new FormBody.Builder().add("secret", this.serverKey)  //服务端key
                .add("response", requestObj.get("capChatToken").getAsString())   //客户端提交的token
                //.add("remoteip", remoteAddr)  //客户的ip地址,不是必须的参数。
                .build();

        //请求头设置
        Headers reqHeaders = new Headers.Builder().add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=UTF-8").add(HttpHeaders.CONTENT_LENGTH, String.valueOf(formBody.contentLength())).build();


        //装入请求中
        Request reRequest = new Request.Builder().url(SITEVE_RIFY).headers(reqHeaders).post(formBody).build();

        Call call = httpClient.newCall(reRequest);
        String responseData = null;

        try {
            //发起请求
            Response googleResponse = call.execute();

            //检查是否获取到响应体
            if (null == googleResponse.body()) throw new IOException();
            responseData = googleResponse.body().string();

        } catch (IOException e) {
            throw new SystemException(ExceptionStatus.OKHTTP_CALL_EXCEPTION_);
        }


        JsonObject jsonObject = JsonParser.parseString(responseData).getAsJsonObject();

    //TODO 后续有了服务器还需接入Cloudflare!!!!

        // 是否执行成功
        if (!jsonObject.get("success").getAsBoolean()) {

            // 在失败的情况下,获取到异常状态码
            JsonArray errorCodes = jsonObject.get("error-codes").getAsJsonArray();
            this.addFreezeIp(remoteAddr);
            return false;
        }

        //获取评分
        double score = jsonObject.get("score").getAsDouble();
        if (score < 0.5) {

            // 如果低于0.5分,服务不接受该请求
            this.addFreezeIp(remoteAddr);
            return false;
        }

        return true;
    }


    /**
     * 冻结ip操作
     *
     * @param ipAddress 需冻结ip
     */
    public void addFreezeIp(String ipAddress) {
        reCapIpManager.freezeIp(ipAddress);
    }


}

需要注意的是请求地址,请求地址同样也必须是国内的谷歌验证api,否则访问不到.

一般来说打分都是在0.9,我自己测试的都是在0.9,可以根据自己的情况来调整判断人机的分数.


网站公告

今日签到

点亮在社区的每一天
去签到