|
|
|
|
@ -14,72 +14,64 @@ logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
class OAuthAccessTokenException(Exception):
|
|
|
|
|
'''
|
|
|
|
|
oauth授权失败异常 zzh: 自定义异常类,用于处理OAuth授权过程中的错误
|
|
|
|
|
oauth授权失败异常
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BaseOauthManager(metaclass=ABCMeta):
|
|
|
|
|
"""获取用户授权""" zzh: OAuth管理器的抽象基类,定义了OAuth流程的标准接口
|
|
|
|
|
AUTH_URL = None # zzh: 授权页面URL,用户在此页面进行OAuth授权
|
|
|
|
|
TOKEN_URL = None # zzh: 获取access_token的API地址
|
|
|
|
|
API_URL = None # zzh: 获取用户信息的API地址
|
|
|
|
|
ICON_NAME = None # zzh: OAuth平台标识名,用于区分不同平台
|
|
|
|
|
"""获取用户授权"""
|
|
|
|
|
AUTH_URL = None
|
|
|
|
|
"""获取token"""
|
|
|
|
|
TOKEN_URL = None
|
|
|
|
|
"""获取用户信息"""
|
|
|
|
|
API_URL = None
|
|
|
|
|
'''icon图标名'''
|
|
|
|
|
ICON_NAME = None
|
|
|
|
|
|
|
|
|
|
def __init__(self, access_token=None, openid=None):
|
|
|
|
|
# zzh: 初始化OAuth管理器,可传入已获取的access_token和openid
|
|
|
|
|
self.access_token = access_token
|
|
|
|
|
self.openid = openid
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_access_token_set(self):
|
|
|
|
|
# zzh: 检查access_token是否已设置
|
|
|
|
|
return self.access_token is not None
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_authorized(self):
|
|
|
|
|
# zzh: 检查是否已完成授权(既有access_token又有openid)
|
|
|
|
|
return self.is_access_token_set and self.access_token is not None and self.openid is not None
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def get_authorization_url(self, nexturl='/'):
|
|
|
|
|
# zzh: 抽象方法,生成授权页面URL
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def get_access_token_by_code(self, code):
|
|
|
|
|
# zzh: 抽象方法,通过授权码获取access_token
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def get_oauth_userinfo(self):
|
|
|
|
|
# zzh: 抽象方法,获取用户信息
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def get_picture(self, metadata):
|
|
|
|
|
# zzh: 抽象方法,从元数据中提取用户头像
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def do_get(self, url, params, headers=None):
|
|
|
|
|
# zzh: 执行GET请求的通用方法
|
|
|
|
|
rsp = requests.get(url=url, params=params, headers=headers)
|
|
|
|
|
logger.info(rsp.text)
|
|
|
|
|
return rsp.text
|
|
|
|
|
|
|
|
|
|
def do_post(self, url, params, headers=None):
|
|
|
|
|
# zzh: 执行POST请求的通用方法
|
|
|
|
|
rsp = requests.post(url, params, headers=headers)
|
|
|
|
|
logger.info(rsp.text)
|
|
|
|
|
return rsp.text
|
|
|
|
|
|
|
|
|
|
def get_config(self):
|
|
|
|
|
# zzh: 从数据库获取对应平台的OAuth配置
|
|
|
|
|
value = OAuthConfig.objects.filter(type=self.ICON_NAME)
|
|
|
|
|
return value[0] if value else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WBOauthManager(BaseOauthManager):
|
|
|
|
|
# zzh: 微博OAuth管理器实现
|
|
|
|
|
AUTH_URL = 'https://api.weibo.com/oauth2/authorize'
|
|
|
|
|
TOKEN_URL = 'https://api.weibo.com/oauth2/access_token'
|
|
|
|
|
API_URL = 'https://api.weibo.com/2/users/show.json'
|
|
|
|
|
@ -87,9 +79,9 @@ class WBOauthManager(BaseOauthManager):
|
|
|
|
|
|
|
|
|
|
def __init__(self, access_token=None, openid=None):
|
|
|
|
|
config = self.get_config()
|
|
|
|
|
self.client_id = config.appkey if config else '' # zzh: 微博应用AppKey
|
|
|
|
|
self.client_secret = config.appsecret if config else '' # zzh: 微博应用AppSecret
|
|
|
|
|
self.callback_url = config.callback_url if config else '' # zzh: 回调地址
|
|
|
|
|
self.client_id = config.appkey if config else ''
|
|
|
|
|
self.client_secret = config.appsecret if config else ''
|
|
|
|
|
self.callback_url = config.callback_url if config else ''
|
|
|
|
|
super(
|
|
|
|
|
WBOauthManager,
|
|
|
|
|
self).__init__(
|
|
|
|
|
@ -97,7 +89,6 @@ class WBOauthManager(BaseOauthManager):
|
|
|
|
|
openid=openid)
|
|
|
|
|
|
|
|
|
|
def get_authorization_url(self, nexturl='/'):
|
|
|
|
|
# zzh: 生成微博授权URL,包含next_url参数用于授权后跳转
|
|
|
|
|
params = {
|
|
|
|
|
'client_id': self.client_id,
|
|
|
|
|
'response_type': 'code',
|
|
|
|
|
@ -107,7 +98,7 @@ class WBOauthManager(BaseOauthManager):
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
|
def get_access_token_by_code(self, code):
|
|
|
|
|
# zzh: 使用授权码获取微博access_token
|
|
|
|
|
|
|
|
|
|
params = {
|
|
|
|
|
'client_id': self.client_id,
|
|
|
|
|
'client_secret': self.client_secret,
|
|
|
|
|
@ -120,7 +111,7 @@ class WBOauthManager(BaseOauthManager):
|
|
|
|
|
obj = json.loads(rsp)
|
|
|
|
|
if 'access_token' in obj:
|
|
|
|
|
self.access_token = str(obj['access_token'])
|
|
|
|
|
self.openid = str(obj['uid']) # zzh: 微博返回的是uid字段
|
|
|
|
|
self.openid = str(obj['uid'])
|
|
|
|
|
return self.get_oauth_userinfo()
|
|
|
|
|
else:
|
|
|
|
|
raise OAuthAccessTokenException(rsp)
|
|
|
|
|
@ -136,9 +127,9 @@ class WBOauthManager(BaseOauthManager):
|
|
|
|
|
try:
|
|
|
|
|
datas = json.loads(rsp)
|
|
|
|
|
user = OAuthUser()
|
|
|
|
|
user.metadata = rsp # zzh: 保存原始API响应数据
|
|
|
|
|
user.picture = datas['avatar_large'] # zzh: 微博大头像
|
|
|
|
|
user.nickname = datas['screen_name'] # zzh: 微博昵称
|
|
|
|
|
user.metadata = rsp
|
|
|
|
|
user.picture = datas['avatar_large']
|
|
|
|
|
user.nickname = datas['screen_name']
|
|
|
|
|
user.openid = datas['id']
|
|
|
|
|
user.type = 'weibo'
|
|
|
|
|
user.token = self.access_token
|
|
|
|
|
@ -151,13 +142,11 @@ class WBOauthManager(BaseOauthManager):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def get_picture(self, metadata):
|
|
|
|
|
# zzh: 从元数据中提取微博用户头像
|
|
|
|
|
datas = json.loads(metadata)
|
|
|
|
|
return datas['avatar_large']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProxyManagerMixin:
|
|
|
|
|
# zzh: 代理混入类,为需要代理访问的OAuth平台提供支持
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
if os.environ.get("HTTP_PROXY"):
|
|
|
|
|
self.proxies = {
|
|
|
|
|
@ -168,20 +157,17 @@ class ProxyManagerMixin:
|
|
|
|
|
self.proxies = None
|
|
|
|
|
|
|
|
|
|
def do_get(self, url, params, headers=None):
|
|
|
|
|
# zzh: 重写GET方法,支持代理
|
|
|
|
|
rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
|
|
|
|
|
logger.info(rsp.text)
|
|
|
|
|
return rsp.text
|
|
|
|
|
|
|
|
|
|
def do_post(self, url, params, headers=None):
|
|
|
|
|
# zzh: 重写POST方法,支持代理
|
|
|
|
|
rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
|
|
|
|
|
logger.info(rsp.text)
|
|
|
|
|
return rsp.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
# zzh: Google OAuth管理器实现,继承代理混入类
|
|
|
|
|
AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
|
|
|
|
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
|
|
|
|
|
API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
|
|
|
|
|
@ -199,7 +185,6 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
openid=openid)
|
|
|
|
|
|
|
|
|
|
def get_authorization_url(self, nexturl='/'):
|
|
|
|
|
# zzh: 生成Google授权URL,scope包含openid和email
|
|
|
|
|
params = {
|
|
|
|
|
'client_id': self.client_id,
|
|
|
|
|
'response_type': 'code',
|
|
|
|
|
@ -224,7 +209,7 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
|
|
|
|
|
if 'access_token' in obj:
|
|
|
|
|
self.access_token = str(obj['access_token'])
|
|
|
|
|
self.openid = str(obj['id_token']) # zzh: Google使用id_token作为openid
|
|
|
|
|
self.openid = str(obj['id_token'])
|
|
|
|
|
logger.info(self.ICON_NAME + ' oauth ' + rsp)
|
|
|
|
|
return self.access_token
|
|
|
|
|
else:
|
|
|
|
|
@ -242,9 +227,9 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
datas = json.loads(rsp)
|
|
|
|
|
user = OAuthUser()
|
|
|
|
|
user.metadata = rsp
|
|
|
|
|
user.picture = datas['picture'] # zzh: Google用户头像
|
|
|
|
|
user.picture = datas['picture']
|
|
|
|
|
user.nickname = datas['name']
|
|
|
|
|
user.openid = datas['sub'] # zzh: Google用户唯一标识
|
|
|
|
|
user.openid = datas['sub']
|
|
|
|
|
user.token = self.access_token
|
|
|
|
|
user.type = 'google'
|
|
|
|
|
if datas['email']:
|
|
|
|
|
@ -261,7 +246,6 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
# zzh: GitHub OAuth管理器实现
|
|
|
|
|
AUTH_URL = 'https://github.com/login/oauth/authorize'
|
|
|
|
|
TOKEN_URL = 'https://github.com/login/oauth/access_token'
|
|
|
|
|
API_URL = 'https://api.github.com/user'
|
|
|
|
|
@ -283,7 +267,7 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
'client_id': self.client_id,
|
|
|
|
|
'response_type': 'code',
|
|
|
|
|
'redirect_uri': f'{self.callback_url}&next_url={next_url}',
|
|
|
|
|
'scope': 'user' # zzh: GitHub授权范围,获取用户基本信息
|
|
|
|
|
'scope': 'user'
|
|
|
|
|
}
|
|
|
|
|
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
|
|
|
|
|
return url
|
|
|
|
|
@ -300,7 +284,7 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
rsp = self.do_post(self.TOKEN_URL, params)
|
|
|
|
|
|
|
|
|
|
from urllib import parse
|
|
|
|
|
r = parse.parse_qs(rsp) # zzh: GitHub返回的是查询字符串格式,需要解析
|
|
|
|
|
r = parse.parse_qs(rsp)
|
|
|
|
|
if 'access_token' in r:
|
|
|
|
|
self.access_token = (r['access_token'][0])
|
|
|
|
|
return self.access_token
|
|
|
|
|
@ -308,14 +292,14 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
raise OAuthAccessTokenException(rsp)
|
|
|
|
|
|
|
|
|
|
def get_oauth_userinfo(self):
|
|
|
|
|
# zzh: GitHub获取用户信息需要在header中传递token
|
|
|
|
|
|
|
|
|
|
rsp = self.do_get(self.API_URL, params={}, headers={
|
|
|
|
|
"Authorization": "token " + self.access_token
|
|
|
|
|
})
|
|
|
|
|
try:
|
|
|
|
|
datas = json.loads(rsp)
|
|
|
|
|
user = OAuthUser()
|
|
|
|
|
user.picture = datas['avatar_url'] # zzh: GitHub头像URL
|
|
|
|
|
user.picture = datas['avatar_url']
|
|
|
|
|
user.nickname = datas['name']
|
|
|
|
|
user.openid = datas['id']
|
|
|
|
|
user.type = 'github'
|
|
|
|
|
@ -335,7 +319,6 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
# zzh: Facebook OAuth管理器实现
|
|
|
|
|
AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth'
|
|
|
|
|
TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token'
|
|
|
|
|
API_URL = 'https://graph.facebook.com/me'
|
|
|
|
|
@ -353,7 +336,6 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
openid=openid)
|
|
|
|
|
|
|
|
|
|
def get_authorization_url(self, next_url='/'):
|
|
|
|
|
# zzh: Facebook授权范围包含email和public_profile
|
|
|
|
|
params = {
|
|
|
|
|
'client_id': self.client_id,
|
|
|
|
|
'response_type': 'code',
|
|
|
|
|
@ -367,7 +349,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
params = {
|
|
|
|
|
'client_id': self.client_id,
|
|
|
|
|
'client_secret': self.client_secret,
|
|
|
|
|
# 'grant_type': 'authorization_code', # zzh: Facebook不需要显式指定grant_type
|
|
|
|
|
# 'grant_type': 'authorization_code',
|
|
|
|
|
'code': code,
|
|
|
|
|
|
|
|
|
|
'redirect_uri': self.callback_url
|
|
|
|
|
@ -383,7 +365,6 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
raise OAuthAccessTokenException(rsp)
|
|
|
|
|
|
|
|
|
|
def get_oauth_userinfo(self):
|
|
|
|
|
# zzh: Facebook需要指定fields参数来获取特定字段
|
|
|
|
|
params = {
|
|
|
|
|
'access_token': self.access_token,
|
|
|
|
|
'fields': 'id,name,picture,email'
|
|
|
|
|
@ -400,7 +381,6 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
if 'email' in datas and datas['email']:
|
|
|
|
|
user.email = datas['email']
|
|
|
|
|
if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']:
|
|
|
|
|
# zzh: Facebook头像URL嵌套在多层结构中
|
|
|
|
|
user.picture = str(datas['picture']['data']['url'])
|
|
|
|
|
return user
|
|
|
|
|
except Exception as e:
|
|
|
|
|
@ -413,11 +393,10 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QQOauthManager(BaseOauthManager):
|
|
|
|
|
# zzh: QQ OAuth管理器实现
|
|
|
|
|
AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize'
|
|
|
|
|
TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'
|
|
|
|
|
API_URL = 'https://graph.qq.com/user/get_user_info'
|
|
|
|
|
OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' # zzh: QQ需要单独获取openid
|
|
|
|
|
OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me'
|
|
|
|
|
ICON_NAME = 'qq'
|
|
|
|
|
|
|
|
|
|
def __init__(self, access_token=None, openid=None):
|
|
|
|
|
@ -450,7 +429,7 @@ class QQOauthManager(BaseOauthManager):
|
|
|
|
|
}
|
|
|
|
|
rsp = self.do_get(self.TOKEN_URL, params)
|
|
|
|
|
if rsp:
|
|
|
|
|
d = urllib.parse.parse_qs(rsp) # zzh: QQ返回查询字符串格式
|
|
|
|
|
d = urllib.parse.parse_qs(rsp)
|
|
|
|
|
if 'access_token' in d:
|
|
|
|
|
token = d['access_token']
|
|
|
|
|
self.access_token = token[0]
|
|
|
|
|
@ -459,14 +438,12 @@ class QQOauthManager(BaseOauthManager):
|
|
|
|
|
raise OAuthAccessTokenException(rsp)
|
|
|
|
|
|
|
|
|
|
def get_open_id(self):
|
|
|
|
|
# zzh: QQ需要单独调用接口获取openid
|
|
|
|
|
if self.is_access_token_set:
|
|
|
|
|
params = {
|
|
|
|
|
'access_token': self.access_token
|
|
|
|
|
}
|
|
|
|
|
rsp = self.do_get(self.OPEN_ID_URL, params)
|
|
|
|
|
if rsp:
|
|
|
|
|
# zzh: QQ返回的是JSONP格式,需要清理
|
|
|
|
|
rsp = rsp.replace(
|
|
|
|
|
'callback(', '').replace(
|
|
|
|
|
')', '').replace(
|
|
|
|
|
@ -481,7 +458,7 @@ class QQOauthManager(BaseOauthManager):
|
|
|
|
|
if openid:
|
|
|
|
|
params = {
|
|
|
|
|
'access_token': self.access_token,
|
|
|
|
|
'oauth_consumer_key': self.client_id, # zzh: QQ需要传递appkey
|
|
|
|
|
'oauth_consumer_key': self.client_id,
|
|
|
|
|
'openid': self.openid
|
|
|
|
|
}
|
|
|
|
|
rsp = self.do_get(self.API_URL, params)
|
|
|
|
|
@ -496,7 +473,7 @@ class QQOauthManager(BaseOauthManager):
|
|
|
|
|
if 'email' in obj:
|
|
|
|
|
user.email = obj['email']
|
|
|
|
|
if 'figureurl' in obj:
|
|
|
|
|
user.picture = str(obj['figureurl']) # zzh: QQ标准头像
|
|
|
|
|
user.picture = str(obj['figureurl'])
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
def get_picture(self, metadata):
|
|
|
|
|
@ -506,7 +483,6 @@ class QQOauthManager(BaseOauthManager):
|
|
|
|
|
|
|
|
|
|
@cache_decorator(expiration=100 * 60)
|
|
|
|
|
def get_oauth_apps():
|
|
|
|
|
# zzh: 获取所有启用的OAuth应用,使用缓存提高性能
|
|
|
|
|
configs = OAuthConfig.objects.filter(is_enable=True).all()
|
|
|
|
|
if not configs:
|
|
|
|
|
return []
|
|
|
|
|
@ -517,7 +493,6 @@ def get_oauth_apps():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_manager_by_type(type):
|
|
|
|
|
# zzh: 根据类型获取对应的OAuth管理器实例
|
|
|
|
|
applications = get_oauth_apps()
|
|
|
|
|
if applications:
|
|
|
|
|
finds = list(
|
|
|
|
|
@ -526,4 +501,4 @@ def get_manager_by_type(type):
|
|
|
|
|
applications))
|
|
|
|
|
if finds:
|
|
|
|
|
return finds[0]
|
|
|
|
|
return None
|
|
|
|
|
return None
|
|
|
|
|
|