使用GitHub-Flask實現GitHub登錄和API交互

使用GitHub-Flask實現GitHub登錄和API交互

來自專欄 Hello, Flask!20 人贊了文章

這篇文章屬於「Flask常用擴展介紹系列」,這個系列的文章目錄索引可以在《Flask常用擴展介紹系列文章索引》看到。

前言

這篇文章大部分內容為《Flask Web開發實戰》第10章的刪減章節,另外摘取了部分書中現有的內容。我為這篇文章單獨編寫了示常式序,GitHub倉庫地址為helloflask/github-login。運行示常式序的步驟如下:

$ git clone https://github.com/helloflask/github-login.git$ cd github-login$ pipenv install --skip-lock # 如果沒有安裝pipenv,那麼執行pip install pipenv$ flask run # 在此之前需要在GitHub註冊OAuth程序並將客戶端ID與密鑰寫入程序,具體見下文

如果你想直接體驗程序,可以訪問部署在PythonAnywhere(「到處都是蛇」)的在線實例。

附註 第三方登錄的原理是與第三方服務進行OAuth認證交互的,這裡不會詳細介紹OAuth,具體可以閱讀OAuth官網列出的資源,另外即將上市的Flask新書里也包含OAuth客戶端、伺服器端實現以及PythonAnywhere部署等相關內容。

什麼是第三方登錄

簡單來說,為一個網站添加第三方登錄指的是提供通過其他第三方平台賬號登入當前網站的功能。比如,使用QQ、微信、新浪微博賬號登錄。對於某些網站,甚至可以僅提供社交賬號登錄的選項,這樣網站本身就不需要管理用戶賬戶等相關信息。對用戶來說,使用第三方登錄可以省去註冊的步驟,更加方便和快捷。

如果項目和GitHub、開源項目、編程語言等方面相關,或是面向的主要用戶群是程序員時,可以僅支持GitHub的第三方登錄,比如Gitter、GitBook、Coveralls和Travis CI等。在Flask程序中,除了手動實現,我們可以藉助其他擴展或庫,我們在這篇文章里要使用的GitHub-Flask擴展專門用於實現GitHub第三方登錄,以及與GitHub進行Web API資源交互。

第三方登錄授權流程

起這個標題是為了更好理解,具體來說,整個流程實際上是指OAuth2中Authorization Code模式的授權流程。為了便於理解,這裡按照實際操作順序列出了整個授權流程的實現步驟:

  1. 在GitHub為我們的程序註冊OAuth程序,獲得Client ID(客戶端ID)和Client Secret(客戶端密鑰)。
  2. 我們在登錄頁面添加「使用GitHub登錄」按鈕,按鈕的URL指向GitHub提供的授權URL,即github.com/login/oauth/
  3. 用戶點擊登錄按鈕,程序訪問GitHub的授權URL,我們在授權URL後附加查詢參數Client ID以及可選的Scope等。GitHub會根據授權URL中的Client ID識別出我們的程序信息,根據scope獲取請求的許可權範圍,最後把這些信息顯示在授權頁面上。
  4. 用戶輸入GitHub的賬戶及密碼,同意授權
  5. 用戶同意授權後GitHub會將用戶重定向到我們註冊OAuth程序時提供的回調URL。如果用戶同意授權,回調URL中會附加一個code(即Authorization Code,通常稱為授權碼),用來交換access令牌(即訪問令牌,也被稱為登錄令牌、存取令牌等)。
  6. 我們在程序中接受到這個回調請求,獲取code,發送一個POST請求到用於獲取access令牌的URL,並附加Client ID、Client Secret和code值以及其他可選的值。
  7. GitHub接收到請求後,驗證code值,成功後會再次向回調URL發起請求,同時在URL的查詢字元串中或請求主體中加入access令牌的值、過期時間、token類型等信息。
  8. 我們的程序獲取access令牌,可以用於後續發起API資源調用,或保存到資料庫備用
  9. 如果用戶是第一次登入,就創建用戶對象並保存到資料庫,最後登入用戶
  10. 這裡可選的步驟是讓用戶設置密碼或資料

在GitHub註冊OAuth程序

