# OAuth授权
OAuth是一种开放的协议,允许以简单和标准的方法从Web、移动端和桌面应用程序进行安全授权。 OAuth用于针对常见的Web服务(如 Google、Facebook和Twitter)对客户端进行身份验证。
提示
对于自定义的Web服务,还可以使用标准的HTTP身份验证,例如在get方法中使用XMLHttpRequest
用户名和密码(例如 xhr.open(verb, url, true, username, password)
)
OAuth目前不是QML/JS API的一部分。 因此,需要编写一些C++代码并将身份验证导出到QML/JS中。 另一个问题是访问令牌的安全存储。
以下是作者认为有用的一些链接:
- http://oauth.net/ (opens new window)
- http://hueniverse.com/oauth/ (opens new window)
- https://github.com/pipacs/o2 (opens new window)
- http://www.johanpaul.com/blog/2011/05/oauth2-explained-with-qt-quick/ (opens new window)
# 集成示例
在本节中,将通过使用Spotify API (opens new window)的OAuth集成示例。 此示例使用C++类和QML/JS的组合。 要了解有关此集成的更多信息,请参阅第16章。
此应用程序的目标是检索经过身份验证的用户最喜欢的前十位艺术家。
# 创建应用程序
首先,需要在Spotify 开发者门户(Spotify Developer's portal) (opens new window)上创建一个专用应用程序。
创建应用后,将收到两个密钥:一个client id
和一个client secret
。
# QML文件
该过程分为两个阶段:
- 应用程序连接到Spotify API,然后请求用户对其授权;
- 如果已授权,应用程序将显示用户最喜欢的前十名艺术家的列表。
# 授权应用程序
从第一步开始:
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import Spotify
当应用程序开始时,将首先导入一个自定义库Spotify
,它定义了一个SpotifyAPI
组件(稍后会谈到);然后将实例化此组件:
ApplicationWindow {
width: 320
height: 568
visible: true
title: qsTr("Spotify OAuth2")
BusyIndicator {
visible: !spotifyApi.isAuthenticated
anchors.centerIn: parent
}
SpotifyAPI {
id: spotifyApi
onIsAuthenticatedChanged: if(isAuthenticated) spotifyModel.update()
}
应用程序被加载完成后,SpotifyAPI
组件将请求对Spotify授权:
Component.onCompleted: {
spotifyApi.setCredentials("CLIENT_ID", "CLIENT_SECRET")
spotifyApi.authorize()
}
在提供授权之前,应用程序中心会显示一个表示忙碌的指示符。
提示
请注意,出于安全原因,API凭证不应直接放入QML文件中!
# 列出用户最喜欢的艺术家
当获得授权后,将进入下一步。 为了显示艺术家列表,使用模型/视图/委托模式:
SpotifyModel {
id: spotifyModel
spotifyApi: spotifyApi
}
ListView {
visible: spotifyApi.isAuthenticated
width: parent.width
height: parent.height
model: spotifyModel
delegate: Pane {
id: delegate
required property var model
topPadding: 0
Column {
width: 300
spacing: 10
Rectangle {
height: 1
width: parent.width
color: delegate.model.index > 0 ? "#3d3d3d" : "transparent"
}
Row {
spacing: 10
Item {
width: 20
height: width
Rectangle {
width: 20
height: 20
anchors.top: parent.top
anchors.right: parent.right
color: "black"
Label {
anchors.centerIn: parent
font.pointSize: 16
text: delegate.model.index + 1
color: "white"
}
}
}
Image {
width: 80
height: width
source: delegate.model.imageURL
fillMode: Image.PreserveAspectFit
}
Column {
Label {
text: delegate.model.name
font.pointSize: 16
font.bold: true
}
Label { text: "Followers: " + delegate.model.followersCount }
}
}
}
}
}
在Spotify
库中定义了模型SpotifyModel
。 为了正常工作,它需要一个SpotifyAPI
。
列表视图在垂直列表中显示艺术家。 艺术家由名称、肖像和追随者总数表示。
# SpotifyAPI
现在进一步了解身份验证流程。 关注SpotifyAPI
类,它是一个在C++端定义的QML_ELEMENT
(QML元素)。
#ifndef SPOTIFYAPI_H
#define SPOTIFYAPI_H
#include <QtCore>
#include <QtNetwork>
#include <QtQml/qqml.h>
#include <QOAuth2AuthorizationCodeFlow>
class SpotifyAPI: public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(bool isAuthenticated READ isAuthenticated WRITE setAuthenticated NOTIFY isAuthenticatedChanged)
public:
SpotifyAPI(QObject *parent = nullptr);
void setAuthenticated(bool isAuthenticated) {
if (m_isAuthenticated != isAuthenticated) {
m_isAuthenticated = isAuthenticated;
emit isAuthenticatedChanged();
}
}
bool isAuthenticated() const {
return m_isAuthenticated;
}
QNetworkReply* getTopArtists();
public slots:
void setCredentials(const QString& clientId, const QString& clientSecret);
void authorize();
signals:
void isAuthenticatedChanged();
private:
QOAuth2AuthorizationCodeFlow m_oauth2;
bool m_isAuthenticated;
};
#endif // SPOTIFYAPI_H
首先,导入<QOAuth2AuthorizationCodeFlow>
类。 此类是QtNetworkAuth
模块的一部分,该模块包含OAuth
的各种实现。
#include <QOAuth2AuthorizationCodeFlow>
SpotifyAPI
类定义了一个isAuthenticated
属性:
Q_PROPERTY(bool isAuthenticated READ isAuthenticated WRITE setAuthenticated NOTIFY isAuthenticatedChanged)
在QML文件中使用的两个public的槽:
void setCredentials(const QString& clientId, const QString& clientSecret);
void authorize();
还有一个代表身份验证流程的私有成员:
QOAuth2AuthorizationCodeFlow m_oauth2;
在实现方面,有以下代码:
#include "spotifyapi.h"
#include <QtGui>
#include <QtCore>
#include <QtNetworkAuth>
SpotifyAPI::SpotifyAPI(QObject *parent): QObject(parent), m_isAuthenticated(false) {
m_oauth2.setAuthorizationUrl(QUrl("https://accounts.spotify.com/authorize"));
m_oauth2.setAccessTokenUrl(QUrl("https://accounts.spotify.com/api/token"));
m_oauth2.setScope("user-top-read");
m_oauth2.setReplyHandler(new QOAuthHttpServerReplyHandler(8000, this));
m_oauth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QMultiMap<QString, QVariant> *parameters) {
if(stage == QAbstractOAuth::Stage::RequestingAuthorization) {
parameters->insert("duration", "permanent");
}
});
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl);
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::statusChanged, [=](QAbstractOAuth::Status status) {
if (status == QAbstractOAuth::Status::Granted) {
setAuthenticated(true);
} else {
setAuthenticated(false);
}
});
}
void SpotifyAPI::setCredentials(const QString& clientId, const QString& clientSecret) {
m_oauth2.setClientIdentifier(clientId);
m_oauth2.setClientIdentifierSharedKey(clientSecret);
}
void SpotifyAPI::authorize() {
m_oauth2.grant();
}
QNetworkReply* SpotifyAPI::getTopArtists() {
return m_oauth2.get(QUrl("https://api.spotify.com/v1/me/top/artists?limit=10"));
}
构造器的任务主要包括配置认证流程。 首先,定义将用作身份验证器的Spotify API路由。
m_oauth2.setAuthorizationUrl(QUrl("https://accounts.spotify.com/authorize"));
m_oauth2.setAccessTokenUrl(QUrl("https://accounts.spotify.com/api/token"));
然后选择想要使用的范围(Spotify 授权):
m_oauth2.setScope("user-top-read");
由于OAuth是双向通信过程,实例化一个专用的本地服务器来处理回复:
m_oauth2.setReplyHandler(new QOAuthHttpServerReplyHandler(8000, this));
最后,配置两个信号槽连接。
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl);
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::statusChanged, [=](QAbstractOAuth::Status status) { /* ... */ })
第一个配置的授权发生在Web浏览器中(通过&QDesktopServices::openUrl
),而第二个配置确保在授权过程完成时得到通知。
authorize()
方法只是一个占位符,用于调用身份验证流程底层的grant()
方法。 这是触发该过程的方法。
void SpotifyAPI::authorize() {
m_oauth2.grant();
}
最后,getTopArtists()
使用m_oauth2
网络访问管理器提供的授权上下文调用web api。
QNetworkReply* SpotifyAPI::getTopArtists() {
return m_oauth2.get(QUrl("https://api.spotify.com/v1/me/top/artists?limit=10"));
}
# Spotify模型
该类是一个 QML_ELEMENT
,它是QAbstractListModel
的子类,用来表示艺术家列表。 它依靠SpotifyAPI
从远程端点收集艺术家信息。
#ifndef SPOTIFYMODEL_H
#define SPOTIFYMODEL_H
#include <QtCore>
#include "spotifyapi.h"
QT_FORWARD_DECLARE_CLASS(QNetworkReply)
class SpotifyModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(SpotifyAPI* spotifyApi READ spotifyApi WRITE setSpotifyApi NOTIFY spotifyApiChanged)
public:
SpotifyModel(QObject *parent = nullptr);
void setSpotifyApi(SpotifyAPI* spotifyApi) {
if (m_spotifyApi != spotifyApi) {
m_spotifyApi = spotifyApi;
emit spotifyApiChanged();
}
}
SpotifyAPI* spotifyApi() const {
return m_spotifyApi;
}
enum {
NameRole = Qt::UserRole + 1,
ImageURLRole,
FollowersCountRole,
HrefRole,
};
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
signals:
void spotifyApiChanged();
void error(const QString &errorString);
public slots:
void update();
private:
QPointer<SpotifyAPI> m_spotifyApi;
QList<QJsonObject> m_artists;
};
#endif // SPOTIFYMODEL_H
该类定义了spotifyApi
属性:
Q_PROPERTY(SpotifyAPI* spotifyApi READ spotifyApi WRITE setSpotifyApi NOTIFY spotifyApiChanged)
角色的枚举(根据QAbstractListModel
):
enum {
NameRole = Qt::UserRole + 1, // The artist's name
ImageURLRole, // The artist's image
FollowersCountRole, // The artist's followers count
HrefRole, // The link to the artist's page
};
触发艺术家列表刷新的槽:
public slots:
void update();
当然,还有艺术家列表,以JSON对象表示:
public slots:
QList<QJsonObject> m_artists;
在实现中,有:
#include "spotifymodel.h"
#include <QtCore>
#include <QtNetwork>
SpotifyModel::SpotifyModel(QObject *parent): QAbstractListModel(parent) {}
QHash<int, QByteArray> SpotifyModel::roleNames() const {
static const QHash<int, QByteArray> names {
{ NameRole, "name" },
{ ImageURLRole, "imageURL" },
{ FollowersCountRole, "followersCount" },
{ HrefRole, "href" },
};
return names;
}
int SpotifyModel::rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
return m_artists.size();
}
int SpotifyModel::columnCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
return m_artists.size() ? 1 : 0;
}
QVariant SpotifyModel::data(const QModelIndex &index, int role) const {
Q_UNUSED(role);
if (!index.isValid())
return QVariant();
if (role == Qt::DisplayRole || role == NameRole) {
return m_artists.at(index.row()).value("name").toString();
}
if (role == ImageURLRole) {
const auto artistObject = m_artists.at(index.row());
const auto imagesValue = artistObject.value("images");
Q_ASSERT(imagesValue.isArray());
const auto imagesArray = imagesValue.toArray();
if (imagesArray.isEmpty())
return "";
const auto imageValue = imagesArray.at(0).toObject();
return imageValue.value("url").toString();
}
if (role == FollowersCountRole) {
const auto artistObject = m_artists.at(index.row());
const auto followersValue = artistObject.value("followers").toObject();
return followersValue.value("total").toInt();
}
if (role == HrefRole) {
return m_artists.at(index.row()).value("href").toString();
}
return QVariant();
}
void SpotifyModel::update() {
if (m_spotifyApi == nullptr) {
emit error("SpotifyModel::error: SpotifyApi is not set.");
return;
}
auto reply = m_spotifyApi->getTopArtists();
connect(reply, &QNetworkReply::finished, [=]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
emit error(reply->errorString());
return;
}
const auto json = reply->readAll();
const auto document = QJsonDocument::fromJson(json);
Q_ASSERT(document.isObject());
const auto rootObject = document.object();
const auto artistsValue = rootObject.value("items");
Q_ASSERT(artistsValue.isArray());
const auto artistsArray = artistsValue.toArray();
if (artistsArray.isEmpty())
return;
beginResetModel();
m_artists.clear();
for (const auto artistValue : qAsConst(artistsArray)) {
Q_ASSERT(artistValue.isObject());
m_artists.append(artistValue.toObject());
}
endResetModel();
});
}
update()
方法调用getTopArtists()
方法,并通过从JSON中提取单个项并刷新模型中的艺术家列表来处理回复。
auto reply = m_spotifyApi->getTopArtists();
connect(reply, &QNetworkReply::finished, [=]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
emit error(reply->errorString());
return;
}
const auto json = reply->readAll();
const auto document = QJsonDocument::fromJson(json);
Q_ASSERT(document.isObject());
const auto rootObject = document.object();
const auto artistsValue = rootObject.value("items");
Q_ASSERT(artistsValue.isArray());
const auto artistsArray = artistsValue.toArray();
if (artistsArray.isEmpty())
return;
beginResetModel();
m_artists.clear();
for (const auto artistValue : qAsConst(artistsArray)) {
Q_ASSERT(artistValue.isObject());
m_artists.append(artistValue.toObject());
}
endResetModel();
});
data()
方法根据请求的模型角色提取艺术家的相关属性并当作QVariant
类型数据返回:
if (role == Qt::DisplayRole || role == NameRole) {
return m_artists.at(index.row()).value("name").toString();
}
if (role == ImageURLRole) {
const auto artistObject = m_artists.at(index.row());
const auto imagesValue = artistObject.value("images");
Q_ASSERT(imagesValue.isArray());
const auto imagesArray = imagesValue.toArray();
if (imagesArray.isEmpty())
return "";
const auto imageValue = imagesArray.at(0).toObject();
return imageValue.value("url").toString();
}
if (role == FollowersCountRole) {
const auto artistObject = m_artists.at(index.row());
const auto followersValue = artistObject.value("followers").toObject();
return followersValue.value("total").toInt();
}
if (role == HrefRole) {
return m_artists.at(index.row()).value("href").toString();
}