QOAuth1使用和Twitter签名API

鉴于工作需要,开发Twitter的直播API,需要使用OAuth1,因此总觉出一些问题

首先,Oauth1是一种签名协议,主要保证了用户的授权是安全的,可以追溯到的,具体可以查询,不详细说了。

目前使用的是QT的QAuth1对象,实现了发送http和签名,但是Qt的签名再发送带body的时候,生成的签名和postman生成结果不一致,也证明确实有问题,我也找到办法解决,后续会单独说明。

好了开始介绍代码。

首先一些命名和声明,方便后续管理使用

class QNetworkReply;
class QOAuth1;
class QNetworkRequest;
using OAuPtr = QSharedPointer<QOAuth1>;

using TaskFun = std::function<void()>;
using TaskContainer = QMap<int, TaskFun>;

构造用于第一步签名的auth对象

void updatePreAuth()
{
	if (!mPreloginAu) {
		mPreloginAu = OAuPtr::create();
		mPreloginAu->moveToThread(this->thread());
	}
	QString key = getAPIKey();
	QString secret = getAPISecret();
	mPreloginAu->setClientCredentials(key, secret);
	mPreloginAu->setSignatureMethod(QOAuth1::SignatureMethod::Hmac_Sha1);
}

上述代码,对象使用智能指针包裹,设置了来自我们开发人员申请的密钥对,注意此处是开发人员申请,twitter给的密钥对。按照twitter的要求,使用的签名算法是hmac.

接下来是登录准备,按照授权要求,登录之前需要获取开发者签名


void prelogin()
{
	QFile::remove(twitter_tmp_bin);
	updatePreAuth();
	QPointer reply = mPreloginAu->post(QUrl(twitter_auth_token_url));
	auto handleResult = [reply, this]() {
		auto finishWork = qScopeGuard([this]() { emit this->preloginFinished(); });

		if (!checkError(reply)) {
			return;
		}

		QString data = reply->readAll().simplified();
		
		auto tmpMap = queryStringToMap(data);
		addToMap(myLastInfo(), tmpMap);
		toShareData();
	};
	connect(reply, &QNetworkReply::finished, this, handleResult, Qt::QueuedConnection);
}

上述代码中,我个人因为业务需求,临时存储了网页返回的数据为文本,将数据转换成json村再本地,方便后续浏览器读取。(Qt又demo如何获取并且解析返回值,但是我这边因为使用了其他的浏览器框架插件,所以只能先暂存)

返回值是一行字符串,可以使用qt的对象解析

QVariantMap queryStringToMap(const QString &srcStr)
{
	QUrlQuery query(srcStr);
	QVariantMap varMap;
	auto items = query.queryItems();
	for (const auto &item : items) {
		varMap.insert(item.first, item.second);
	}
	return varMap;
}

上面解析到大概三个值

oauth_token=cDjNLgAAAaaAAA5LplAAABghFHw8Q&oauth_token_secret=V4U96aa2c8dPxsuYbMsvT7rN1Ne2CYYLfI&oauth_callback_confirmed=true

就上面三个键值对,我这里存成map

const auto twitter_login_url = "https://api.twitter.com/oauth/authorize";

QUrl xxxTwitterLoginInfo::getLoginUrl() const
{
	QVariantMap tmpInfo;
	QString cahchePath = pls_get_user_path(QCoreApplication::applicationName() + QDir::separator() + "Cache/" + QString(TWITTER));
	loadDataFromFile(tmpInfo, cahchePath);
	mResult = QJsonObject::fromVariantMap(tmpInfo);
	QUrl ret(twitter_login_url);
	auto auToken = mResult.value(ChannelData::oauth_token).toString();
	QUrlQuery query;
	query.addQueryItem(ChannelData::oauth_token, auToken);
	ret.setQuery(query);
	qDebug() << " twitter url " << ret;
	return ret;
}

上述函数,是因为之前存储了第一步开发者授权签名返回值,现在需要把授权信息带入url,交给网页登录

接下来就是网页登录了


