2016年1月4日 星期一

如何在 GAE 上佈署 jQuery EasyUI 專案 (四) : Google 登入

在跨越 2015 與 2016 的兩天, 因為突然想到以前曾經在 GAE 上佈署過 ExtJS 與 jQueryUI 專案, 一時性起想說也用 EasyUI 試試看, 沒想到自己忘了早就測試過了. 不過那時是自備函式庫資源, 有點麻煩, 我想何不採用 CDN 試試看, 不但可以節省硬碟空間, 也省掉安排檔案的麻煩.

在前兩篇測試紀錄中, 我使用 Google 的 Datastore 資料儲存庫來儲存使用者資料, 其實 GAE 也提供 users API 模組可以讓我們利用 Gmail 帳戶進行基本的身分認證, 也就是使用者必須登入 Gmail 帳號後才能進入我們的網頁應用程式. 應用程式使用 users API 只能取得使用者的 email 帳號與暱稱, 因為認證是透過 Google 進行, 並非應用程式, 因此使用者不用擔心洩漏密碼等資訊.

接下來打算在之前的兩篇文章基礎上, 測試 Google 使用者登入模組的功能, 參考之前的文章 :

# GAE 的資料儲存 (Datastore)
# 如何在 GAE 中處理 HTTP 要求
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (一)
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (二)
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (三)

下列測試延續上篇 "如何在 GAE 上佈署 jQuery EasyUI 專案 (三)"  中的測試三修改. 要使用 Google 帳號驗證首先必須匯入 users 模組, 在主控程式 main.py 中加入下列匯入指令 :

from google.appengine.api import users

呼叫此模組的 get_current_user() 函式會傳回一個 User 類別的實體, 其中包含了目前此瀏覽器已登入 Google 的使用者帳號資料 (暱稱與 email), 如果尚未登入則傳回 Null.

然後增加 google_user_login_1 這個 URL 路徑 (僅列出增加或修改部分) :

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1)
], debug=True)

亦即當收到 /google_user_login_1 的存取要求時, 由 google_user_login_1 類別處理, 所以相對地也要增加處理此路徑的類別與 get 函式, 作法有三種, 第一種是以程式來判斷處理 :

class google_user_login_1(webapp2.RequestHandler):
    def get(self):
        user=users.get_current_user();
        if user:
            logout_url=users.create_logout_url(self.request.path)
            content=("歡迎 %s!<br>您已經以 %s 帳號登入了! (<a href='%s'>登出</a>)" %
                (user.nickname(), user.email(), logout_url))
        else:
            login_url=users.create_login_url(self.request.path)
            content=("您尚未登入 (<a href='%s'>登入</a>)" % login_url)
        self.response.out.write(content)

這裡我們用 users.get_current_user() 來取得此瀏覽器程序已登入 Google 帳號之使用者物件實體 user, 如果成功就顯示歡迎訊息, 並且附上帶有登出 Google 帳號之超連結. 透過呼叫 users 模組之 create_logout_url() 函式可取得登出 Google 帳號之 URL, 傳入參數是登出後要重導向之目的地 URL, 這裡我們傳入 self.request.path 表示登出後回到目前這個網址, 即 /google_user_login_1. 

若使用者尚未登入 Google 帳號, 就顯示附有登入超連結的訊息, 利用呼叫 users 模組的 create_user_login() 函式即可取得 Google 登入畫面的 URL. 注意這裡的 content 是兩個 tuple 的資料結構, 使用與 C 語言的 printf() 格式化輸出函式類似的語法來將變數填入 tuple 內的 %s 格式變數, 兩個 tuple 以 % 隔開, 而且變數一一相對應. 這裡我們直接將 content 這個 tuple 透過 response.out 物件輸出, 不另外製作一個 html 模板檔案, 實際範例如下所示 : 


在本地端測試結果如下 :


這只是在 GAE SDK 上面模擬的結果, 實際上並不會去 Google 驗證. 應用程式發布至 GAE 後, 真正登入畫面如下 : 


第二種做法是在定義要求處理類別時, 在開頭處使用 @login_required 這個 GAE 所提供的函式裝飾器 (decorator) 來強制導向至 Google 登入, 參考 :

# Google Cloud : Utility Functions

如下面測試 2 所示, 首先我們添加一個 /google_user_login_2 路徑 :

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2)
], debug=True)

然後定義要求處理類別如下 :

class google_user_login_2(webapp2.RequestHandler):
    @login_required
    def get(self):
        user=users.get_current_user();
        logout_url=users.create_logout_url(self.request.path)
        content=("歡迎 %s!<br>您已經以 %s 帳號登入了! (<a href='%s'>登出</a>)" %
                (user.nickname(), user.email(), logout_url))
        self.response.out.write(content)

由於必須通過 Google 登入才會進入 get() 函式, 因此這裡只要處理已登入情況即可. 實際測試連結如下 :

測試 2 : http://jqueryeasyui.appspot.com/google_user_login_2 

效果是一樣的, 只是少了登入前的頁面而已.

第三種作法是在 app.yaml 檔案中針對必須登入後才能進入之 url 加入 login: required 來強制登入. 首先開啟 app.yaml 檔, 然後於 main.app 前面加入針對路徑 /google_user_login_3 的登入要求 :

application: jqueryeasyui
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico
- url: /static
  static_dir: static
- url: /google_user_login_3
  script: main.app
  login: required
