在Flask程序中實現GitHub登陸和GitHub資源交互

本文基於《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與密鑰寫入程序,具體見上文複製代碼

提示 若是你想直接體驗程序,能夠訪問在線Demopython

第三方登陸

簡單來講,爲一個網站添加第三方登陸指的是提供經過其餘第三方平臺帳號登入當前網站的功能。好比,使用QQ、微信、新浪微博帳號登陸。對於某些網站,甚至能夠僅提供社交帳號登陸的選項,這樣網站自己就不須要管理用戶帳戶等相關信息。對用戶來講,使用第三方登陸能夠省去註冊的步驟,更加方便和快捷。git

使用GitHub-Flask實現GitHub第三方登陸

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

附註 第三方登陸的原理是與第三方服務進行OAuth認證交互的,這裏不會詳細介紹OAuth,具體能夠閱讀OAuth官網列出的資源。github

第三方登陸受權流程

起這個標題是爲了更好理解,具體來講,整個流程其實是指OAuth2中Authorization Code模式的受權流程。爲了便於理解,這裏按照實際操做順序列出了整個受權流程的實現步驟:sql

  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/ap…填寫註冊表單(若是你沒有GitHub帳戶,那麼須要先註冊一個才能訪問這個頁面。),註冊表單各個字段的做用和示例如圖所示:django

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

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

註冊成功後,咱們會在重定向後的頁面看到咱們的Client ID(客戶端ID)和Client Secret(客戶端密鑰),咱們須要將這兩個值分別賦值給配置變量GITHUB_CLIENT_ID和GITHUB_CLIENT_SECRET:
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-

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中附加的可選參數以下所示:

這三個參數均可以在調用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(即https://github.com/login/oauth/access_token)發起一個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_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返回的響應後,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_getter
def 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_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'))
        ...複製代碼
若是你想讓用戶也能夠直接使用帳戶密碼登陸,那麼能夠在受權成功後重定向到新的頁面請求用戶設置密碼。

相關連接

相關文章
相關標籤/搜索