- QQ登录:即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。
- 一般QQ登录成功就直接进入系统,本项目还需要绑定用户
- 如果用户已注册,直接绑定用户
- 如果没有注册,还需要分配用户信息
一、QQ登录开发文档
1、QQ互联开发者申请步骤
若想实现QQ登录,需要成为QQ互联的开发者,审核通过才可实现。
相关连接:http://wiki.connect.qq.com/%E6%88%90%E4%B8%BA%E5%BC%80%E5%8F%91%E8%80%85
2、QQ互联应用申请步骤
成为QQ互联开发者后,还需创建应用,即获取本项目对应与QQ互联的应用ID。
相关连接:http://wiki.connect.qq.com/__trashed-2
3、网站对接QQ登录步骤
QQ互联提供有开发文档,帮助开发者实现QQ登录。
相关连接:http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0
- QQ登录开发流程:(以下内容截取了2021.2.12QQ说明文档)
3.1 准备工作_OAuth2.0
- 本步骤的作用:
接入QQ登录前,网站需首先进行申请,获得对应的appid与appkey,以保证后续流程中可正确对网站与用户进行验证与授权。 - 本步骤在整个流程中的位置:
3.1.1 申请appid和appkey
-
申请appid和appkey的用途
- appid:应用的唯一标识。在OAuth2.0认证过程中,appid的值即为oauth_consumer_key的值。
- appkey:appid对应的密钥,访问用户资源时用来验证应用的合法性。在OAuth2.0认证过程中,appkey的值即为oauth_consumer_secret的值。
-
申请地址
https://connect.qq.com/manage.html#/ -
申请流程
- 1.开发者资质审核
- 参考文章:开发者注册流程
- 2.申请appid(oauth_consumer_key/client_id)和appkey(auth_consumer_secret/client_secret);
- (1)进入https://connect.qq.com/manage.html#/页面,点击“创建应用”,在弹出的对话框中填写网站或应用的详细资料(名称,域名,回调地址);
- (2)点击“确定”按钮,提交资料后,获取appid和appkey。
- 注意:申请appid时,登录的QQ号码将与申请到的appid绑定,后续维护均需要使用该号码。
- 注意:对appid和appkey信息进行保密,不要随意泄漏。
- 1.开发者资质审核
3.1.2 保证连接畅通
- 接入QQ登录时,网站需要不停的和Qzone进行交互,发送请求和接受响应。
- 1.对于PC网站:
- 请在你的服务器上ping graph.qq.com ,保证连接畅通。
- 2.移动应用无需此步骤
- 1.对于PC网站:
3.2 放置“QQ登录”按钮_OAuth2.0
- 本步骤的作用:
在网站页面上放置“QQ登录”按钮,并为按钮添加前台代码,实现点击按钮即弹出QQ登录对话框。 - 本步骤在整个流程中的位置:
3.2.1 下载“QQ登录”按钮图片,并将按钮放置在页面合适的位置
3.2.2 为“QQ登录”按钮添加前台代码
- 1.效果演示
- 用户在页面上点击“QQ登录”按钮,将触发QQ登录对话框,效果如下图所示:
- 用户在页面上点击“QQ登录”按钮,将触发QQ登录对话框,效果如下图所示:
- 2.前台代码
- 为了实现上述效果,应该为“QQ登录”按钮图片添加如下前台代码:
<img src=QQ登录图标文件在服务器上的地址 onclick=按钮点击事件>
- 3.代码示例
- 写一个函数“toLogin()”,该函数通过调用“index.php”中的qq_login函数来实现将页面跳转到QQ登录页面。
(示例中的oauth/index.php,请参见从SDK下载页面下载PHP SDK,在Connect2.1文件夹下的index.php文件。)
- 写一个函数“toLogin()”,该函数通过调用“index.php”中的qq_login函数来实现将页面跳转到QQ登录页面。
<script>
function toLogin()
{
//以下为按钮点击事件的逻辑。注意这里要重新打开窗口
//否则后面跳转到QQ登录,授权页面时会直接缩小当前浏览器的窗口,而不是打开新窗口
var A=window.open("oauth/index.php","TencentLogin",
"width=450,height=320,menubar=0,scrollbars=1,
resizable=1,status=1,titlebar=0,toolbar=0,location=1");
}
</script>
- 为按钮添加“toLogin()”事件:
<a href="#" onclick='toLogin()'>
<img src="img/qq_login.png"></a>
3.3 使用Authorization_Code获取Access_Token
- 本步骤的作用:
通过用户验证登录和授权,获取Access Token,为下一步获取用户的OpenID做准备;
同时,Access Token是应用在调用OpenAPI访问和修改用户数据时必须传入的参数。
移动端应用可以直接获得AccessToken,请参考使用Implicit_Grant方式获取Access_Token - 本步骤在整个流程中的位置:
- 对于应用而言,需要进行两步:
- 1.获取Authorization Code;
- 2.通过Authorization Code获取Access Token
- 过程详解
Step1:获取Authorization Code(提供扫码页面)
- 请求地址:PC网站:https://graph.qq.com/oauth2.0/authorize
- 请求方法:GET
- 请求参数:请求参数请包含如下内容:
参数 | 是否必须 | 含义 |
---|---|---|
response_type | 必须 | 授权类型,此值固定为“code”。 |
client_id | 必须 | 申请QQ登录成功后,分配给应用的appid。 |
redirect_uri | 必须 | 成功授权后的回调地址,必须是注册appid时填写的主域名下的地址,建议设置为网站首页或网站的用户中心。注意需要将url进行URLEncode。 |
state | 必须 | client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。请务必严格按照流程检查用户与state参数状态的绑定。 |
返回说明:
- 如果用户成功登录并授权,则会跳转到指定的回调地址,并在redirect_uri地址后带上Authorization Code和原始的state值。如:
PC网站:http://graph.qq.com/demo/index.jsp?code=9A5F************************06AF&state=test
注意:此code会在10分钟内过期。 - 如果用户在登录授权过程中取消登录流程,对于PC网站,登录页面直接关闭;
错误码说明:
接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。
PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码。
Step2:通过Authorization Code获取Access Token
-
请求地址:PC网站:https://graph.qq.com/oauth2.0/token
-
请求方法:GET
-
请求参数:请求参数请包含如下内容:
-
返回说明:
-
如果成功返回,即可在返回包中获取到Access Token。 如(不指定fmt时):
-
access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
-
-
错误码说明:
接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。
PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码。
Step3:(可选)权限自动续期,获取Access Token
- Access_Token的有效期默认是3个月,过期后需要用户重新授权才能获得新的Access_Token。本步骤可以实现授权自动续期,避免要求用户再次授权的操作,提升用户体验。
- 请求地址:PC网站:https://graph.qq.com/oauth2.0/token
- 请求方法:GET
- 请求参数:请求参数请包含如下内容:
- 返回说明:
如果成功返回,即可在返回包中获取到Access Token。 如(不指定fmt时):
access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14。
- 错误码说明:
接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。
PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码。
3.4 获取用户OpenID_OAuth2.0
- 本步骤的作用:
通过输入在上一步获取的Access Token,得到对应用户身份的OpenID。
OpenID是此网站上或应用中唯一对应用户身份的标识,网站或应用可将此ID进行存储,便于用户下次登录时辨识其身份,或将其与用户在网站上或应用中的原有账号进行绑定。 - 本步骤在整个流程中的位置:
- 1 请求地址:PC网站:https://graph.qq.com/oauth2.0/me
- 2 请求方法:GET
- 3 请求参数:请求参数请包含如下内容:
参数 | 是否必须 | 含义 |
---|---|---|
access_token | 必须 | 在Step1中获取到的access token。 |
- 4 返回说明
PC网站接入时,获取到用户OpenID,返回包如下(如果fmt参数未指定):
callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
openid是此网站上唯一对应用户身份的标识,网站可将此ID进行存储便于用户下次登录时辨识其身份,或将其与用户在网站上的原有账号进行绑定。
- 5 错误码说明
接口调用有错误时,会返回code和msg字段,以url参数对的形式返回,value部分会进行url编码(UTF-8)。
PC网站接入时,错误码详细信息请参见:100000-100031:PC网站接入时的公共返回码。
4、QQ登录流程分析
二、QQ登录工具QQLoginTool
- 上面的登录流程可以使用QQLoginTool实现
- 该工具封装了QQ登录时对接QQ互联接口的请求操作。可用于快速实现QQ登录。
- QQLoginTool安装:pip install QQLoginTool
- 使用说明
- 1.导入
from QQLoginTool.QQtool import OAuthQQ
- 2.初始化OAuthQQ对象
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET, redirect_uri=settings.QQ_REDIRECT_URI, state=next)
- 3.获取QQ登录扫码页面,扫码后得到Authorization Code
login_url = oauth.get_qq_url()
- 4.通过Authorization Code获取Access Token
access_token = oauth.get_access_token(code)
- 5.通过Access Token获取OpenID
openid = oauth.get_open_id(access_token)
三、定义QQ登录模型类
- QQ登录成功后,我们需要将QQ用户和商场用户关联到一起,方便下次QQ登录时使用,所以我们选择使用MySQL数据库进行存储。
- 保存字段主要有:创建时间、更新时间 、QQ的openid和用户id
- 此功能可以不创建新的app,直接在uses的modles.py里创建,也可以创建新的app,本次项目就创建新的app,方便以后写微信、支付宝等其它第三方登录
python ../manage.py startapp oauth
1、创建数据模型基类
- 创建时间、更新时间这两个字段在很多表中都需要,固需要创建成基类,其它数据模型在其基础上继承
- 此两字段不需要单独创建表,故定义为抽象模型类 abstract = True,数据库迁移时不创建新表
from django.db import models
class BaseModel(models.Model):
create_time=models.DateTimeField(auto_now_add=True,verbose_name="创建时间")
update_time=models.DateTimeField(auto_now=True,verbose_name="更新时间")
class Meta:
# 数据库迁移的时候不创建BaseModel这个表
abstract = True
2、定义QQ登录模型类
- 继承BaseModel类,用户id是外健,users.User类
from django.db import models
from utils.models import BaseModel
class OAuthQQUser(BaseModel):
"""QQ登录用户数据"""
user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name='用户')
openid = models.CharField(max_length=64, verbose_name='openid')
class Meta:
db_table = 'tb_oauth_qq'
verbose_name = 'QQ登录用户数据'
verbose_name_plural = verbose_name
3、注册应用
4、数据库迁移
python manage.py makemigrations
python manage.py migrate
四、OAuth2.0认证获取openid
- 首先前端发起QQ登录请求,需要后端返回一个QQ登录界面的链接
- 前端接收到QQ登录界面的链接后,打开并展示QQ登录界面(由QQ服务器返回QQ登录界面及相关参数)
- 用户扫描QQ登录链,完成QQ录登验证(QQ服务器处理)
- 如果验证成功,QQ服务器会将处理结果回调信息传给后端,后端根据接口参数完成系统登录处理
1、获取QQ登录扫码页面
1.1 接口设计
- 用户点击QQ登录图标,前端即会发起QQ登录的ajax请求
- 后端接收到请求,接收参数,
- 利用QQLoginTools工具得到QQ登录的url
- 返回用JSON方式,发送给前端
1.1.1 请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /qq/login/ |
1.1.2 请求参数:查询参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
next | string | 否 | 用于记录QQ登录成功后进入的网址 |
1.1.3 响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
login_url | QQ登录扫码页面链接 |
1.2 QQ登录参数
- QQ互联开发者申请后,会有以下3个参数:QQ_CLIENT_ID、QQ_CLIENT_SECRET、QQ_REDIRECT_URI
- 一般公司会有专人负责申请,直接向公司领用此3个参数即可
- 建议将此参数定义在配置文件中
1.3 后端逻辑实现
# ./apps/oauth/views.py
from django.shortcuts import render
from django.views import View
from QQLoginTool.QQtool import OAuthQQ
from django.conf import settings
from django import http
from utils.response_code import RETCODE
# Create your views here.
class QQAuthURLView(View):
def get(self,request):
# 获取参数
next=request.GET.get("next")
print(next)
# 创建工具对象
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI, state=next)
# 生成扫描链接地址
login_url = oauth.get_qq_url()
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'login_url': login_url})
1.3.2 注册路由
# 注册子路由
# ./apps/oauth/urls.py
from django.urls import path
from . import views
urlpatterns = [
# 提供QQ登录扫描页面
path('qq/login/', views.QQAuthURLView.as_view()),
]
# 注册主路由
# ./lgshop/urls.py
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
path('users/', include('users.urls')),
path('', include('contents.urls')),
path('',include("verifications.urls")),
path("",include("oauth.urls"))
]
1.4 前端
- ./templates/login.html
- ./static/login.js
2、用户用手机QQ扫描QQ登录二维码后,并确认登录后,出现下面错误界面
-
返回址址:http://www.meiduo.site:8000/oauth_callback?code=62168281346F9B7CBF5964694C5B26F2&state=%2F
-
根据错误提示,将www.meiduo.site 添加到允许访问的地址列表中,同时还要将127.0.0.1也添加到允许访问的地址列表中
-
在开发环境下,服务都跑在127.0.0.1上,所以要将本机绑定www.meiduo.site域名
- Windows系统:编辑 C:\Windows\System32\drivers\etc\hosts
- 在linux ubuntu系统或者Mac系统:编辑 /etc/hosts
- Windows系统:编辑 C:\Windows\System32\drivers\etc\hosts
3、接收Authorization Codece,获取openid
- 用户在QQ登录成功后,QQ会将用户重定向到我们配置的回调网址。
- 在QQ重定向到回调网址时,会传给我们一个Authorization Code。
- 我们需要拿到Authorization Code并完成OAuth2.0认证获取openid。
- 在本项目中,我们申请QQ登录开发资质时配置的回调网址为:
- http://www.meiduo.site:8000/oauth_callback
- QQ互联重定向的完整网址为:http://www.meiduo.site:8000/oauth_callback/?code=AE263F12675FA79185B54870D79730A7&state=%2F
- 使用access_token向QQ服务器请求openid
2.1 view实现
- 通过QQ回调url,拿到code
- 通过code ,向QQ服务器申请access_token
- 通过QQ服务器返回的access_token,再向QQ服务器申请openid
- 当获得QQ的openid后,就完成QQ登录的验证部分
- 根据项目需要,还需要将openid与用户名绑定(相关讲解详见后面的4、openid绑定用户的处理)
2.2 路由设置
4、openid是否绑定用户的处理
- 得到openid后,需要检查openid是否绑定用户,即查询表tb_oauth_qq中是否有openid
- 得到oauth_user对象,但user对象是它的外键
- 如果存在,即openid与用户进行了绑定,使用用户操作登录需要的步骤
- 保持登录状态:利用django的login()
- 需要获得跳转路径: next关建字改为了state
- 将用户设置到cookie中
- 返回前端
- 如果不存在,即openid未与用户进行了绑定过,需要显示绑定页面
- 绑定界面:用户名、手机号、密码、图片验证、短信验证、隐藏的openid
- 隐藏的openid需要后端做为参数传递到前端
- openid不能用明文传输,需要加密,提交后还需要解密还原
- 可以使用itsdangerous对openid进行加密及解密
- 绑定界面:用户名、手机号、密码、图片验证、短信验证、隐藏的openid
4.1 使用itsdangerous加密和解密
- 安装itsdangerous : pip install itsdangerous
- 将加密解密的函数,定义到独立的文件utils.py中
- 加解密需要用到数据证书,建议用项目配置文件中SECRET_KEY,也可以自定义一个
- 使用TimedJSONWebSignatureSerializer可以生成带有有效期的token
4.1.1 itsdangerous的使用
- itsdangerous模块的参考资料链接 http://itsdangerous.readthedocs.io/en/latest/
- 安装:pip install itsdangerous
- TimedJSONWebSignatureSerializer的使用
- 使用TimedJSONWebSignatureSerializer可以生成带有有效期的token
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
# serializer = Serializer(秘钥, 有效期秒)
serializer = Serializer(settings.SECRET_KEY, 300)
# serializer.dumps(数据), 返回bytes类型
token = serializer.dumps({'mobile': '185xxxxxxx78'})
token = token.decode()
# 检验token
# 验证失败,会抛出itsdangerous.BadData异常
serializer = Serializer(settings.SECRET_KEY, 300)
try:
data = serializer.loads(token)
except BadData:
return None
4.1.2 itsdangerous加密和解密(utils.py)
# .oauth.utils.py
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
from . import constants
def check_access_token(openid):
"""
反序列化
:param openid: openid密文
:return: openid: 明文
"""
serializer = Serializer(settings.SECRET_KEY, constants.ACCESS_TOKEN_EXPIRES)
# constants.ACCESS_TOKEN_EXPIRES=600
try:
data = serializer.loads(openid)
except Exception as e:
return None
else:
return data.get('openid')
def generate_access_token(openid):
"""
签名、序列化openid
:param openid: 明文
:return: openid密文
"""
serializer = Serializer(settings.SECRET_KEY, constants.ACCESS_TOKEN_EXPIRES)
data = {'openid': openid}
# 类型 是字节
token = serializer.dumps(data)
return token.decode()
5、views.py代码实现
class QQAuthUserView(View):
"""处理QQ登录回调"""
def get(self, request):
"""处理QQ登录回调的业务逻辑"""
code = request.GET.get('code')
if not code:
return http.HttpResponseForbidden('获取code失败')
# 创建工具对象
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI)
try:
# 使用code获取access_token
access_token = oauth.get_access_token(code)
# 使用access_token获取openid
openid = oauth.get_open_id(access_token)
except Exception as e:
logger.error(e)
return http.HttpResponseServerError('OAuth2.0认证失败')
# openid QQ用户的ID
# 使用openid判断该QQ用户是否绑定商城的用户
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果没有找到记录 openid 未绑定商城用户 展示绑定页面
# openid 是明文 Sdasdasdasdasy7iyw432 => 明文 可逆 签名的算法
context = {'access_token_openid': generate_access_token(openid)}
return render(request, 'oauth_callback.html', context=context)
else:
# 找到记录 登录
# 状态保持
login(request, oauth_user.user)
next = request.GET.get('state')
response = redirect(next)
response.set_cookie('username', oauth_user.user.username, max_age=3600 * 24)
# 响应结果 重定向到首页
return response
6、用户绑定实现
- 用户绑定实现类似于用户注册的业务逻辑
- 当用户输入的手机号对应的用户已存在
- 直接将该已存在用户跟openid绑定
- 当用户输入的手机号对应的用户不存在
- 新建一个用户,并跟openid绑定
6.1 请求方式
选项 | 方案 |
---|---|
请求方法 | POST |
请求地址 | /oauth_callback/ |
6.2 请求参数:表单参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
username | string | 是 | 用户名 |
password | string | 是 | 密码 |
mobile | string | 是 | 手机号 |
sms_code | string | 是 | 短信验证码 |
6.3 响应结果
响应结果 | 响应内容 |
---|---|
注册失败 | 响应错误提示 |
注册成功 | 重定向到首页 |
6.4 views实现
- 接收参数
- 手机号、密码、短信验证码、密文的openid
- 校验参数
- 短信验证码是否过期,如果过期,返回错误信息
- 短信验证码是否正确,如果错误,返回错误信息
- 解密openid,如果为空(过期或错误),返回错误信息
- 检查手机号是否存在(即是否注册过)
- 如果手机没有注册过,以此手机号为用户名创建新用户(用户名=手机,密码,手机号)
- 如果手机号注册过,判断密码是否正确,如果错误,返回错误
- 绑定用户
- 将用记信息与openid保存到数据库中
- 保持状态
- 保存cookie
- 重定向
def post(self,request):
"""实现绑定用户的业务逻辑"""
# 接收参数
mobile = request.POST.get('mobile')
password = request.POST.get('password')
sms_code_client = request.POST.get('sms_code')
access_token = request.POST.get('access_token_openid') # openid密文
# 校验参数
# 判断短信验证码是否一致
redis_conn = get_redis_connection('verify_code')
sms_code_server = redis_conn.get('sms_%s' % mobile)
if sms_code_server is None:
return render(request, 'oauth_callback.html', {
'sms_code_errmsg': '无效的短信验证码'})
if sms_code_client != sms_code_server.decode():
return render(request, 'oauth_callback.html', {
'sms_code_errmsg': '输入短信验证码有误'})
# 判断openid是否有效
openid = check_access_token(access_token)
if not openid: # openid 过期 或 不对
return render(request, 'oauth_callback.html', {
'openid_errmsg': 'openid已经失效'})
# 使用手机号查询对应的用户是否存在
try:
user = User.objects.get(mobile=mobile)
except User.DoesNotExist:
# 如果不存在, 创建一个新的用户
user = User.objects.create_user(username=mobile, password=password, mobile=mobile)
else:
# 如果存在,校验密码
if not user.check_password(password):
return render(request, 'oauth_callback.html', {
'account_errmsg': '账号或者密码错误'})
# 绑定用户
# oauth_qq_user = OAuthQQUser(user=user, openid=openid)
# oauth_qq_user.save()
try:
oauth_qq_user = OAuthQQUser.objects.create(user=user, openid=openid)
except Exception as e:
return render(request, 'oauth_callback.html', {
'account_errmsg': '账号或者密码错误'})
# 重定向到首页
# 状态保持
login(request, oauth_qq_user.user)
next = request.GET.get('state')
# print(next)
response = redirect(next)
response.set_cookie('username', oauth_qq_user.user.username, max_age=3600 * 24)
# 响应结果 重定向到首页
return response