1. OAuth 2.0 原理介绍
1.1 OAuth 2.0 概述
OAuth(开放授权) 是一个正式的互联网标准协议,OAuth 2.0 是它的后续版本,主要用于解决无需用户提供用户名和密码时,第三方应用程序能够获取用户存储在服务器提供商那里的某些数据(头像、用户名等)。例如,常用的使用微信、QQ或微博账号登录其他网站等情形。OAuth允许用户提供一个令牌(Token)给第三方网站,一个令牌对应一个特定的网站,同时该令牌只能在特定的时间内访问特定的资源。
- C和D可以在同一个服务器上
- 令牌有时效性和有效性,用户可以手动管理Token的有效性
1.2 OAuth 几种授权模式
-
授权码模式
后台服务器与服务提供商的服务器进行互动来获取Token,这个过程不通过用户的浏览器,是最安全的一种方法。
-
简化模式
没有授权流程中的C/D步骤,而是用户浏览器通过资源服务器发送的代码提取token,并直接发送给第三方应用程序。
- 密码模式
用户必须把密码给客户端,但是客户端不得存储密码,通常用于用户对客户端高度信任的情况下。比如客户端是操作系统的一部分,或者是由一个著名公司出品的产品,而认证服务器也只有在其他模式无法执行的情况下才考虑这种模式。
- 客户端模式
用户直接向客户端注册,客户端以自己的名义要求服务提供商提供服务,这其实不存在授权的问题。
1.3 OAuth 授权码模式
授权码模式是功能最完整、流程最严密的授权模式;简化模式不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,条狗了"授权码"这个步骤;密码模式需要用户向客户端提供自己的用户名和密码;客户端以自己的名义而不是以用户的名义向“服务提供商”进行认证。后几种模式都是授权码模式的简化版,我们这里着重讲解授权码模式。
2. 使用Python编写简单的基本功能
前面已经提到,OAuth 2.0 授权是一个三方协作的过程,为了进一步简化编码的难度,我们将第三方应用、授权服务器和资源服务器都集成在一台主机上面(也就是我们的个人编码电脑)的Flask程序上。当然,现实中是没有人这么做的,因为只要域名一致,网站就可以同村Cookie存取账号和密码信息以实现登录。不过,在这里,我们故意不存储Cookie信息,以模拟整个OAuth 2.0 的流程。这小节我们着重讲解授权服务器、资源服务器和客户端程序(第三方应用)的编码。
本小节内容,包含以下两点知识:
- 实现重定向机制
- 实现授权码机制
- 服务端代码 app.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on 2019年2月20日
@author: 杨现 成都天府机场建设指挥部
'''
import base64,random,time
from flask import Flask,request,redirect
# 定义APP应用
app =Flask(__name__)
# 定义用户名和密码
users={
"yangxian":["123456"]
}
auth_code={
}
# 生成token的函数,自定义一种token的格式
def gen_token(uid):
token=base64.b64encode(':'.join([str(uid), str(random.random()),str(time.time() + 7200)]))
users[uid].append(token)
return token
# 验证Token的有效性
def verify_token(token):
_token=base64.b64decode(token) # 解密token
if not users.get(_token.split(':')[0])[-1]==token:
return -1
if float(_token.split(':')[-1]) >= time.time():
return 1
else:
return 0
# 定义路由,即访问uri为:"http://localhost:5000/index"
# 5000是flask的默认端口
@app.route('/index',methods=['POST','GET'])
def index():
print(request.headers)
return 'hello'
# 登录时,如果用户名和密码匹配,则生成令牌
@app.route('/login',methods=['POST','GET'])
def login():
ori_use=request.headers['Authorization'].split(' ')[-1]
uid,pwd=bytes.decode(base64.b64decode(ori_use)).split(':')
if users.get(uid)[0]==pwd: # 如果用于的第一个密码等于此密码
return gen_token(uid)
else:
return 'error'
@app.route('/test',methods=['POST','GET'])
def test():
token=request.args.get('token')
if verify_token(token)==1:
return 'data' # 返回数据,即资源管理服务器的功能
else:
return 'error' # 返回错误信息
# 客户端登录的逻辑
"""
/client/login 相当于第三方网站
/oauth 相当于授权方,只是这里写到一起了,为了方便
"""
@app.route('/client/login',methods=['POST','GET'])
def client_login():
uri="http://localhost:5000/oauth"
return redirect(uri)
# token发放机制
@app.route('/oauth',methods=['POST','GET'])
def oauth():
# 授权码的发放
if request.args.get('code'):
# 如果授权码的对应的uri请求中的uri,则为其用户生成令牌
if auth_code.get(int(request.args.get('code')))==request.args.get('redirect_uti'):
return gen_token(request.args.get('client_id'))
return "please login"
# 生成授权码方法
def gen_code(uri):
code=random.randint(0,10000)
auth_code[code]=uri # 授权码对应的uri
return code
if __name__=="__main__":
app.run(debug=True)
- 用户端代码 request_t.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on 2019年2月20日
@author: 杨现 成都天府机场建设指挥部
'''
import requests
r=requests.get('http://127.0.0.1:5000/client/login',auth=('yangxian','123456'))
# print(r.text)
print(r.history) # 可以显示重定向的情况
3. 使用Flask实现简单的OAuth 2.0授权服务器
- 整合并优化之前的代码
- 讲解Requests 验证结果的步骤
- 启动服务器,并通过Requests进行验证
- 服务端代码 app.py (在上一节的代码中添加了部分代码)
#! /usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on 2019年2月20日
@author: Lenovo
'''
import base64,random,time
from flask import Flask,request,redirect
# 定义APP应用
app =Flask(__name__)
# 定义用户名和密码
users={
"yangxian":["123456"]
}
client_id='123456'
users[client_id]=[]
auth_code={ }
oauth_redirect_uri=[]
redirect_uri="http://localhost:5000/client/passport"
# 生成token的函数,自定义一种token的格式
def gen_token(uid):
token=base64.b64encode(':'.join([str(uid), str(random.random()),str(time.time() + 7200)]))
users[uid].append(token)
return token
# 验证Token的有效性
def verify_token(token):
_token=base64.b64decode(token) # 解密token
if not users.get(_token.split(':')[0])[-1]==token:
return -1
if float(_token.split(':')[-1]) >= time.time():
return 1
else:
return 0
# 定义路由,即访问uri为:"http://localhost:5000/index"
# 5000是flask的默认端口
@app.route('/index',methods=['POST','GET'])
def index():
print(request.headers)
return 'hello'
# 登录时,如果用户名和密码匹配,则生成令牌
@app.route('/login',methods=['POST','GET'])
def login():
ori_use=request.headers['Authorization'].split(' ')[-1]
uid,pwd=bytes.decode(base64.b64decode(ori_use)).split(':')
if users.get(uid)[0]==pwd: # 如果用于的第一个密码等于此密码
return gen_token(uid)
else:
return 'error'
@app.route('/test',methods=['POST','GET'])
def test():
token=request.args.get('token')
if verify_token(token)==1:
return 'data' # 返回数据,即资源管理服务器的功能
else:
return 'error' # 返回错误信息
# 客户端登录的逻辑
"""
/client/login 相当于第三方网站
/oauth 相当于授权方,只是这里写到一起了,为了方便
"""
@app.route('/client/login',methods=['POST','GET'])
def client_login():
uri="http://localhost:5000/oauth?response_type=code&client_id=%s&redirect_uri=%s"%(client_id,redirect_uri)
return redirect(uri)
# token发放机制
@app.route('/oauth',methods=['POST','GET'])
def oauth():
# 授权码的发放
if request.args.get('code'):
# 如果授权码的对应的uri请求中的uri,则为其用户生成令牌
if auth_code.get(int(request.args.get('code')))==request.args.get('redirect_uti'):
return gen_token(request.args.get('client_id'))
if request.args.get('user'):
if users.get(request.args.get('user'))[0]==request.args.get('pw') and oauth_redirect_uri:
uri=oauth_redirect_uri[0]+'?code=%s' %gen_code(oauth_redirect_uri[0])
return redirect(uri)
if request.args.get('redirect_uri'):
oauth_redirect_uri.append(request.args.get('redirect_uri'))
return "please login"
@app.route('/client/passport',methods=['POST','GET'])
def client_passport():
code=request.args.get('code')
uri='http://localhost:5000/oauth?grant_type=authorization_code&code=%s&redirect_uri=%s&client_id=%s'%(code,client_id)
return redirect(uri)
# 生成授权码方法
def gen_code(uri):
code=random.randint(0,10000)
auth_code[code]=uri # 授权码对应的uri
return code
if __name__=="__main__":
app.run(debug=True)
- 客户端代码
#! /usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on 2019年2月20日
@author: Lenovo
'''
import requests
r=requests.get("http://localhost:5000/client/login")
print(r.text)
print('======================')
print(r.history)
print('======================')
print(r.url)
print('======================')
uri_login=r.url.split('?')[0]='?user=yangxian&pw=123456'
r2=requests.get(uri_login)
print(r2.text)
print(r2.history)
print('======================')
r=requests.get("http://127.0.0.1:5000/test",params={'token':r2.text})
print(r.text)
4. 总结
本课程我们学习了OAuth 2.0的一种实现方式,我们应当掌握以下知识:
- 清楚OAuth 2.0 协议的内容
- 掌握Flask重定向的操作
- 掌握OAuth 2.0 协议中的授权码模式
如果想继续提高,请参考《OAuth介绍及实现(上)》的内容。
作者信息
本部分内容参考自极客学院网络视频课程!