鉴于工作需要,开发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 ¶meters,
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的请求。