Authentication using OAuth
使用OAuth进行身份验证
OAuth is an open protocol to allow secure authorization in a simple and standard method from web, mobile, and desktop applications. OAuth is used to authenticate a client against common web-services such as Google, Facebook, and Twitter.
OAuth是一个开放的协议,允许通过简单的标准方法从web、移动和桌面应用程序进行安全授权。OAuth用于根据常见的web服务(如Google、Facebook和Twitter)对客户端进行身份验证。
TIP
注
For a custom web-service you could also use the standard HTTP authentication for example by using the
XMLHttpRequest
username and password in the get method (e.g.xhr.open(verb, url, true, username, password)
)对于自定义web服务,您还可以使用标准HTTP身份验证,例如,在get方法中使用XMLHttpRequest用户名和密码(例如
xhr.open(verb, url, true, username, password
)
OAuth is currently not part of a QML/JS API. So you would need to write some C++ code and export the authentication to QML/JS. Another issue would be the secure storage of the access token.
OAuth目前不是QML/JS API的一部分。因此,您需要编写一些C++代码并将验证导出到QML/JS。另一个问题是访问令牌的安全存储。
Here are some links which we find useful:
以下是一些我们认为有用的链接:
- http://oauth.net/
- http://hueniverse.com/oauth/
- https://github.com/pipacs/o2
- http://www.johanpaul.com/blog/2011/05/oauth2-explained-with-qt-quick/
Integration example
集成示例
In this section, we will go through an example of OAuth integration using the Spotify API. This example uses a combination of C++ classes and QML/JS. To discover more on this integration, please refer to Chapter 16.
在本节中,我们将介绍一个使用Spotify API进行OAuth集成的示例。这个例子使用C++类和QML/JS的组合。有关此集成的更多信息,请参阅第16章。
This application's goal is to retrieve the top ten favourite artists of the authenticated user.
该应用程序的目标是检索经过身份验证的用户最喜欢的十位艺术家。
Creating the App
创建应用程序
First, you will need to create a dedicated app on the Spotify Developer's portal.
首先,你需要在Spotify开发者的门户上创建一个专用的应用程序。
Once your app is created, you'll receive two keys: a client id
and a client secret
.
创建应用程序后,您将收到两个密钥:client id
和client secret
。
The QML file
QML文件
The process is divided in two phases:
该过程分为两个阶段:
- The application connects to the Spotify API, which in turns requests the user to authorize it;
- If authorized, the application displays the list of the top ten favourite artists of the user.
- 该应用程序连接到Spotify API,后者反过来请求用户对其进行授权;
- 如果获得授权,应用程序将显示用户最喜爱的十位艺术家的列表。
Authorizing the app
授权应用程序
Let's start with the first step:
让我们从第一步开始:
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import Spotify
When the application starts, we will first import a custom library, Spotify
, that defines a SpotifyAPI
component (we'll come to that later). This component will then be instanciated:
当应用程序启动时,我们将首先导入一个自定义库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()
}
Once the application has been loaded, the SpotifyAPI
component will request an authorization to Spotify:
加载应用程序后,SpotifyAPI组件将请求对Spotify的授权:
Component.onCompleted: {
spotifyApi.setCredentials("CLIENT_ID", "CLIENT_SECRET")
spotifyApi.authorize()
}
Until the authorization is provided, a busy indicator will be displayed in the center of the app.
在提供授权之前,应用程序中心将显示忙碌指示灯。
TIP
注
Please note that for security reasons, the API credentials should never be put directly into a QML file!
请注意,出于安全原因,不应将API凭据直接放入QML文件中!
Listing the user's favorite artists
列出用户最喜欢的艺术家
The next step happens when the authorization has been granted. To display the list of artists, we will use the Model/View/Delegate pattern:
下一步将在授权被授予时进行。要显示艺术家列表,我们将使用模型/视图/委托模式:
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 }
}
}
}
}
}
The model SpotifyModel
is defined in the Spotify
library. To work properly, it needs a SpotifyAPI
.
SpotifyModel
在Spotify库中定义。要正常工作,它需要一个SpotifyAPI。
The ListView displays a vertical list of artists. An artist is represented by a name, an image and the total count of followers.
ListView显示艺术家的垂直列表。一个艺术家由一个名字、一个形象和追随者总数来代表。
SpotifyAPI
Let's now get a bit deeper into the authentication flow. We'll focus on the SpotifyAPI
class, a QML_ELEMENT
defined on the C++ side.
现在让我们更深入地了解一下身份验证流程。我们将关注于SpotifyAPI
类,C++上定义的一个QML_ELEMENT
。
#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
First, we'll import the <QOAuth2AuthorizationCodeFlow>
class. This class is a part of the QtNetworkAuth
module, which contains various implementations of OAuth
.
首先,我们将导入<QOAuth2AuthorizationCodeFlow>类。此类是QtNetworkAuth模块的一部分,该模块包含OAuth的各种实现。
#include <QOAuth2AuthorizationCodeFlow>
Our class, SpotifyAPI
, will define a isAuthenticated
property:
我们的类SpotifyAPI将定义一个isAuthenticated属性:
Q_PROPERTY(bool isAuthenticated READ isAuthenticated WRITE setAuthenticated NOTIFY isAuthenticatedChanged)
The two public slots that we used in the QML files:
我们在QML文件中使用的两个公有槽:
void setCredentials(const QString& clientId, const QString& clientSecret);
void authorize();
And a private member representing the authentication flow:
以及表示认证流的私有成员:
QOAuth2AuthorizationCodeFlow m_oauth2;
On the implementation side, we have the following code:
在实现方面,我们有以下代码:
#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"));
}
The constructor task mainly consists in configuring the authentication flow. First, we define the Spotify API routes that will serve as authenticators.
构造函数任务主要包括配置身份验证流。首先,我们定义将用作身份验证程序的Spotify API路由。
m_oauth2.setAuthorizationUrl(QUrl("https://accounts.spotify.com/authorize"));
m_oauth2.setAccessTokenUrl(QUrl("https://accounts.spotify.com/api/token"));
We then select the scope (= the Spotify authorizations) that we want to use:
然后,我们选择要使用的范围(=Spotify授权):
m_oauth2.setScope("user-top-read");
Since OAuth is a two-way communication process, we instanciate a dedicated local server to handle the replies:
由于OAuth是一个双向通信过程,我们实例化了一个专用的本地服务器来处理回复:
m_oauth2.setReplyHandler(new QOAuthHttpServerReplyHandler(8000, this));
Finally, we configure two signals and slots.
最后,我们配置了两个信号和槽。
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, &QDesktopServices::openUrl);
connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::statusChanged, [=](QAbstractOAuth::Status status) { /* ... */ })
The first one configures the authorization to happen within a web-browser (through &QDesktopServices::openUrl
), while the second makes sure that we are notified when the authorization process has been completed.
第一种方法将授权配置为在web浏览器中进行(通过&QDesktopServices::openUrl),而第二种方法确保在授权过程完成时通知我们。
The authorize()
method is only a placeholder for calling the underlying grant()
method of the authentication flow. This is the method that triggers the process.
authorize()方法只是用于调用身份验证流的底层grant()方法的占位符。这就是触发该过程的方法。
void SpotifyAPI::authorize() {
m_oauth2.grant();
}
Finally, the getTopArtists()
calls the web api using the authorization context provided by the m_oauth2
network access manager.
最后,getTopArtists()使用m_oauth2网络访问管理器提供的授权上下文调用web api。
QNetworkReply* SpotifyAPI::getTopArtists() {
return m_oauth2.get(QUrl("https://api.spotify.com/v1/me/top/artists?limit=10"));
}
The Spotify model
Spotify模型
This class is a QML_ELEMENT
that subclasses QAbstractListModel
to represent our list of artists. It relies on SpotifyAPI
to gather the artists from the remote endpoint.
这个类是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
This class defines a spotifyApi
property:
此类定义了spotifyApi属性:
Q_PROPERTY(SpotifyAPI* spotifyApi READ spotifyApi WRITE setSpotifyApi NOTIFY spotifyApiChanged)
An enumeration of Roles (as per QAbstractListModel
):
角色枚举(根据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
};
A slot to trigger the refresh of the artists list:
触发艺术家列表刷新的槽:
public slots:
void update();
And, of course, the list of artists, represented as JSON objects:
当然,还有艺术家列表,以JSON对象表示:
public slots:
QList<QJsonObject> m_artists;
On the implementation side, we have:
在实现方面,我们有:
#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();
});
}
The update()
method calls the getTopArtists()
method and handle its reply by extracting the individual items from the JSON document and refreshing the list of artists within the model.
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();
});
The data()
method extracts, depending on the requested model role, the relevant attributes of an Artist and returns as a QVariant
:
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();
}
示例源码下载