- url: .*
  script: main.app

libraries:
- name: webapp2
  version: "2.5.2"

注意, 此 URL 必須在 .* 之前, 否則不會被處理. 接著在 main.py 中加入 google_user_login_3 的路徑如下 :

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3)
], debug=True)

然後加入其路徑處理類別, 我們可以沿用上面 google_user_login_2 的複製過來改成  google_user_login_3 即可 :

class google_user_login_3(webapp2.RequestHandler):
    def get(self):
        user=users.get_current_user();
        logout_url=users.create_logout_url(self.request.path)
        nickname=user.nickname()
        content=("歡迎 %s!<br>您已經以 %s 帳號登入了! (<a href='%s'>登出</a>)" %
                (user.nickname(), user.email(), logout_url))
        self.response.out.write(content)

測試 3 : http://jqueryeasyui.appspot.com/google_user_login_3 

可見效果與上面測試 2 是一樣的. 以上都是在 main.py 裡直接輸出網頁, 比較不好控制板型, 應該用模板檔案為宜. 在下面的測試 4 裡就改用模板來輸出網頁, 我在 templates 下增加了一個模板檔案 google_user_login_4.htm :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
<p>歡迎! {{nickname}}<br>
您已經以 {{email}} 帳號登入了! (<a href="{{logout_url}}">登出</a>)</p>
{% endblock%}

這裡我們希望 main.py 在渲染此模板時能傳入 nickname, email, 以及 logout_url 這三個變數, 因此 main.py 增加了 google_user_login_4 的路徑與其要求處理類別如下 :

class google_user_login_4(webapp2.RequestHandler):
    @login_required
    def get(self):
        url="templates/google_user_login_4.htm"
        user=users.get_current_user();
        nickname=user.nickname()
        email=user.email()
        logout_url=users.create_logout_url("/google_user_login_4")
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,
            {'nickname':nickname,'email':email,'logout_url':logout_url})
        self.response.out.write(content)

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3),
    ('/google_user_login_4', google_user_login_4)
], debug=True)

這裡為了簡單計 (懶得再去 app.yaml 中添加路徑處理項目), 採用測試 2 的 @login_required 裝飾字來強制登入, 同時在呼叫 template 類別的 render() 方法渲染模板時, 傳入了nickname, email, 以及 logout_url 三個變數. 注意, JSON 的索引全部都要用單引號括起來 (雙引號不行, 這是 JSON 的標準格式), 否則啥也傳不過去. 實際測試範例如下 :

測試 4 : http://jqueryeasyui.appspot.com/google_user_login_4 

注意, 在渲染時不要傳遞含有 html 的字串, 否則會被當作文字處理, 例如剛開始時我把測試 3 的 content 當作變數傳過去, 這樣 google_user_logine_4.htm 模板裡只要用 {{content}} 接收即可, 但這樣卻得到如下渲染結果 :

歡迎 test@example.com!<br>您已經以 test@example.com 帳號登入了! (<a href='/_ah/login?continue=http%3A//localhost%3A11080/google_user_login_3&action=logout'>登出</a>)

另外, users 類別還有一個方法 is_current_user_admin() 可以判別登入者是否為 GAE 應用程式的管理者, 我在 main.py 中增加 google_user_login_5 路徑與其處理類別, 如下列測試 5 所示 :

class google_user_login_5(webapp2.RequestHandler):
    @login_required
    def get(self):
        user=users.get_current_user();
        logout_url=users.create_logout_url(self.request.path)
        if users.is_current_user_admin():
            content="您是此應用程式的管理者!"
        else:
            content=("歡迎 %s!<br>您已經以 %s 帳號登入了! (<a href='%s'>登出</a>)" %
                (user.nickname(), user.email(), logout_url))
        self.response.out.write(content)

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/easyui_1', easyui_1),
    ('/easyui_2', easyui_2),
    ('/easyui_3', easyui_3),
    ('/easyui_4', easyui_4),
    ('/login_4', login_4),
    ('/easyui_5', easyui_5),
    ('/login_5', login_5),
    ('/easyui_5_1', easyui_5_1),
    ('/home', home),
    ('/google_user_login_1', google_user_login_1),
    ('/google_user_login_2', google_user_login_2),
    ('/google_user_login_3', google_user_login_3),
    ('/google_user_login_4', google_user_login_4),
    ('/google_user_login_5', google_user_login_5)
], debug=True)

測試 5 : http://jqueryeasyui.appspot.com/google_user_login_5 

如果是這個應用程式的管理者, 登入後會顯示 "您是此應用程式的管理者!", 否則顯示一般登入訊息, 我以兩個 Gmail 帳號測試, 其中一個是本應用程式 jqueryeasyui 的管理帳號, 一個不是, 確實輸出正確結果 :


 

OK, Google User API 的測試完成! 可見 Google 登入其實沒啥料, 只能取得帳號而已, 如果需要儲存更多的使用者資料, 例如性別, 職業, 電話, 喜好等會員資訊, 還是得自己在 Datastore 實作會員資料表.

以上測試參考了上官林傑的 "Google應用服務引擎開發實戰" 這本書中的說明, 此書已絕版 (圖書館有), 但範例程式還可以在下列網址下載 (只到第四章而已) :

# Google應用服務引擎開發實戰範例程式碼

以上測試範例原始碼可從下列網址下載 :

# 下載原始碼


沒有留言 :