本系列文之後也会置於个人网站
本篇应一些後续考量移除了部分内容。若对本篇有兴趣还请多关注本系列後续消息。
(不过有可能会改用Electron.js改写,虽然架构上会再麻烦一些。在此前也可以留言让我知道想看PySide完整版本还是Electron.js完整版本)
这次应用使用PySide
来实现界面;qrcode
来产生需要的QR Code;并使用requests
来与身份验证与授权服务器的API沟通。现在透过pip
进行安装需要的packages。
pip install PySide6 requests qrcode
其实本来可以考虑用electron.js,但是基於一些考量,最後决定使用PySide。
在昨天,透过Qt Designer建立了两个需要的使用者界面,今天来实现逻辑部分。
在之前所设计的ui档案分别是:example-device-code-app.ui
和login-dialog.ui
。这部分会分别将这两份载入到类别内使用。所以同样来建立两个Widgets:ExampleDeviceCodeApp
和loginDialog
。
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相关的处理。
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 Id
和Client Secret
。
device_code
的方法提供一个方式,透过requests.post
处理device code的endpint来取得device_code
会user_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_token
和refresh_token
。
def vertifyDeviceCode(self,
device_code: str,
*,
client_id=None,
client_secret=None):
#......
pass
也同样透过requests
呼叫相对应API的endpoint来取得使用者资讯。
def getUserInfo(self, access_token: str):
#......
pass
最後建立一个实例供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)
再回到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回传的资料而言,通常还包含interval
和expires_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
透过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 (略过)
每隔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
回顾一下昨天提到的,我们希望透过将 attention 机制加到 LSTM 中藉此找出每段语音中重要...
package.json专案与套件相依设定档 在开启专案时我们使用的npm run serve指令就...
Mbed Simulator Importance of Mbed platform in rapi...
「依赖反向原则 (Dependency Inversion Principle, DIP) 告诉我...
那今天,我打算一步一步写出演算法,顺便跟大家分享关於我的理解,首先决策树算法有ID3和C4.5和CA...