Qt6 QML Book/网络设置/使用OAuth进行身份验证

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:

以下是一些我们认为有用的链接:

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开发者的门户上创建一个专用的应用程序。

Qt6 QML Book/网络设置/使用OAuth进行身份验证

Once your app is created, you'll receive two keys: a client id and a client secret.

创建应用程序后,您将收到两个密钥:client idclient secret

Qt6 QML Book/网络设置/使用OAuth进行身份验证

The QML file

QML文件

The process is divided in two phases:

该过程分为两个阶段:

  1. The application connects to the Spotify API, which in turns requests the user to authorize it;
  2. 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();
    }

Qt6 QML Book/网络设置/使用OAuth进行身份验证

示例源码下载 

上一篇:valgrind报的几种内存泄露(转)


下一篇:数据库五章其二 ——SQL