和其他主流第三方服務相同,GitHub使用OAuth2中的Authorization Code模式認證。因為認證後,根據授權的許可權,客戶端可以獲取到用戶的資源,為了便於對客戶端進行識別和限制,我們需要在GitHub上進行註冊,獲取到客戶端ID和密鑰才能進行OAuth授權。

在服務提供方的網站上進行OAuth程序註冊時,通常需要提供程序的基本信息,比如程序的名稱、描述、主頁等,這些信息會顯示在要求用戶授權的頁面上,供用戶識別。在GitHub中進行OAuth程序註冊非常簡單,訪問github.com/settings/app填寫註冊表單(如果你沒有GitHub賬戶,那麼需要先註冊一個才能訪問這個頁面。),註冊表單各個欄位的作用和示例如圖所示:

在GitHub註冊OAuth程序

表單中的信息都可以後續進行修改。在開發時,程序的名稱、主頁和描述可以使用臨時的佔位內容。但Callback URL(回調URL)需要正確填寫,這個回調URL用來在用戶確認授權後重定向到程序中。因為我們需要在本地開發時進行測試,所以需要填寫本地程序的URL,比如127.0.0.1:5000/callback,我們需要創建處理這個請求的視圖函數,在這個視圖函數中獲取回調URL附加的信息,後面會詳細介紹。

注意 這裡因為是在開發時進行本地測試,所以填寫了程序運行的地址,在生產環境要避免指定埠。另外,在這裡localhost和127.0.0.1將會被視為兩個地址。在程序部署上線時,你需要將這些地址更換為真實的網站域名地址。

註冊成功後,我們會在重定向後的頁面看到我們的Client ID(客戶端ID)和Client Secret(客戶端密鑰),我們需要將這兩個值分別賦值給配置變數GITHUB_CLIENT_ID和GITHUB_CLIENT_SECRET:

GITHUB_CLIENT_ID = GitHub客戶端IDGITHUB_CLIENT_SECRET = GitHub客戶端密鑰

注意 示常式序中為了便於測試,直接在腳本中寫出了,在生產環境下,你應該將它們寫入到環境變數,然後在腳本中從環境變數讀取。

安裝並初始化GitHub-Flask

首先使用pip或Pipenv等工具安裝GitHub-Flask:

$ pip install github-flask

和其他擴展類似,你可以使用下面的方式初始化擴展(注意擴展類大小寫):

from flask import Flaskfrom flask_github import GitHubapp = Flask(__name__)github = GitHub(app)

如果你使用工廠函數創建程序,那麼可以使用下面的方式初始化擴展:

from flask import Flaskfrom flask_github import GitHubgithub = GitHub()... def create_app(): app = Flask(__name__) github.init_app(app) ... return app

注意 雖然擴展名稱是GitHub-Flask,但實際的包名稱仍然是flask_github(Flask擴展名稱可以倒置(即「Foo-Flask」),但包名稱的形式必須為「flask_foo「。)。另外要注意擴展類的拼寫,其中H為大寫。

準備工作

在示常式序中,我們首先進行了下面的基礎工作:

  • 定義基本配置
  • 創建一個簡單的用戶模型來存儲用戶信息(使用Flask-SQLAlchemy)
  • 實現登錄和註銷的管理功能(使用session實現,可以使用Flask-Login簡化)
  • 創建用於初始化資料庫的命令函數

app = Flask(__name__)app.config[SECRET_KEY] = os.getenv(SECRET_KEY, secret string)# Flask-SQLAlchemyapp.config[SQLALCHEMY_DATABASE_URI] = sqlite:/// + os.path.join(app.root_path, data.db)app.config[SQLALCHEMY_TRACK_MODIFICATIONS] = Falsedb = SQLAlchemy(app)# 命令函數@app.cli.command()@click.option(--drop, is_flag=True, help=Create after drop.)def initdb(drop): """Initialize the database.""" if drop: db.drop_all() db.create_all() click.echo(Initialized database.)# 存儲用戶信息的資料庫模型類class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100)) # 用戶名 access_token = db.Column(db.String(200)) # 授權完成後獲取的訪問令牌# 管理每個請求的登錄狀態,如果已登錄(session里有用戶id值),將模型類對象保存到g對象中@app.before_requestdef before_request(): g.user = None if user_id in session: g.user = User.query.get(session[user_id])# 登入@app.route(/login)def login(): if session.get(user_id, None) is None: ... # 進行OAuth授權流程,具體見後面 flash(Already logged in.) return redirect(url_for(index))# 登出@app.route(/logout)def logout(): session.pop(user_id, None) flash(Goodbye.) return redirect(url_for(index))

