什么是OAuth和OAuth流程

最近在开发博客网站登录过程中,涉及到了多个前端对应一个后端的前后端分离项目如何使用OAuth完成第三方授权登录的问题,特此总结一篇文章,详细记录了完整的开发过程思路分析和具体的代码实现,大家需要相同的业务场景时可参考使用,

什么是OAuth和OAuth流程

一、OAuth概述

1. OAuth功能

举个例子,当你想要上班摸鱼逛知乎时,但是你从来没有注册过知乎账号,而且你又嫌麻烦不愿意注册知乎账号,此时就可以使用第三方社交账号登录,例如使用qq账号授权登录后,会自动登录知乎账号,并将自己的用户名、性别、头像等基本信息就会保存在知乎平台做账号绑定。

2. 什么是OAuth

首先说明一点,OAuth 不是一个API或者公共服务,而是一个验证授权的开放标准,只要授权方和被授权方遵守这个协议去写代码提供服务,那双方就是实现了OAuth模式。目前可以提供OAuth的平台有很多,他们都遵从这个标准,实现了自己的OAuth功能。虽然OAuth指定了明确的标准,但是各家的使用方式还是略有差异。

OAuth主要有OAuth 1.0、OAuth 1.0a、OAuth 2.0三个版本。OAuth 1.0a主要是修复了 OAuth 1.0的安全 问题,OAuth 2 是为了解决 OAuth 1.0a 过于复杂的问题。OAuth2.0 是目前广泛使用的版本,目前第三方平台也都是基于OAuth2标准开放服务。

3. OAuth流程

上述例子中的知乎就是客户端,QQ就是认证服务器,OAuth2.0就是客户端和认证服务器之间为了解决相互不信任而产生的一个授权协议。(要是相互信任那豆瓣直接读取QQ的数据库登录不就好了,搞这么费劲作甚)

整个流程分为以下三个阶段

① 用户点击QQ登录进入授权页面同意授权,登录完成后获取到code;

② 知乎网站请求QQ服务器,通过code换取授权access_token;

③ QQ通过网页授权access_token向知乎返回用户的基本信息。

二、项目开发思路

1. 整体流程思路

上面举例仅是简单的业务场景,前后端不分离项目开发思路。但是遇到多个前端对应一个后端的前后端分离项目,开发的流程思路还是略有差异。

完整的设计思路如下:

首先在各个第三方登录平台创建应用,PC端和手机端各一个,是两个不同的key和secret。当用户点击第三方按钮登录时,前端传客户端类型和第三方平台给后端API接口,获取到应用的client id(也就是key)前端依据各个平台的请求地址格式,拼接成完整的url地址,其中包含前端的回调地址,并调转到第三方登录页用户登录完成后,会跳转到请求地址中指定的redirect_uri前端回调地址前端回调页获取到code参数后请求传参给后端。后端通过code参数请求第三方平台,换取token,然后使用token获取用户详细信息。后期根据openid判断用户是否已注册过(已注册——>直接登录;未注册——>获取用户信息并创建用户然后登录)返回给前端用户id和token。前端存储用户信息,并跳转到首页。至此,整个流程完成

2. 前端模块设计

前端的工作主要有两部分,分别是登录页和回调页

登录页放置第三方登录按钮,当用户点击登录后,后后端API接口传入登录平台、应用类型(桌面端还是移动端)两个参数,获取到client id,然后根据不同的第三方登录平台要求拼接URL地址,跳转到第三方登录页

回调页功能是当用户完成登录授权后,会跳转到回调页,从URL中获取到code参数,传递给后端。等待后端完成登录处理后,获取到用户id和token,并保存到local storage或者session storage中

3. 后端模块设计

后端的工作主要有两部分,分别是查询应用client id和完成用户登录

查询应用client id为一个接口,用于登录页请求。根据前端传入的登录平台、应用类型两个参数,返回应用的client id

用户登录为另一个API接口,用户回调页请求。用户传入code后,请求第三方平台OAuth接口,获取用户openid。然后判断当前用户是否已注册过账号(已注册——>直接登录;未注册——>获取用户信息并创建用户然后登录)并返回给前端用户id和token

三、应用创建与注意事项

1. 新浪微博

相关地址

微博申请地址:https://open.weibo.com/

微博登录文档:https://open.weibo.com/wiki/Connect/login

注意事项

