如何在React Native中对NodeJS OTP接口二次及以上请求触发reCaptcha验证
解决方案:基于请求计数的reCaptcha触发机制
一、判断用户是否第二次及以上调用API
核心思路是在服务端维护用户的请求计数(前端计数不可靠,易被篡改),通过用户唯一标识绑定请求次数,推荐用Redis存储(高效支持自增和过期策略)。
实现步骤:
- 确定用户唯一标识:
- 优先用请求中的手机号(OTP接口通常关联手机号);
- 若用户未登录,可使用
设备ID + IP地址的组合,避免同一IP下不同用户被误统计。
- 服务端计数逻辑:
- 每次请求进入时,对用户标识对应的Redis键做自增;
- 首次计数时设置过期时间(与OTP有效期一致,比如15分钟),避免长期无效计数占用存储。
代码示例(NodeJS + Redis):
const Redis = require('ioredis'); const redis = new Redis(process.env.REDIS_URL); // 封装请求计数检查函数 async function getOTPRequestCount(userIdentifier) { const redisKey = `otp:request:count:${userIdentifier}`; // 自增计数,原子操作避免并发问题 const requestCount = await redis.incr(redisKey); // 首次请求时设置过期时间 if (requestCount === 1) { await redis.expire(redisKey, 900); // 15分钟 = 900秒 } return requestCount; } // 在OTP接口中调用 app.post('/api/send-otp', async (req, res) => { const { phone } = req.body; // 用手机号作为用户标识 const requestCount = await getOTPRequestCount(phone); // 将计数挂载到请求对象,供后续逻辑使用 req.otpRequestCount = requestCount; // 继续后续OTP发送逻辑(暂时省略) });
二、仅在请求次数≥2时触发reCaptcha验证
需要前后端配合:服务端判断计数后告知前端是否需要验证,前端完成验证后携带token重新请求,服务端校验token有效性。
服务端逻辑:
- 当请求次数≥2时,返回
needCaptcha: true给前端,提示需要验证; - 前端携带captchaToken重新请求时,调用Google reCaptcha官方接口校验token有效性,校验通过后再发送OTP。
服务端验证reCaptcha代码示例:
// 封装reCaptcha验证函数 async function verifyRecaptchaToken(token) { const secretKey = process.env.RECAPTCHA_SECRET_KEY; try { const response = await fetch( `https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${token}`, { method: 'POST' } ); const result = await response.json(); return result.success; } catch (error) { console.error('reCaptcha验证失败:', error); return false; } } // 更新OTP接口逻辑 app.post('/api/send-otp', async (req, res) => { const { phone, captchaToken } = req.body; const requestCount = await getOTPRequestCount(phone); // 次数≥2时校验reCaptcha if (requestCount >= 2) { if (!captchaToken) { return res.status(400).json({ needCaptcha: true, message: '请完成人机验证后重试' }); } const isCaptchaValid = await verifyRecaptchaToken(captchaToken); if (!isCaptchaValid) { return res.status(400).json({ message: '人机验证失败,请重试' }); } } // 执行OTP发送逻辑 try { await sendOTPService(phone); // 自定义的OTP发送服务 res.json({ message: 'OTP已发送至您的手机' }); } catch (error) { res.status(500).json({ message: 'OTP发送失败,请稍后重试' }); } });
React Native前端实现:
使用适配移动端的reCaptcha库(如react-native-google-recaptcha),根据服务端返回的needCaptcha状态触发验证。
import React, { useState, useRef } from 'react'; import { View, TextInput, Button, Alert, StyleSheet } from 'react-native'; import Recaptcha from 'react-native-google-recaptcha'; const SendOTPScreen = () => { const [phoneNumber, setPhoneNumber] = useState(''); const [showCaptcha, setShowCaptcha] = useState(false); const recaptchaRef = useRef(null); const handleSendOTP = async () => { if (!phoneNumber.trim()) { Alert.alert('提示', '请输入手机号'); return; } try { const response = await fetch('https://your-api-domain.com/api/send-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: phoneNumber }) }); const data = await response.json(); if (data.needCaptcha) { setShowCaptcha(true); recaptchaRef.current?.open(); } else if (response.ok) { Alert.alert('成功', 'OTP已发送'); } else { Alert.alert('错误', data.message); } } catch (error) { Alert.alert('错误', '网络请求失败,请检查网络'); } }; const handleCaptchaSuccess = async (token) => { setShowCaptcha(false); try { const response = await fetch('https://your-api-domain.com/api/send-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: phoneNumber, captchaToken: token }) }); const data = await response.json(); if (response.ok) { Alert.alert('成功', 'OTP已发送'); } else { Alert.alert('错误', data.message); } } catch (error) { Alert.alert('错误', '验证请求失败'); } }; return ( <View style={styles.container}> <TextInput style={styles.input} placeholder="请输入手机号" value={phoneNumber} onChangeText={setPhoneNumber} keyboardType="phone-pad" /> <Button title="发送OTP" onPress={handleSendOTP} /> <Recaptcha ref={recaptchaRef} siteKey="your-recaptcha-site-key" onVerify={handleCaptchaSuccess} onExpire={() => Alert.alert('提示', '验证码已过期,请重新验证')} containerStyle={{ display: showCaptcha ? 'flex' : 'none' }} /> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, padding: 20, justifyContent: 'center' }, input: { height: 40, borderColor: '#ccc', borderWidth: 1, marginBottom: 20, paddingHorizontal: 10 } }); export default SendOTPScreen;
注意事项:
- 用户标识的安全性:避免仅用IP作为标识,防止同一IP下多个用户被误限制;
- Redis过期策略:过期时间需与OTP有效期匹配,避免用户间隔过久请求时计数仍有效;
- 密钥管理:reCaptcha的密钥和Redis连接信息需存储在环境变量中,禁止硬编码;
- 异常处理:服务端需处理reCaptcha验证接口的网络异常,避免因第三方服务不可用导致接口故障。
内容的提问来源于stack exchange,提问作者warCommander
相关产品推荐
相关产品推荐