現在我們可以執行上面創建的initdb命令來創建資料庫和表(確保當前目錄在demos/github-login下):

$ flask initdb

創建登錄按鈕

我們在本節一開始詳細描述了以GitHub為例的完整的OAuth授權的過程,現在讓我們來創建登錄按鈕。示常式序非常簡單,只包含一個主頁(index.html),這個頁面由index視圖處理:

@app.route(/)def index(): is_login = True if g.user else False # 判斷用戶登錄狀態 return render_template(index.html, is_login=is_login)

這個視圖在渲染模板時傳入了用於判斷用戶登錄狀態的is_login變數,我們在模板中根據這個變數渲染不同的元素,如果已經登入,顯示退出按鈕,否則顯示登入按鈕:

{% if is_login %} <a class="btn" href="{{ url_for(logout) }}">Logout</a>{% else %} <a class="btn" href="{{ url_for(login) }}">Login with GitHub</a>{% endif %}

未登錄情況下的主頁如下圖所示:

登錄按鈕

在實際的項目中,你可以使用GitHub的logo來讓登錄按鈕更好看一些。

提示 使用Flask-Login時,你可以直接在模板中通過current_user.is_authenticated屬性來判斷用戶登入狀態。

發送授權請求

這個登錄按鈕的URL指向的是login視圖,這個視圖用來發送授權請求,如下所示:

@app.route(/login)def login(): if session.get(user_id, None) is None: # 判斷用戶登錄狀態 return github.authorize(scope=repo) flash(Already logged in.) return redirect(url_for(index))

在這個視圖中,如果用戶沒有登錄,我們就調用github.authorize()方法。這個方法會生成授權URL,並向這個URL發送請求。

附註 GitHub-Flask擴內置了除了客戶端ID和密鑰外所有必要的URL,比如API的URL,獲取訪問令牌的URL等(我們也可以通過相應的配置鍵進行修改,具體參考GitHub-Flask的文檔)。

發起認證請求的URL中必須加入的參數是客戶端ID,GitHub-Flask會自動使用我們之前通過配置變數傳入的值。在授權URL中附加的可選參數如下所示:

授權請求URL附加的可選查詢參數

這三個參數都可以在調用github.authorize()方法時使用對應的名稱作為關鍵字參數傳入。

如果不設置scope,GitHub-Flask擴展默認設置為None,那麼會擁有的許可權是獲取用戶的公開信息。但是因為我們需要測試為項目加星(star)的操作,所以需要請求名為repo的許可權值。

附註 選擇scope時盡量只選擇需要的內容,申請太多的許可權可能會被用戶拒絕。GitHub提供的所有的可用scope列表及其說明可以在GitHub開發文檔看到。

如果不設置redirect_uri,那麼GitHub會使用我們填寫的callback URL。但是需要注意的是,如果我們填寫了,那就必須和註冊程序時填寫的URL完全相同。我們在這裡沒有指定,因此將會使用註冊OAuth程序時設置的http://localhost:5000/callback/github

獲取access令牌(訪問令牌)

現在程序會重定向到GitHub的授權頁面(會先要求登錄GitHub),如下圖所示:

授權頁面

當用戶同意授權或拒絕授權後,GitHub會將用戶重定向到我們設置的callback URL,我們需要創建一個視圖函數來處理回調請求。如果用戶同意授權,GitHub會在重定向的請求中加入code參數,一個臨時生成的值,用於程序再次發起請求交換access token。程序這時需要向請求訪問令牌URL(即github.com/login/oauth/)發起一個POST請求,附帶客戶端ID、客戶端密鑰、code以及可選的redirect_uri和state。請求成功後的的響應會包含訪問令牌(Access Token)。