新浪微博需要实名认证,只认证身份信息,不验证回调地址等信息是否正确。等审核通过再修改回调地址。在审核期间修改回调地址不生效。

2. 支付宝

相关地址

支付宝申请地址https://open.alipay.com/

支付宝官方文档:https://opendocs.alipay.com/support/01rg6a

注意事项

支付宝同样也需要实名认证,但也是只验证身份信息。但调用支付宝OAuth时需要使用支付宝sdk完成。

调用sdk建立连接是需要传入KEY、PRIVATE_KEY、PUBLIC_KEY三个参数,其中PUBLIC_KEY从应用信息——>接口加签方式中查看,PRIVATE_KEY是创建应用时上传的证书key

3. 视图(views.py)

import jsonimport urllib.parsefrom loguru import loggerfrom rest_framework import status, viewsetsfrom rest_framework.response import Responsefrom rest_framework.views import APIViewfrom public.tools import OAuthfrom django.conf import settingsclass OAuthIDAPIView(APIView): “”” 获取第三方登录应用ID “”” @staticmethod def get(request): platform = request.query_params.get(‘platform’) kind = request.query_params.get(‘kind’) result = {‘clientId’: settings.AUTH[platform.upper()][kind.upper()][‘KEY’]} return Response(result, status=status.HTTP_200_OK)class OAuthCallbackAPIView(APIView): “”” 授权第三方登录后回调地址 “”” @staticmethod def post(request): platform = request.data.get(‘platform’) kind = request.data.get(‘kind’) code = request.data.get(‘code’) redirect_uri = request.data.get(‘redirect_uri’) print(platform, code, redirect_uri, kind) auth = OAuth(platform, kind, code, redirect_uri) result = {} if platform == ‘WEIBO’: result = auth.weiboLogin() elif platform == ‘QQ’: result = auth.qqLogin() elif platform == ‘PAY’: result = auth.payLogin() elif platform == ‘GITHUB’: result = auth.githubLogin() elif platform == ‘BAIDU’: result = auth.baiduLogin() elif platform == ‘MICROSOFT’: result = auth.microsoftLogin() return Response(result, status=status.HTTP_200_OK)

4. OAuth类(tools.py)

