Day33 - 【实战篇-预告】Device Code(4)

本系列文之後也会置於个人网站


本篇应一些後续考量移除了部分内容。若对本篇有兴趣还请多关注本系列後续消息。
(不过有可能会改用Electron.js改写,虽然架构上会再麻烦一些。在此前也可以留言让我知道想看PySide完整版本还是Electron.js完整版本)

这次应用使用PySide来实现界面;qrcode来产生需要的QR Code;并使用requests来与身份验证与授权服务器的API沟通。现在透过pip进行安装需要的packages。

pip install PySide6 requests qrcode

其实本来可以考虑用electron.js,但是基於一些考量,最後决定使用PySide。

在昨天,透过Qt Designer建立了两个需要的使用者界面,今天来实现逻辑部分。

建立Widget

在之前所设计的ui档案分别是:example-device-code-app.uilogin-dialog.ui。这部分会分别将这两份载入到类别内使用。所以同样来建立两个Widgets:ExampleDeviceCodeApploginDialog

class ExampleDeviceCodeApp(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        self.__load_ui()

    def __load_ui(self):
        #......
        pass

class loginDialog(QWidget):
    def __init__(self, api, afterLogin=lambda device_code: None):
        LOG.debug('init')
        QWidget.__init__(self)
        self.__afterLogin = afterLogin
        self.__api = api
        self.__load_ui()

    def __load_ui(self):
        #......
        pass

绑定操作事件

接着需要在ExampleDeviceCodeApp,针对登入按钮绑定事件。

    def __bind_event(self):
        loginButton: QPushButton = self.findChild(QPushButton, 'loginButton')
        loginButton.clicked.connect(self.onClickLoginBtn)

    def onClickLoginBtn(self):
        self.dialog = loginDialog(api=api,
                                  afterLogin=self.__afterLogin)
        self.dialog.show()

登入按钮处理的逻辑很简单,也就是再开一个dialog,也就是loginDialog而已。特别的是,在建立loginDialog时,传入一个callback,当登入成功的时候出发。

注: 这个写法并不是Qt常态的Dialog写法。

    def __afterLogin(self, tokens
                     ):

        user_info = api.getUserInfo(tokens.get('access_token'))
        name = user_info.get('name', "Unknow")
        self.message.setText(f'Hello, {name}!')

关於如何实现API沟通会放到之後说明。登入成成功後会取得access_token,会需要透过这个存取权杖来取得登入的使用者资讯。接着将取得的帐号名称显式在画面上。

与授权服务器通讯

会需要在建立一个类别来处理API相关的处理。

api类别的基本讯息

class Keycloak:
    '''
    example:
    > api = Keycloak('http://localhost:8080', 'quick-start')
    '''
    def __init__(self, base: str, realm: str, *,
                 client_id: str = None,
                 client_secret: str = None):
        self.__base = base
        self.__realm = realm
        self.client_id = client_id
        self.client_secret = client_secret

    @property
    def base_url(self):
        base = self.__base
        realm = self.__realm
        return f'{base}/auth/realms/{realm}'

这个类别会记忆base_url,要登入的realm。并且在Device Code Flow下还需要在设定Client IdClient Secret

取得device_code的方法

提供一个方式,透过requests.post处理device code的endpint来取得device_codeuser_code

    def getDeviceCode(self, *,
                      client_id=None,
                      client_secret=None):
        #......
        pass

在之前看过Keycloak回传的资讯可能长成

{
    "device_code": "boTQ6vd49RXTOYOb7dwXBCpHYskzOjXvDPjkXxniMN0",
    "user_code": "HZYO-ROXJ",
    "verification_uri": "http://localhost:8080/auth/realms/quick-start/device",
    "verification_uri_complete": "http://localhost:8080/auth/realms/quick-start/device?user_code=HZYO-ROXJ",
    "expires_in": 600,
    "interval": 5
}

验证device_code的方法

同样透过requests来检查是否有人登入授权了。如果回传200表示有人登入授权,并可以取得access_tokenrefresh_token

    def vertifyDeviceCode(self,
                          device_code: str,
                          *,
                          client_id=None,
                          client_secret=None):
        #......
        pass

取得使用者资讯的方法

也同样透过requests呼叫相对应API的endpoint来取得使用者资讯。

    def getUserInfo(self, access_token: str):
        #......
        pass

建立api实例

最後建立一个实例供Widgets使用。

KEYCLOAK_URL = 'http://localhost:8080'
KEYCLOAK_REALM = 'quick-start'
KEYCLOAK_CLIENT_ID = 'example-device-app'
KEYCLOAK_CLIENT_SECRET = '2eefb27e-ac98-47c4-8ac5-82e8edc73b30'

api = Keycloak(KEYCLOAK_URL, KEYCLOAK_REALM,
               client_id=KEYCLOAK_CLIENT_ID,
               client_secret=KEYCLOAK_CLIENT_SECRET)

处理Device Code检查逻辑

再回到loginDialog类别,在__init__在添加个self.__init_api(),好让在画面啓动後自动取取得一个新的device_code,并等待使用者登入。

取得device_code

__init_api的内容,首先会需要取得device_code的相关讯息:

        device_code_info = self.__api.getDeviceCode()
        device_code = device_code_info.get('device_code')
        user_code = device_code_info.get('user_code')
        verification_uri = device_code_info.get('verification_uri', '')
        verification_uri_complete = device_code_info.get('verification_uri_complete', '')
        interval = device_code_info.get('interval', 5)
        expires_in = device_code_info.get('expires_in', 60)

        self.__device_code = device_code
        self.__interval = interval
        self.__expires_in = expires_in
        self.__curr = 0

以Keycloak回传的资料而言,通常还包含intervalexpires_in。前者最短每隔5秒检查一次是否有人登入;後者表明这个device_code在多久後过期,无法在使用,也就意味这没有人登入,需要重新取得device_code,但这里处理并不会直接重新取得device_code,而是直接关闭dialog。

        if self.__curr > self.__expires_in:
            self.close()
            # self.__timer.stop()
            e = Exception("Timeout: Login Fail.")
            self.__afterLogin(e)
            raise e

产生QR Code

透过qrcode来产生一个包含登入URL资讯的QR Code,并将图档资讯储存於buf

        self.__qrcode = qrcode.make(verification_uri_complete, box_size=250)
        img = self.__qrcode.get_image()
        #......

将资讯显式在画面上

最後将相关资讯显式在画面上

        base_url_widget: QLabel = self.findChild(QLabel, 'BaseLoginURL')
        base_url_widget.setText(f'[{verification_uri}]({verification_uri})')

        user_code_widget: QLabel = self.findChild(QLabel, 'user_code')
        user_code_widget.setText(f'{user_code}')

当然还包含QR Code (略过)

(keypoint)检查是否有人登入授权

每隔interval秒需要去检查一次是否有人登入。使用QTimer来处理,这里个结果会每5秒去触发一次self.checkLogin

        self.__timer = QTimer()
        timer: QTimer = self.__timer
        timer.timeout.connect(self.checkLogin)
        timer.start(interval*1000)

self.checkLogin的内容主要也就是透过API去检查是否有人登入授权:

            result = self.__api.vertifyDeviceCode(self.__device_code)

如果登入成功的话,就在呼叫__afterLogin。将资讯返回给主要的Widget。

        self.__afterLogin(result)
        self.close()

取得使用者讯息,并显式在画面上

回到ExampleDeviceCodeApp。在登入以後,取得access_token。在透过存取权杖取得使用者资讯显式在画面上:

    def __afterLogin(self, tokens):
        user_info = api.getUserInfo(tokens.get('access_token'))
        name = user_info.get('name', "Unknow")
        self.message.setText(f'Hello, {name}!')

结语

本系列暂有後续计划,故本篇隐藏了部分内容。若对全文感兴趣还请留意本系列後续发展。
但也保留了关键的检查登入的部分,也依然能够看出爲何应用知道有人登入授权,看似自己登入一样。

相似登入的应用在之前也聊过了。这次比较特别的是QTimer那一段吧!这次很清楚知道每5秒会去检查一次是否有人登入授权。也就是在登入授权以後,最多可能等待个五秒才会更新画面。

实际上轮询(polling)的方式可能不是唯一一种检查方法。甚至这种方式某些程度上感觉有点笨。如果授权服务器和登入的应用有很高度的亲密性的话,获取可以在登入後,由验证授权服务器进一步通知应用去取得存取权杖。不过要这样处理,不是应用还需要提供一个API端点接受通知,就是需要与验证授权服务器建立一个长连线。相对来说轮询方式真的简单很多。

参考资料


<<:  企业资料通讯Week4 (3) | HTTP message

>>:  网路是怎样连接的(九)TCP的性能优化(下)

Day27 - 动态模型 part2 (LSTM with attention)

回顾一下昨天提到的,我们希望透过将 attention 机制加到 LSTM 中藉此找出每段语音中重要...

Day16-Vue CLI 环境设定与打包部属

package.json专案与套件相依设定档 在开启专案时我们使用的npm run serve指令就...

Day 08 Section Summary 2

Mbed Simulator Importance of Mbed platform in rapi...

Day 21: SOLID 设计原则 — DIP (待改进中... )

「依赖反向原则 (Dependency Inversion Principle, DIP) 告诉我...

DAY03随机森林演算法

那今天,我打算一步一步写出演算法,顺便跟大家分享关於我的理解,首先决策树算法有ID3和C4.5和CA...