很幸運,上面的一系列工作GitHub-Flask會在背後替我們完成。我們只需要創建一個視圖函數,定義正確的URL規則(這裡的URL規則需要和GitHub上填寫的Callback URL匹配),並為其附加一個github.authorized_handler裝飾器。另外,這個函數要接受一個access_token參數,GitHub-Flask會在授權請求結束後通過這個參數傳入訪問令牌,如下所示:

@app.route(/callback/github)@github.authorized_handlerdef authorized(access_token): if access_token is None: flash(Login failed.) return redirect(url_for(index)) # 下面會進行創建新用戶,保存訪問令牌,登入用戶等操作,具體見後面 ... return redirect(url_for(chat.app))

接受到GitHub返回的響應後,GitHub-Flask會調用這個authorized()函數,並傳入access_token的值。如果授權失敗,access_token的值會是None,這時我們重定向到主頁頁面,並顯示一個錯誤消息。如果access_token不為None,我們會進行創建新用戶,保存訪問令牌,登入用戶等操作,具體見下一節。

獲取用戶在GitHub上的資源

在獲取到訪問令牌後,我們需要做下面的工作:

  • 判斷用戶是否已經存在於資料庫中,如果存在就登入用戶,更新訪問令牌值(因為access是有過期時間的)
  • 如果資料庫中沒有該用戶,那麼創建一個新的用戶記錄,傳入對應的數據,最後登入用戶

在這個示常式序中,我們使用用戶名(username)作為用戶的唯一標識,為了從資料庫中查找對應的用戶,我們需要獲取用戶在GitHub上的用戶名。

如果授權成功,那麼我們就使用這個訪問令牌向GitHub提供的Web API的/user端點發起一次GET請求。這可以通過GitHub-Flask提供的get()方法實現,傳入訪問令牌作為access_token參數的值。我們把表示用戶的資源端點「user」傳入get()方法,因為GitHub-Flask會自動補全完整的請求URL,即api.github.com/user

response = github.get(user, access_token=access_token)

提示 GitHub-Flask提供了一系列方法來調用GitHub通過Web API開放的資源。和在jQuery為AJAX提供的方法類似,它提供了底層的request()方法和方便的get()、post()、put()、delete()等方法(這些方法內部會調用request方法),可以用來發送不同HTTP方法的請求。

/user端點對應用戶資料,返回的JSON數據如下所示:

{ "avatar_url": "https://avatars3.githubusercontent.com/u/12967000?v=4", "bio": null, "blog": "greyli.com", "company": "None", "created_at": "2015-06-19T13:00:23Z", "email": "withlihui@gmail.com", "events_url": "https://api.github.com/users/greyli/events{/privacy}", "followers": 132, "followers_url": "https://api.github.com/users/greyli/followers", "following": 8, "following_url": "https://api.github.com/users/greyli/following{/other_user}", "gists_url": "https://api.github.com/users/greyli/gists{/gist_id}", "gravatar_id": "", "hireable": true, "html_url": "https://github.com/greyli", "id": 12967000, "location": "China", "login": "greyli", "name": "Grey Li", "node_id": "MDQ6VXNlcjEyOTY3MDAw", "organizations_url": "https://api.github.com/users/greyli/orgs", "public_gists": 7, "public_repos": 61, "received_events_url": "https://api.github.com/users/greyli/received_events", "repos_url": "https://api.github.com/users/greyli/repos", "site_admin": false, "starred_url": "https://api.github.com/users/greyli/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/greyli/subscriptions", "type": "User", "updated_at": "2018-06-24T02:05:38Z", "url": "https://api.github.com/users/greyli"}

附註 用戶端點返回的響應示例以及其他所有開放的資源端點可以在GitHub的API文檔(developer.github.com/v3)中看到。

GitHub-Flask會把GitHub的JSON響應主體解析為一個字典並返回,我們使用對應的鍵獲取這些數據。其中登錄用戶名使用login作為鍵獲取:

username = response[login]

獲取到用戶名後,我們判斷是否已存在該用戶,如果存在更新access_token欄位值;如果不存在則創建一個新的User實例,把用戶名和訪問令牌存儲到用戶模型的對應欄位里:

user = User.query.filter_by(username=username).first()if user is None: user = User(username=username, access_token=access_token) db.session.add(user) user.access_token = access_token # update access token db.session.commit()

