在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'))
        ...复制代码
若是你想让用户也能够直接使用帐户密码登陆,那么能够在受权成功后重定向到新的页面请求用户设置密码。

相关连接

相关文章
相关标签/搜索