# 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中。 另一个问题是访问令牌的安全存储。

以下是作者认为有用的一些链接:

# 集成示例

在本节中,将通过使用Spotify API (opens new window)的OAuth集成示例。 此示例使用C++类和QML/JS的组合。 要了解有关此集成的更多信息,请参阅第16章。

此应用程序的目标是检索经过身份验证的用户最喜欢的前十位艺术家。

# 创建应用程序

首先,需要在Spotify 开发者门户(Spotify Developer's portal) (opens new window)上创建一个专用应用程序。

image

创建应用后,将收到两个密钥:一个client id和一个client secret

image

# QML文件

该过程分为两个阶段:

  1. 应用程序连接到Spotify API,然后请求用户对其授权;
  2. 如果已授权,应用程序将显示用户最喜欢的前十名艺术家的列表。

# 授权应用程序

从第一步开始:

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

image

最后更新: 1/26/2022, 8:52:56 AM