auto redirect_key = "your url string";
bool xxxxxxxx::channelLoginWithAccount(QJsonObject &result, QWidget *parent) const
{
	bool bResult = false;
	auto check = [this](QJsonObject &result_, const QString &url, const QMap<QString, QString> &) {
		qDebug() << " redirect url " << url;
		if (!url.contains(redirect_key)) {
			return xxxResultCheckingResult::Continue;
		}
		QUrl urlo(url);
		QUrlQuery query(urlo);
		if (!query.isEmpty()) {
			auto items = query.queryItems();
			for (auto item : items) {
				this->mResult.insert(item.first, item.second);
			}
			result_ = this->mResult;
			return xxxResultCheckingResult::Ok;
		} else {
			return xxResultCheckingResult::Close;
		}
	};

	del_pannel_cookies(TWITTER);

	std_map<std::string, std::string> header;
	bResult = xxx_browser_view(result, getLoginUrl(), header, TWITTER, check, parent, false);

	return bResult;
}

上述函数,用于登录,使用cef浏览器登录,中途会因为开发者之前再twitter的设置中,跳转到指定的网页,识别验证网页是我设置的url,那就结束,再网页的地址中,可以解析到返回的token密钥对,用户ID等

到此,登录授权完成,用户授权码得到,后续就可以为用户使用需要的api

之后返回的值需要交换获取成为token密钥对


void xxxTwitterDataHandler::getAccessToken()
{
	qDebug() << " begin getAccessToken ";

	QUrl url(twitter_access_token_url);
	QUrlQuery query;
	auto authToken = getInfo(myLastInfo(), channel_data::oauth_token);
	query.addQueryItem(channel_data::oauth_token, authToken);

	auto authVerify = getInfo(myLastInfo(), channel_data::oauth_verifier);
	query.addQueryItem(channel_data::oauth_verifier, authVerify);

	url.setQuery(query);

	http::Request request;
	request.url(url);

	auto handleSuccess = [this](const http::Reply &reply) {
		QString data = reply.data();
		QVariantMap jsonMap = queryStringToMap(data);
		addToMap(myLastInfo(), jsonMap);
		myLastInfo()[ChannelData::g_channelStatus] = ChannelData::Valid;
		myLastInfo()[ChannelData::g_createTime] = QDateTime::currentDateTime();
		qDebug() << " finish update twitter info " << myLastInfo();
		next();
	};
	auto handleFail = [this](const pls::http::Reply &reply) {
		qDebug() << " error get token " << reply.data();
		myCallback()({myLastInfo()});
	};

	request.okResult(handleSuccess);
	request.failResult(handleFail);
	
	request.timeout(ET_REQUEST_TIMEOUT);
	request.method(pls::http::Method::Post);
	http::request(request);

上面的函数是业务函数,本质是使用networkmanager 发送post,而post的url需要把之前的参数传达进去,获取返回值,返回值和之前的登录一样,是一行字符串,可以通过分隔符或者qt的query对象拿到。addmap是业务函数,用于将两个map叠加

上面所有的授权工作就完成了,接下来就可以调用api了

准备API工作:

OAuPtr xxxTwitterDataHandler::createAPIAuth()
{
	auto authPtr = OAuPtr::create();
	authPtr->moveToThread(this->thread());
	authPtr->setContentType(QAbstractOAuth::ContentType::Json);

	//for some api will redirect to other url
	auto manager = new QNetworkAccessManager();
	manager->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy);
	authPtr->setNetworkAccessManager(manager);

	return authPtr;
}

因为twitter使用post,规定是json,因此设置了content type

sameoriginredirectpolicy 是因为twitter部分api中间会重定向跳转,需要设置,如果不设置,会返回错误码 307之类的3开头的错误码

void xxxTwitterDataHandler::updateApiAuth()
{
	if (!mApiAu) {
		mApiAu = createAPIAuth();
	}

	QString key = getAPIKey();
	QString secret = getAPISecret();
	mApiAu->setClientCredentials(key, secret);
	mApiAu->setSignatureMethod(QOAuth1::SignatureMethod::Hmac_Sha1);

	//add token info
	auto auToken = getInfo(myLastInfo(), channel_data::oauth_token);
	auto auSecret = getInfo(myLastInfo(), channel_data::oauth_token_secret);
	mApiAu->setTokenCredentials(auToken, auSecret);

	next();
}