import randomimport datetimeimport uuidfrom urllib.parse import urlencodefrom loguru import loggerimport requestsfrom alipay.aop.api.request.AlipaySystemOauthTokenRequest import AlipaySystemOauthTokenRequestfrom alipay.aop.api.request.AlipayUserInfoShareRequest import AlipayUserInfoShareRequestfrom alipay.aop.api.constant.ParamConstants import *from alipay.aop.api.response.AlipaySystemOauthTokenResponse import AlipaySystemOauthTokenResponsefrom alipay.aop.api.response.AlipayUserInfoShareResponse import AlipayUserInfoShareResponsefrom django.core.cache import cachefrom django.core.mail import EmailMultiAlternativesfrom django.conf import settingsfrom django.utils import timezoneimport jsonfrom account.models import UserInfo, UserSource, OAuthIdfrom rest_framework_simplejwt.tokens import RefreshTokenfrom alipay.aop.api.AlipayClientConfig import AlipayClientConfigfrom alipay.aop.api.DefaultAlipayClient import DefaultAlipayClientimport tracebackclass OAuth: “”” 第三方登录 “”” def __init__(self, platform, kind, code, redirect_uri): print(platform, kind, code, redirect_uri) self._client_key = settings.AUTH[platform][kind][‘KEY’] # 应用id self._client_secret = settings.AUTH[platform][kind][‘SECRET’] # 应用key self._code = code # 用户code self._redirect_uri = redirect_uri # 登录回调地址 self.openid = ” # 用户第三方登录ID self.source_id = ” # 用户来源id self.user_id = ” # 用户id self.platform = platform # 第三方登录平台 self.kind = kind # 前端类型(PC或M) def __checkUserRegister(self): “”” 检查用户是否已注册 :return: “”” print(‘开始检测用户是否已注册过’) user = OAuthId.objects.filter(source_id=self.source_id, openid=self.openid) if user.count() != 0: self.user_id = user.first().user.id return True else: return False def __createUser(self, username, **kwargs): “”” 创建新用户 :param username: 用户名 :param kwargs: 用户信息 :return: None “”” print(“开始创建新用户啦”) while UserInfo.objects.filter(username=username): # 防止用户名重复 username = username str(random.randrange(10)) userinfo = { ‘source_id’: self.source_id, ‘username’: username, ‘password’: str(uuid.uuid1()) } for key, value in kwargs.items(): userinfo[key] = value logger.info(‘存储信息:{}’.format(userinfo)) print(userinfo) # 用户信息表插入数据 new_user = UserInfo.objects.create_user(**userinfo) self.user_id = new_user.id # OAuthId表插入数据 OAuthId.objects.create(user_id=self.user_id, source_id=self.source_id, openid=self.openid) def __userLogin(self): “”” 用户登录签发token :return: “”” print(“开始登录了”) user = UserInfo.objects.get(id=self.user_id) user.last_login = timezone.now() user.save() refresh = RefreshToken.for_user(user) result = dict() result[‘token’] = str(refresh.access_token) result[‘userid’] = user.id result[‘username’] = user.username return result def weiboLogin(self): “”” 微博登录 :return: “”” print(“微博登录了”) self.source_id = UserSource.objects.get(name=’微博’).id # 获取用户access_token和uid access_token_url = ‘https://api.weibo.com/oauth2/access_token?client_id={0}&client_secret={1}&grant_type=authorization_code&code={2}&redirect_uri={3}’.format( self._client_key, self._client_secret, self._code, self._redirect_uri) access_response = requests.post(access_token_url).json() print(access_response[‘access_token’], access_response[‘uid’]) self.openid = access_response[‘uid’] # 判断用户是否已注册过 user = self.__checkUserRegister() if user: print(‘已注册过,直接登录’) return self.__userLogin() else: # 获取用户信息 userinfo_url = “https://api.weibo.com/2/users/show.json?access_token={0}&uid={1}”.format( access_response[‘access_token’], access_response[‘uid’]) userinfo_response = requests.get(userinfo_url).json() print(userinfo_response) logger.info(‘微博用户信息:{}’.format(userinfo_response)) username = userinfo_response[‘name’] signature = userinfo_response[‘description’] photo = userinfo_response[‘avatar_large’] web = userinfo_response[‘url’] area_name = userinfo_response[‘location’] if userinfo_response[‘gender’] == ‘f’: sex = 2 else: sex = 1 # 新建用户 self.__createUser(username, signature=signature, photo=photo, web=web, area_name=area_name, sex=sex) # 用户登录 return self.__userLogin() def qqLogin(self): “”” QQ登录 “”” print(“QQ登录了”) self.source_id = UserSource.objects.get(name=’qq’).id # 获取用户access_token access_token_url = ‘https://graph.qq.com/oauth2.0/token?client_id={0}&client_secret={1}&grant_type=authorization_code&code={2}&redirect_uri={3}&fmt=json’.format( self._client_key, self._client_secret, self._code, self._redirect_uri) access_response = requests.get(access_token_url).json() print(access_response[‘access_token’]) # 使用Access Token获取用户的OpenID openID_url = ‘https://graph.qq.com/oauth2.0/me?access_token={}&fmt=json’.format(access_response[‘access_token’]) openID_response = requests.get(openID_url).json() print(“openID:”, openID_response[‘openid’]) self.openid = openID_response[‘openid’] # 判断用户是否已注册过 user = self.__checkUserRegister() if user: print(‘已注册过,直接登录’) return self.__userLogin() else: # 获取用户信息 print(‘开始获取用户信息’) userinfo_url = “https://graph.qq.com/user/get_user_info?access_token={0}&oauth_consumer_key={1}&openid={2}&fmt=json”.format( access_response[‘access_token’], self._client_key, self.openid) userinfo_response = requests.get(userinfo_url).json() logger.info(‘QQ用户信息:{}’.format(userinfo_response)) print(userinfo_response) username = userinfo_response[‘nickname’] photo = userinfo_response[‘figureurl_2’] area_name = userinfo_response[‘province’] ‘ ‘ userinfo_response[‘city’] if userinfo_response[‘gender’] == ‘女’: sex = 2 else: sex = 1 # 新建用户 self.__createUser(username, photo=photo, area_name=area_name, sex=sex) # 用户登录 return self.__userLogin() def baiduLogin(self): “”” 百度账号登录 “”” print(“百度登录了”) self.source_id = UserSource.objects.get(name=’百度’).id print(self.source_id) # 获取用户access_token access_token_url = ‘https://openapi.baidu.com/oauth/2.0/token?grant_type=authorization_code&client_id={0}&client_secret={1}&code={2}&redirect_uri={3}’.format( self._client_key, self._client_secret, self._code, self._redirect_uri) access_response = requests.get(access_token_url).json() print(access_response[‘access_token’]) # 使用Access Token获取用户信息 userinfo_url = “https://openapi.baidu.com/rest/2.0/passport/users/getInfo?access_token={}”.format( access_response[‘access_token’]) userinfo_response = requests.get(userinfo_url).json() logger.info(‘百度用户信息:{}’.format(userinfo_response)) print(userinfo_response) self.openid = userinfo_response[‘portrait’] # 判断用户是否已注册过 user = self.__checkUserRegister() if user: print(‘已注册过,直接登录’) return self.__userLogin() else: # 获取用户信息 username = userinfo_response[‘username’] photo = ‘https://himg.bdimg.com/sys/portrait/item/’ userinfo_response[‘portrait’] # 新建用户 self.__createUser(username, photo=photo) # 用户登录 return self.__userLogin() def microsoftLogin(self): “”” 微软账号登录 “”” print(“微软账号登录了”) self.source_id = UserSource.objects.get(name=’微软’).id print(self.source_id) # 获取用户access_token access_token_headers = { ‘Content-Type’: ‘application/x-www-form-urlencoded’ } access_token_data = { “client_id”: self._client_key, “client_secret”: self._client_secret, “code”: self._code, “redirect_uri”: self._redirect_uri, “grant_type”: ‘authorization_code’, “scope”: ‘offline_access user.read’ } access_token_url = ‘https://login.microsoftonline.com/consumers/oauth2/v2.0/token’ access_response = requests.post(access_token_url, headers=access_token_headers, data=urlencode(access_token_data)).json() print(access_response[‘access_token’]) # 使用Access Token获取用户信息 userinfo_headers = { “Authorization”: ‘Bearer ‘ access_response[‘access_token’], “Host”: ‘graph.microsoft.com’ } userinfo_url = “https://graph.microsoft.com/v1.0/me” userinfo_response = requests.get(userinfo_url, headers=userinfo_headers).json() logger.info(‘微软用户信息:{}’.format(userinfo_response)) print(userinfo_response) self.openid = userinfo_response[‘id’] # 判断用户是否已注册过 user = self.__checkUserRegister() if user: print(‘已注册过,直接登录’) return self.__userLogin() else: # 获取用户信息 username = userinfo_response[‘displayName’].replace(” “, “”) # 新建用户 self.__createUser(username) # 用户登录 return self.__userLogin() def __payToken(self, client): “”” 支付宝通过code获取用户token :param client: :return: “”” # 构造请求参数对象 request = AlipaySystemOauthTokenRequest() request.code = self._code request.grant_type = “authorization_code” response_content = None # 执行API调用 try: response_content = client.execute(request) except Exception as e: print(traceback.format_exc(), e) if not response_content: print(“failed execute”) else: # 解析响应结果 response = AlipaySystemOauthTokenResponse() response.parse_response_content(response_content) if response.is_success(): # 如果业务成功,可以通过response属性获取需要的值 auth_token = response.access_token self.openid = response.user_id return auth_token # 响应失败的业务处理 else: # 如果业务失败,可以从错误码中可以得知错误情况,具体错误码信息可以查看接口文档 print(response.code “,” response.msg “,” response.sub_code “,” response.sub_msg) def __payUserInfo(self, client, token): “”” 获取支付宝用户信息 :return: “”” request = AlipayUserInfoShareRequest() # 添加auth_token udf_params = dict() udf_params[P_AUTH_TOKEN] = token request.udf_params = udf_params response_content = None # 执行API调用 try: # 执行接口请求 response_content = client.execute(request) except Exception as e: print(traceback.format_exc(), e) if not response_content: print(“failed execute”) else: response = AlipayUserInfoShareResponse() # 解析响应结果 response.parse_response_content(response_content) # 响应成功的业务处理 if response.is_success(): # 如果业务成功,可以通过response属性获取需要的值 # print(response) logger.info(‘支付宝用户信息:{}’.format(response)) username = response.nick_name photo = response.avatar area_name = response.province ‘ ‘ response.city if response.gender == ‘f’: sex = 2 else: sex = 1 # 新建用户 self.__createUser(username, photo=photo, area_name=area_name, sex=sex) # 响应失败的业务处理 else: # 如果业务失败,可以从错误码中可以得知错误情况,具体错误码信息可以查看接口文档 print(response.code “,” response.msg “,” response.sub_code “,” response.sub_msg) def payLogin(self): “”” 支付宝登录 “”” print(“支付宝登录了”) self.source_id = UserSource.objects.get(name=’支付宝’).id # 实例化客户端 alipay_client_config = AlipayClientConfig() alipay_client_config.server_url = ‘https://openapi.alipay.com/gateway.do’ alipay_client_config.app_id = self._client_key alipay_client_config.app_private_key = settings.AUTH[self.platform][self.kind][‘PRIVATE_KEY’] alipay_client_config.alipay_public_key = settings.AUTH[self.platform][self.kind][‘PUBLIC_KEY’] client = DefaultAlipayClient(alipay_client_config) # 获取用户token token = self.__payToken(client) # 判断用户是否已注册过 user = self.__checkUserRegister() if user: print(‘已注册过,直接登录’) return self.__userLogin() else: # 获取用户信息 print(‘开始获取用户信息’) self.__payUserInfo(client, token) return self.__userLogin() def __githubToken(self): “”” github获取用户token :return: “”” response = None headers = { ‘accept’: ‘application/json’ } url = ‘https://github.com/login/oauth/access_token?client_id={0}&client_secret={1}&code={2}’.format( self._client_key, self._client_secret, self._code) i = 0 while i < 3: try: print(“开始尝试获取token”, timezone.localtime()) response = requests.post(url, headers=headers, timeout=5).json() if response: return response except requests.exceptions.RequestException: i = 1 if response is None: print(“获取token请求失败了”) return False def __githubUserInfo(self, token): “”” github获取用户信息 :return: “”” response = None headers = { ‘accept’: ‘application/json’, ‘Authorization’: ‘token ‘ token } url = ‘https://api.github.com/user’ i = 0 while i < 3: try: print(“开始尝试获取用户信息”, timezone.localtime()) response = requests.get(url, headers=headers, timeout=5).json() if response: print(response) logger.info(‘github用户信息:{}’.format(response)) return response except requests.exceptions.RequestException: i = 1 if response is None: print(“获取用户信息失败了”) return False def githubLogin(self): “”” github登录 :return: “”” print(“github登录了”) self.source_id = UserSource.objects.get(name=’github’).id # 获取用户access_token access_response = self.__githubToken() if access_response: print(access_response[‘access_token’]) # 获取用户信息 userinfo_response = self.__githubUserInfo(access_response[‘access_token’]) if userinfo_response: print(userinfo_response) self.openid = userinfo_response[‘id’] # 判断用户是否已注册过 user = self.__checkUserRegister() if user: print(‘已注册过,直接登录’) return self.__userLogin() else: if userinfo_response[‘name’]: username = userinfo_response[‘name’] else: username = userinfo_response[‘login’] signature = userinfo_response[‘bio’] photo = userinfo_response[‘avatar_url’] if userinfo_response[‘blog’]: web = userinfo_response[‘blog’] else: web = userinfo_response[‘html_url’] area_name = userinfo_response[‘location’] # 新建用户 self.__createUser(username, signature=signature, photo=photo, web=web, area_name=area_name) # 用户登录 return self.__userLogin() else: return False

五、前端代码

1. API地址封装

// 获取第三方登录IDexport function getOAuthID(platform) {return index.get(‘/account/OAuthID/’ ‘?platform=’ platform ‘&kind=PC’)}// 第三方授权登录后回调export function postOAuthCallback(params) {return index.post(‘/account/OAuthCallback/’, params)}

2. PC端用户登录页

第三方账号登录

3. PC端授权回调页

4. 手机端用户登录页

业务逻辑与手机端一致,只是授权页跳转时URL参数手机端和PC端略有差异

第三方账号登录

QQ

支付宝

百度

微博

GitHub

微软

@import “src/assets/style/index”;

五、效果演示

1. 手机端登录

访问地址:https://m.cuiliangblog.cn/loginRegister登录页

2. 电脑端登录

访问地址:https://www.cuiliangblog.cn/loginRegister用户登录页授权回调页首页

3. admin后台

本站部分内容由互联网用户自发贡献,该文观点仅代表作者本人,本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规等内容,请举报!一经查实,本站将立刻删除。
本站部分内容由互联网用户自发贡献,该文观点仅代表作者本人,本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。

如发现本站有涉嫌抄袭侵权/违法违规等内容,请<举报!一经查实,本站将立刻删除。