最後,我們登入對應的用戶對象或是新創建的用戶對象(將用戶id寫入session):

flash(Login success.)# log the user in# if you use flask-login, just call login_user() here.session[user_id] = user.id

因為我們需要在其他視圖裡調用GitHub資源,為了避免每次都獲取和傳入訪問令牌,我們可以使用github.access_token_getter裝飾器創建一個統一的令牌獲取函數:

@github.access_token_getterdef token_getter(): user = g.user if user is not None: return user.access_token

當你在某處直接使用github.get()等方法而不傳入訪問令牌時,GitHub-Flask會通過你提供的這個回調函數來獲取訪問令牌。

注意 雖然在很多開源庫的示常式序中,都會把access令牌存儲到session中,但session不能用來存儲敏感信息(具體可以訪問專欄的這篇文章了解)。所以除了作測試用途,在生產環境下正確的做法是把訪問令牌存儲到資料庫中。

現在,我們的主頁視圖需要更新,對於登錄的用戶,我們將會顯示用戶在GitHub上的資料:

@app.route(/)def index(): if g.user: is_login = True response = github.get(user) avatar = response[avatar_url] username = response[name] url = response[html_url] return render_template(index.html, is_login=is_login, avatar=avatar, username=username, url=url) is_login = False return render_template(index.html, is_login=is_login)

類似的,我們使用github.get()方法獲取/user端點的用戶資料,因為設置了令牌獲取函數,所以不用顯式的傳入訪問令牌值。這些數據(頭像、顯示用戶名和GitHub用戶主頁URL)將會顯示在主頁,如下圖所示:

因為我們在進行授權時請求了repo許可權,我們還可以對用戶的倉庫進行各類操作,示常式序中添加了一個加星的示例,如果你登錄後點擊主頁的「Star HelloFlask on GitHub」按鈕,就會加星對應的倉庫。這個按鈕指向的star視圖如下所示:

@app.route(/star/helloflask)def star(): github.put(user/starred/greyli/helloflask, headers={Content-Length: 0}) flash(Star success.) return redirect(url_for(index))

完整的用於處理回調請求的authorized()視圖函數如下所示:

@app.route(/callback/github)@github.authorized_handlerdef authorized(access_token): if access_token is None: flash(Login failed.) return redirect(url_for(index)) response = github.get(user, access_token=access_token) username = response[login] # get username user = User.query.filter_by(username=username).first() if user is None: user = User(username=username, access_token=access_token) db.session.add(user) user.access_token = access_token # update access token db.session.commit() flash(Login success.) # log the user in # if you use flask-login, just call login_user() here. session[user_id] = user.id return redirect(url_for(index))

走進現實

一次完整的OAuth認證就這樣完成了。在實際的項目中,支持第三方登錄後,我們需要對原有的登錄系統進行調整。通過第三方認證創建的用戶沒有密碼,所以如果這部分用戶使用傳統方式登錄的話會出現錯誤。我們添加一個if判斷,如果用戶對象的password_hash欄位(存儲密碼散列值)為空時,我們會返回一個錯誤提示,提醒用戶使用上次使用的第三方服務進行登錄,比如:

@app.route(/login, methods=[GET, POST])def login(): ... if request.method == POST: ... user = User.query.filter_by(email=email).first() if user is not None: if user.password_hash is None: flash(Please use the third patry service to log in.) return redirect(url_for(.login)) ...

如果你想讓用戶也可以直接使用賬戶密碼登錄,那麼可以在授權成功後重定向到新的頁面請求用戶設置密碼。

相關鏈接

  • 在線實例:helloflask.pythonanywhere.com
  • 示常式序源碼:github.com/helloflask/g
  • GitHub-Flask項目主頁:github.com/cenkalti/git
  • GitHub-Flask文檔:github-flask.readthedocs.org
  • GitHub OAuth文檔:developer.github.com/ap
  • GitHub Web API文檔:developer.github.com/v3

推薦閱讀:

Python按F5運行出現invalid syntax?
[code]批量視頻/圖片 切圖做樣本
python os模塊學習
MYSQL轉SQL Server腳本
手把手教你用Python進行回歸(附代碼、學習資料)

TAG:GitHub | Flask | Python |