创建了对象,接着就是把之前获取的授权码,开发者授权密钥对都设置进去,也就是需要两对密钥,开发者和后面登录之后用户那边拿到的一对

info相关的操作都是map的读取,我业务上的需要的逻辑

准备好了api的数据,接下来就是访问。

测试获取用户信息:


void xxxTwitterDataHandler::getUserInfomation()
{
	qDebug() << " begin getUserInfomation ";
	QUrlQuery query;
	query.addQueryItem("user.fields", "location,profile_image_url,description");
	auto userID = getInfo(myLastInfo(), channel_data::g_userID);
	query.addQueryItem("ids", userID);

	QUrl url(twitter_user_url);
	url.setQuery(query);

	auto handleResult = [this](QNetworkReply *reply) { //
		auto scopeFun = qScopeGuard([this]() { myCallback()({myLastInfo()}); });
		auto data = reply->readAll();
		auto jsonDoc = QJsonDocument::fromJson(data);
		PLS_INFO(TWITTER, "reply data  %s", data.constData());
		auto users = jsonDoc.object().value("data").toArray();
		if (users.isEmpty()) {
			PLS_INFO(TWITTER, "%s", "reply error when get user information,exmpty user info");
			return;
		}
		auto userInfo = users.first();
		auto jsonMap = userInfo.toVariant().toMap();
		addToMap(myLastInfo(), jsonMap, getMyDataMaper());
		myLastInfo().insert(ChannelData::g_shareUrl, "");
		qDebug() << " user information data " << data;
		next();
	};

	sendRequest(url.toString(), handleResult);
}

上述代码获取用户信息,主要是简单资料,头像等,后续我会贴上url

sendrequest用于统一处理发送的请求,主要是因为我这边需要访问很多api,错误需要统一处理;也是因为qt的post存在业务缺陷,无法完全实现我的需求,后面会再解释


void xxxTwitterDataHandler::sendRequest(const QString &url, const SuccessCallback &callback, const QJsonObject &body, const QString &httpMethod)
{
	http::Request request(http::Exclude::NoDefaultRequestHeaders);
	request.url(url);
	request.contentType("application/json");
	auto addAuthor = [this, url, httpMethod](QNetworkRequest *requestP) { //
		this->prepareRequest(url, requestP, httpMethod.toUpper());
	};
	request.additional(addAuthor);
	if (!body.isEmpty()) {
		request.body(body);
	}

	auto handleResult = [this, callback](const http::Reply &reply) {
		if (!checkError(reply.reply())) {
			emit requestFinished(reply.reply()->url().toString(), false);
			return;
		}
		callback(reply.reply());
		emit requestFinished(reply.reply()->url().toString(), true);
	};
	request.ignoreCache();
	request.result(handleResult);

	
	request.timeout(NET_REQUEST_TIMEOUT);
	auto methd = pls::http::Method::Get;
	if (httpMethod.contains("get", Qt::CaseInsensitive)) {
		methd = pls::http::Method::Get;
	} else if (httpMethod.contains("put", Qt::CaseInsensitive)) {
		methd = pls::http::Method::Put;
	} else if (httpMethod.contains("post", Qt::CaseInsensitive)) {
		methd = pls::http::Method::Post;
	} else if (httpMethod.contains("delete", Qt::CaseInsensitive)) {
		methd = pls::http::Method::Delete;
	}
	request.method(methd);
	http::request(request);
}

上述函数,本质实现了发送四种请求,为什么要这么绕呢,好像qoauth1又post,get,put啊

不是我不知道,是因为我花了四天时间,发现qt的这个对象post发送不了json的请求,都是错误,使用抓包和postman对比发现的问题

QByteArray QOAuth1Private::generateSignature(const QVariantMap &parameters,
                                             const QUrl &url,
                                             QNetworkAccessManager::Operation operation) const
{
    QOAuth1Signature signature(url,
                               clientIdentifierSharedKey,
                               tokenSecret,
                               static_cast<QOAuth1Signature::HttpRequestMethod>(operation),
                               parameters);
    return formatSignature(signature);
}

