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應用服務引擎開發實戰範例程式碼

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

下載原始碼


沒有留言 :