0
点赞
收藏
分享

微信扫一扫

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

安七月读书 2022-01-31 阅读 68
qtqt6qml

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)对客户端进行身份验证。

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 idclient secret

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.

在提供授权之前,应用程序中心将显示忙碌指示灯。

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();
    }

示例源码下载 

举报

相关推荐

0 条评论