本文基於《Flask Web開發實戰》(噹噹滿100減50)的刪減內容改寫而來,更多Flask文章和開源項目能夠訪問helloflask.com查看。html
本文示例程序的源碼託管在GitHub上(helloflask/github-login)。運行示例程序的步驟以下:node
$ git clone https://github.com/helloflask/github-login.git
$ cd github-login
$ pipenv install # 若是沒有安裝pipenv,那麼執行pip install pipenv安裝
$ flask run # 在此以前須要在GitHub註冊OAuth程序並將客戶端ID與密鑰寫入程序,具體見上文複製代碼
提示 若是你想直接體驗程序,能夠訪問在線Demo。python
簡單來講,爲一個網站添加第三方登陸指的是提供經過其餘第三方平臺帳號登入當前網站的功能。好比,使用QQ、微信、新浪微博帳號登陸。對於某些網站,甚至能夠僅提供社交帳號登陸的選項,這樣網站自己就不須要管理用戶帳戶等相關信息。對用戶來講,使用第三方登陸能夠省去註冊的步驟,更加方便和快捷。git
若是項目和GitHub、開源項目、編程語言等方面相關,或是面向的主要用戶羣是程序員時,能夠僅支持GitHub的第三方登陸,好比Gitter、GitBook、Coveralls和Travis CI等。在Flask程序中,除了手動實現,咱們能夠藉助其餘擴展或庫,咱們在這篇文章裏要使用的GitHub-Flask擴展專門用於實現GitHub第三方登陸,以及與GitHub進行Web API資源交互。程序員
附註 第三方登陸的原理是與第三方服務進行OAuth認證交互的,這裏不會詳細介紹OAuth,具體能夠閱讀OAuth官網列出的資源。github
起這個標題是爲了更好理解,具體來講,整個流程其實是指OAuth2中Authorization Code模式的受權流程。爲了便於理解,這裏按照實際操做順序列出了整個受權流程的實現步驟:sql
和其餘主流第三方服務相同,GitHub使用OAuth2中的Authorization Code模式認證。由於認證後,根據受權的權限,客戶端能夠獲取到用戶的資源,爲了便於對客戶端進行識別和限制,咱們須要在GitHub上進行註冊,獲取到客戶端ID和密鑰才能進行OAuth受權。數據庫
在服務提供方的網站上進行OAuth程序註冊時,一般須要提供程序的基本信息,好比程序的名稱、描述、主頁等,這些信息會顯示在要求用戶受權的頁面上,供用戶識別。在GitHub中進行OAuth程序註冊很是簡單,訪問github.com/settings/ap…填寫註冊表單(若是你沒有GitHub帳戶,那麼須要先註冊一個才能訪問這個頁面。),註冊表單各個字段的做用和示例如圖所示:django
表單中的信息均可之後續進行修改。在開發時,程序的名稱、主頁和描述可使用臨時的佔位內容。但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客戶端密鑰'複製代碼
首先使用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爲大寫。
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開發文檔看到。
如今程序會重定向到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'))複製代碼
在這個示例程序中,咱們使用用戶名(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'))
...複製代碼