React+Express对接Twitter OAuth1.0a返回215错误问题求助
Twitter OAuth 1.0a 接口返回215错误排查与修复
Twitter接口返回215错误的核心原因是鉴权签名校验不通过,你的代码存在多处不符合OAuth 1.0a规范的实现问题,具体如下:
- 密钥赋值错误:
oauth_consumer_key_secret错误读取了TWITTER_CONSUMER_KEY,应当读取对应消费者密钥环境变量TWITTER_CONSUMER_SECRET - 时间戳格式错误:OAuth 1.0a要求
oauth_timestamp为Unix秒级纯数字时间戳,你使用date.toUTCString()生成的GMT格式时间字符串完全不符合规范 - 签名基串构造错误:
- 基串内的请求URL必须为完整接口地址(即
https://api.twitter.com/oauth/request_token),你仅传入了不带路径的根域名 - 参与签名的所有参数必须先做RFC3986规范URL编码,再按照参数名ASCII码升序排列拼接,你直接拼接参数既未编码也未排序
- 你发起请求时在URL Query中额外追加了
oauth_consumer_key参数,该参数未纳入签名参数集合,会直接导致签名不匹配
- 基串内的请求URL必须为完整接口地址(即
- 签名密钥拼接错误:调用
/request_token属于获取未授权请求令牌阶段,此时还没有用户授权对应的oauth_token_secret,签名密钥的后半段应为空字符串,不能提前拼接应用全局的Access Token Secret - 签名值未编码:生成的
oauth_signature在放入Authorization头前必须做RFC3986 URL编码,原始base64值包含的+/=等特殊字符会被服务端解析错误 - 流程逻辑错误:
/request_token阶段不需要传入oauth_token参数,该参数是用户授权完成后兑换访问令牌阶段才需要的参数,提前传入固定的Access Token会直接打乱签名计算逻辑
修复后可直接运行的代码
const crypto = require('crypto'); const needle = require('needle'); // RFC3986规范URL编码实现 const rfc3986Encode = (str) => { return encodeURIComponent(str) .replace(/!/g, '%21') .replace(/'/g, '%27') .replace(/\(/g, '%28') .replace(/\)/g, '%29') .replace(/\*/g, '%2A'); }; const verifyTwitter = async(req,res) => { const API_BASE_URL = process.env.TWITTER_BASE_URL; // https://api.twitter.com/oauth const oauth_consumer_key = process.env.TWITTER_CONSUMER_KEY; const oauth_consumer_secret = process.env.TWITTER_CONSUMER_SECRET; const ACTION = '/request_token'; const requestUrl = `${API_BASE_URL}${ACTION}`; // 生成随机nonce const nonceLen = 32; const oauth_nonce = crypto.randomBytes(Math.ceil(nonceLen * 3 / 4)) .toString('base64') .slice(0, nonceLen) .replace(/\+/g, '0') .replace(/\//g, '0'); const method = 'POST'; // 生成秒级Unix时间戳 const oauth_timestamp = Math.floor(Date.now() / 1000).toString(); const oauth_signature_method = 'HMAC-SHA1'; const oauth_version = '1.0'; // 收集request_token阶段参与签名的参数,无需传入oauth_token const signParams = { oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_version }; // 参数按ASCII码升序排序后拼接 const sortedParamKeys = Object.keys(signParams).sort(); const paramStr = sortedParamKeys .map(key => `${rfc3986Encode(key)}=${rfc3986Encode(signParams[key])}`) .join('&'); // 构造签名基串,三个组成部分分别做URL编码 const signatureBaseStr = [ method.toUpperCase(), rfc3986Encode(requestUrl), rfc3986Encode(paramStr) ].join('&'); // request_token阶段签名密钥的token_secret部分为空 const signKey = `${rfc3986Encode(oauth_consumer_secret)}&`; // 生成签名并做URL编码 const oauth_signature = rfc3986Encode( crypto.createHmac('sha1', signKey).update(signatureBaseStr).digest('base64') ); // 构造规范的Authorization头,不在URL中额外追加OAuth参数 const authHeader = `OAuth oauth_consumer_key="${rfc3986Encode(oauth_consumer_key)}", oauth_nonce="${rfc3986Encode(oauth_nonce)}", oauth_signature_method="${rfc3986Encode(oauth_signature_method)}", oauth_timestamp="${rfc3986Encode(oauth_timestamp)}", oauth_version="${rfc3986Encode(oauth_version)}", oauth_signature="${oauth_signature}"`; try { const oAuthResponse = await needle('post', requestUrl, {}, { headers: { authorization: authHeader } }); console.dir(oAuthResponse.body); console.log(oAuthResponse.headers); return res.status(200).send(oAuthResponse.body); } catch (err) { return res.status(500).send({ error: err.message }); } }
后续对接访问令牌兑换接口时,再将拿到的
oauth_token和对应oauth_token_secret加入签名参数集合、拼接到签名密钥后半段即可,不要在请求令牌阶段提前传入应用全局的Access Token。
内容的提问来源于stack exchange,提问作者Justin Seo
相关产品推荐
相关产品推荐