上面的代码是Qt的源码,内部就是当你传递一个map作为post参数的时候会调用,也就是生成的签名会强制对你的所有body做hash,因此生成的签名不正确。

所以我只能自己写,又两种,一种是下面的,也就是我之际上工作用的

void xxxTwitterDataHandler::prepareRequest(const QString &url, QNetworkRequest *requestP, const QString &httpMethod)
{
	requestP->setUrl(url);
	mApiAu->prepareRequest(requestP, httpMethod.toUtf8());
}

上述代码就是是使用传入的request对象自己构造,把qoauth1对象生成的签名放入request的header里面。url是必须的,如果传入之前为空,生成的签名不正确。httpmethod就是get,post,put等,字符串记住是大写。

另外一种方法,自己使用Qt的另外一种对象生成签名的字符串。验证过了,没有问题


void MainWindow::on_pushButton_clicked()
{
    QUrl url(ui->urlTxt->text());
    auto shareKey = ui->lineEdit_2->text();
    auto tokenSec = ui->lineEdit_4->text();
    QOAuth1Signature sig;
    sig.setClientSharedKey(shareKey);
    sig.setTokenSecret(tokenSec);
    sig.setUrl(url);
    QVariantMap parame;
    parame.insert("oauth_timestamp",ui->timeTxt->text());
    parame.insert("oauth_consumer_key","v2Lo35uffIcRAKFZYgDZB8umH");
    parame.insert("oauth_token","1198850206126628869-INfRoJF4crq1Ap9x8ce61xwxIgQUZG");
    parame.insert("oauth_nonce",ui->randomTxt->text());
    parame.insert("oauth_version","1.0");
    parame.insert("oauth_signature_method","HMAC-SHA1");
    sig.setCustomMethodString("POST");

    sig.setParameters(parame);

    QOAuth1Signature sig2(url,shareKey,tokenSec,QOAuth1Signature::HttpRequestMethod::Post,parame);


    qDebug()<<" result "<<sig.hmacSha1().toBase64().toPercentEncoding()   <<" \n  value "<<sig.keys()

           <<"parameters \n " <<sig.parameters();

    qDebug()<<" \n result 2 "<<sig2.hmacSha1().toBase64().toPercentEncoding()   <<" \n  value "<<sig2.keys()

           <<"parameters \n " <<sig2.parameters();
}

上面两个对象都是生成相同的签名,然后需要自己把签名的字符串组装到request里面去。具体可以看qt源码。生成的结果字符串应该是下面这样子:Authorization 部分

curl --location --request GET 'https://api.twitter.com/2/users?ids=1198850206126628869&user.fields=location,profile_image_url,description' \
--header 'Authorization: OAuth oauth_consumer_key="v2Lo35uffIcRAKFZYgDZB8umH",oauth_token="1198850206126628869-INfRoJF4crq1Ap9x8ce61xwxIgQUZG",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1658106499",oauth_nonce="Pmert9RhUT6",oauth_version="1.0",oauth_signature="RVczgNcoqxKOIB21%2BLQ9y5Wan%2FM%3D"' \

最后贴上我用到的字符串key


const QString twitterApi = "https://api.twitter.com/";
//use cusumer secret siganture
const QString twitter_auth_token_url = twitterApi + "oauth/request_token";
const QString twitter_access_token_url = twitterApi + "oauth/access_token";

//use cusumer  secret and token secret to signature
const QString twitter_user_url = twitterApi + "2/users";
const QString twitter_region_url = twitterApi + "2/region";
const QString twitter_source_url = twitterApi + "2/users/%1/sources";
const QString twitter_broadcast_url = twitterApi + "2/users/%1/broadcasts";

