本文基於《Flask Web開發實戰》(噹噹滿100減50)的刪減內容改寫而來,更多Flask文章和開源專案可以訪問helloflask.com檢視。
執行示例程式
本文示例程式的原始碼託管在GitHub上(helloflask/github-login)。執行示例程式的步驟如下:
$ git clone https://github.com/helloflask/github-login.git
$ cd github-login
$ pipenv install # 如果沒有安裝pipenv,那麼執行pip install pipenv安裝
$ flask run # 在此之前需要在GitHub註冊OAuth程式並將客戶端ID與金鑰寫入程式,具體見上文複製程式碼
提示 如果你想直接體驗程式,可以訪問線上Demo。
第三方登入
簡單來說,為一個網站新增第三方登入指的是提供通過其他第三方平臺賬號登入當前網站的功能。比如,使用QQ、微信、新浪微博賬號登入。對於某些網站,甚至可以僅提供社交賬號登入的選項,這樣網站本身就不需要管理使用者賬戶等相關資訊。對使用者來說,使用第三方登入可以省去註冊的步驟,更加方便和快捷。
如果專案和GitHub、開源專案、程式語言等方面相關,或是面向的主要使用者群是程式設計師時,可以僅支援GitHub的第三方登入,比如Gitter、GitBook、Coveralls和Travis CI等。在Flask程式中,除了手動實現,我們可以藉助其他擴充套件或庫,我們在這篇文章裡要使用的GitHub-Flask擴充套件專門用於實現GitHub第三方登入,以及與GitHub進行Web API資源互動。
附註 第三方登入的原理是與第三方服務進行OAuth認證互動的,這裡不會詳細介紹OAuth,具體可以閱讀OAuth官網列出的資源。
第三方登入授權流程
起這個標題是為了更好理解,具體來說,整個流程實際上是指OAuth2中Authorization Code模式的授權流程。為了便於理解,這裡按照實際操作順序列出了整個授權流程的實現步驟:
-
在GitHub為我們的程式註冊OAuth程式,獲得Client ID(客戶端ID)和Client Secret(客戶端金鑰)。
-
我們在登入頁面新增“使用GitHub登入”按鈕,按鈕的URL指向GitHub提供的授權URL,即github.com/login/oauth…。
-
使用者點選登入按鈕,程式訪問GitHub的授權URL,我們在授權URL後附加查詢引數Client ID以及可選的Scope等。GitHub會根據授權URL中的Client ID識別出我們的程式資訊,根據scope獲取請求的許可權範圍,最後把這些資訊顯示在授權頁面上。
-
使用者輸入GitHub的賬戶及密碼,同意授權
-
使用者同意授權後GitHub會將使用者重定向到我們註冊OAuth程式時提供的回撥URL。如果使用者同意授權,回撥URL中會附加一個code(即Authorization Code,通常稱為授權碼),用來交換access令牌(即訪問令牌,也被稱為登入令牌、存取令牌等)。
-
我們在程式中接受到這個回撥請求,獲取code,傳送一個POST請求到用於獲取access令牌的URL,並附加Client ID、Client Secret和code值以及其他可選的值。
-
GitHub接收到請求後,驗證code值,成功後會再次向回撥URL發起請求,同時在URL的查詢字串中或請求主體中加入access令牌的值、過期時間、token型別等資訊。
-
我們的程式獲取access令牌,可以用於後續發起API資源呼叫,或儲存到資料庫備用
-
如果使用者是第一次登入,就建立使用者物件並儲存到資料庫,最後登入使用者
-
這裡可選的步驟是讓使用者設定密碼或資料
在GitHub註冊OAuth程式
和其他主流第三方服務相同,GitHub使用OAuth2中的Authorization Code模式認證。因為認證後,根據授權的許可權,客戶端可以獲取到使用者的資源,為了便於對客戶端進行識別和限制,我們需要在GitHub上進行註冊,獲取到客戶端ID和金鑰才能進行OAuth授權。
在服務提供方的網站上進行OAuth程式註冊時,通常需要提供程式的基本資訊,比如程式的名稱、描述、主頁等,這些資訊會顯示在要求使用者授權的頁面上,供使用者識別。在GitHub中進行OAuth程式註冊非常簡單,訪問github.com/settings/ap…填寫登錄檔單(如果你沒有GitHub賬戶,那麼需要先註冊一個才能訪問這個頁面。),登錄檔單各個欄位的作用和示例如圖所示:
表單中的資訊都可以後續進行修改。在開發時,程式的名稱、主頁和描述可以使用臨時的佔位內容。但Callback URL(回撥URL)需要正確填寫,這個回撥URL用來在使用者確認授權後重定向到程式中。因為我們需要在本地開發時進行測試,所以需要填寫本地程式的URL,比如http://127.0.0.1:5000/callback/github,我們需要建立處理這個請求的檢視函式,在這個檢視函式中獲取回撥URL附加的資訊,後面會詳細介紹。
注意 這裡因為是在開發時進行本地測試,所以填寫了程式執行的地址,在生產環境要避免指定埠。另外,在這裡localhost和127.0.0.1將會被視為兩個地址。在程式部署上線時,你需要將這些地址更換為真實的網站域名地址。
GITHUB_CLIENT_ID = `GitHub客戶端ID`
GITHUB_CLIENT_SECRET = `GitHub客戶端金鑰`複製程式碼
安裝並初始化GitHub-Flask
首先使用pip或Pipenv等工具安裝GitHub-Flask:
$ pip install github-flask複製程式碼
from flask import Flask
from flask_github import GitHub
app = Flask(__name__)
github = GitHub(app)複製程式碼
from flask import Flask
from flask_github import GitHub
github = 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-SQLAlchemy
app.config[`SQLALCHEMY_DATABASE_URI`] = `sqlite:///` + os.path.join(app.root_path, `data.db`)
app.config[`SQLALCHEMY_TRACK_MODIFICATIONS`] = False
db = 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_request
def 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-
$ 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的文件)。
這三個引數都可以在呼叫github.authorize()方法時使用對應的名稱作為關鍵字引數傳入。
如果不設定scope,GitHub-Flask擴充套件預設設定為None,那麼會擁有的許可權是獲取使用者的公開資訊。但是因為我們需要測試為專案加星(star)的操作,所以需要請求名為repo的許可權值。
附註 選擇scope時儘量只選擇需要的內容,申請太多的許可權可能會被使用者拒絕。GitHub提供的所有的可用scope列表及其說明可以在GitHub開發文件看到。
獲取access令牌(訪問令牌)
現在程式會重定向到GitHub的授權頁面(會先要求登入GitHub),如下圖所示:
當使用者同意授權或拒絕授權後,GitHub會將使用者重定向到我們設定的callback URL,我們需要建立一個檢視函式來處理回撥請求。如果使用者同意授權,GitHub會在重定向的請求中加入code引數,一個臨時生成的值,用於程式再次發起請求交換access token。程式這時需要向請求訪問令牌URL(即https://github.com/login/oauth/access_token)發起一個POST請求,附帶客戶端ID、客戶端金鑰、code以及可選的redirect_uri和state。請求成功後的的響應會包含訪問令牌(Access Token)。
@app.route(`/callback/github`)
@github.authorized_handler
def authorized(access_token):
if access_token is None:
flash(`Login failed.`)
return redirect(url_for(`index`))
# 下面會進行建立新使用者,儲存訪問令牌,登入使用者等操作,具體見後面
...
return redirect(url_for(`chat.app`))複製程式碼
獲取和操作使用者在GitHub上的資源
-
判斷使用者是否已經存在於資料庫中,如果存在就登入使用者,更新訪問令牌值(因為access是有過期時間的)
-
如果資料庫中沒有該使用者,那麼建立一個新的使用者記錄,傳入對應的資料,最後登入使用者
在這個示例程式中,我們使用使用者名稱(username)作為使用者的唯一標識,為了從資料庫中查詢對應的使用者,我們需要獲取使用者在GitHub上的使用者名稱。
response = github.get(`user`, access_token=access_token)複製程式碼
/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/)中看到。
username = response[`login`]複製程式碼
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複製程式碼
@github.access_token_getter
def token_getter():
user = g.user
if user is not None:
return user.access_token複製程式碼
當你在某處直接使用github.get()等方法而不傳入訪問令牌時,GitHub-Flask會通過你提供的這個回撥函式來獲取訪問令牌。
現在,我們的主頁檢視需要更新,對於登入的使用者,我們將會顯示使用者在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`))複製程式碼
@app.route(`/callback/github`)
@github.authorized_handler
def 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`))
...複製程式碼
相關連結
-
示例程式原始碼:github.com/helloflask/…
-
OAuth2官網:oauth.net/2/
-
GitHub-Flask專案主頁:github.com/cenkalti/gi…
-
GitHub-Flask文件:github-flask.readthedocs.org/
-
GitHub OAuth文件:developer.github.com/apps/buildi…
-
GitHub Web API文件:developer.github.com/v3/