#define twitter_tmp_bin (getChannelCacheDir() + "/" + QString(TWITTER))
//twitter
const QString oauth_token = "oauth_token";
const QString oauth_token_secret = "oauth_token_secret";
const QString oauth_callback_confirmed = "oauth_callback_confirmed";
const QString oauth_verifier = "oauth_verifier";
const QString twitter_source_id = "source_id";
const QString twitter_region = "region";
const QString default_region = "ap-northeast-2";
const QString is_low_latency = "is_low_latency";

贴一个可以可以成功的,使用Qoauth1对象的get方法


void xxxTwitterDataHandler::getUserInfomation()
{
	qDebug() << " begin getUserInfomation ";
	QUrlQuery query;
	query.addQueryItem("user.fields", "location,profile_image_url,description");

	auto userID = getInfo(myLastInfo(), channel_data::g_userID);
	query.addQueryItem("ids", userID);

	QUrl url(twitter_user_url);
	url.setQuery(query);
	QPointer reply = mApiAu->get(url);
	auto handleResult = [reply, this]() { //
		auto scopeFun = qScopeGuard([this]() { myCallback()({myLastInfo()}); });

		if (!checkError(reply)) {
			myLastInfo()[ChannelData::g_channelStatus] = ChannelData::Error;
			return;
		}
		auto data = reply->readAll();
		auto jsonDoc = QJsonDocument::fromJson(data);
		PLS_INFO(TWITTER, "reply data  %s", data.constData());
		auto users = jsonDoc.object().value("data").toArray();
		if (users.isEmpty()) {
			PLS_INFO(TWITTER, "%s", "reply error when get user information,exmpty user info");
			return;
		}
		auto userInfo = users.first();
		auto jsonMap = userInfo.toVariant().toMap();
		addToMap(myLastInfo(), jsonMap, getMyDataMaper());
		myLastInfo().insert(ChannelData::g_shareUrl, "");
		qDebug() << " user information data " << data;
		next();
	};
	connect(reply, &QNetworkReply::finished, this, handleResult, Qt::QueuedConnection);
}

此代码段是最早的版本,访问get请求没有问题,就是后续其他post又json的body出问题,才改成前面的。如果要自己写,那就是使用networkmanager 发送post,先把request组装好,发送就没有问题了。那个发送的request不会对body进行hash计算。

下面是抓包发送的get

GET https://api.twitter.com/2/users/1198850206126628869/sources HTTP/1.1
Host: api.twitter.com
Authorization: OAuth oauth_consumer_key="v2Lo35uffIcRaaAKFZ2YdgDZB8umH",oauth_nonce="dhTvFkDY",oauth_signature="T4i8lLUopwV%2BjS%2B2lGHL3D2O9Vs%3D",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1657769811",oauth_token="1198850206126628869-INfRoJF4crq1Ap9x8ce61xwxIgQUZG",oauth_version="1.0"
Cookie: guest_id_marketing=v1%3A165776950786013766; guest_id_ads=v1%3A165776950786013766; personalization_id="v1_hGXIEh9uiJS6+WaO9NvwBw=="; guest_id=v1%3A165776950786013766
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,en,*
User-Agent: Mozilla/5.0

下面是post

POST https://api.twitter.com/2/users/1198850206126628869/sources HTTP/1.1
Host: api.twitter.com
Authorization: OAuth oauth_consumer_key="v2Lo35uffIcRAKFZYgDZB8umH",oauth_nonce="ii9a2HlC",oauth_signature="e1leybmtRaMT%2BhVffLG88892md4%3D",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1657873941",oauth_token="1198850206126628869-INfRoJF4crq1Ap9x8ce61xwxIgQUZG",oauth_version="1.0"
Content-Type: application/json
Cookie: guest_id_marketing=v1%3A165787143973669276; guest_id_ads=v1%3A165787143973669276; personalization_id="v1_NUCjEBjZsQE5TebiULpA3A=="; guest_id=v1%3A165787143973669276
Content-Length: 102
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,en,*
User-Agent: Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.11082

{
    "name": "PRISMLiveStudio6c2d798b-74e8-4665-b867-179530a52e4b",
    "region": "ap-northeast-2"
}

发送的请求可以尝试抓包,比对postman发送的结果。

好了差不多就是这样子了,QT的QOauth1,总结下来就是,不要发送带body的请求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值