QQ登录:即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。
若想实现QQ登录,需要成为QQ互联的开发者,审核通过才可实现。
成为QQ互联开发者后,还需创建应用,即获取本项目对应与QQ互联的应用ID。
QQ互联提供有开发文档,帮助开发者实现QQ登录。
1、客户端点击QQ登陆按钮,请求商城得到扫码登陆链接,
2、客户端打开扫码登陆链接,请求QQ互联返回客户端扫码登陆页面,
3、客户端扫码,请求QQ互联返回Authorization Code,
4、商城使用Authorization Code请求QQ互联得到access_token,
5、商城使用access_token请求QQ互联得到openid.
QQ登录成功后,我们需要将QQ用户和美多商场用户关联到一起,方便下次QQ登录时使用,所以我们选择使用MySQL数据库进行存储。
为了给项目中模型类补充数据创建时间和更新时间两个字段,我们需要定义模型类基类。 在meiduo_mall.utils/models.py文件中创建模型类基类。
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:
abstract = True # 说明是抽象模型类, 用于继承使用,数据库迁移时不会创建BaseModel的表
创建一个新的应用oauth,用来实现QQ第三方认证登录。
# oauth
url(r'^oauth/', include('oauth.urls')),
在oauth/models.py中定义QQ身份(openid)与用户模型类User的关联关系
from django.db import models
from meiduo_mall.utils.models import BaseModel
# Create your models here.s
class OAuthQQUser(BaseModel):
"""QQ登录用户数据"""
user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name='用户')
openid = models.CharField(max_length=64, verbose_name='openid', db_index=True)
class Meta:
db_table = 'tb_oauth_qq'
verbose_name = 'QQ登录用户数据'
verbose_name_plural = verbose_name
$ python manage.py makemigrations $ python manage.py migrate
pip install QQLoginTool
from QQLoginTool.QQtool import OAuthQQ
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()
access_token = oauth.get_access_token(code)
openid = oauth.get_open_id(access_token)
待处理业务逻辑
# 提取code请求参数
# 使用code向QQ服务器请求access_token
# 使用access_token向QQ服务器请求openid
# 使用openid查询该QQ用户是否在美多商城中绑定过用户
# 如果openid已绑定美多商城用户,直接生成JWT token,并返回
# 如果openid没绑定美多商城用户,创建用户并绑定到openid
1.请求方式
选项 |
方案 |
请求方法 |
GET |
请求地址 |
/qq/authorization/ |
2.请求参数:查询参数
参数名 |
类型 |
是否必传 |
说明 |
next |
string |
否 |
用于记录QQ登录成功后进入的网址 |
3.响应结果:JSON
字段 |
说明 |
code |
状态码 |
errmsg |
错误信息 |
login_url |
QQ登录扫码页面链接 |
4.后端逻辑实现
class QQAuthURLView(View):
"""提供QQ登录页面网址
https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&state=xxx
"""
def get(self, request):
# next表示从哪个页面进入到的登录页面,将来登录成功后,就自动回到那个页面
next = request.GET.get('next')
# 获取QQ登录页面网址
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})
5.QQ登录参数
QQ_CLIENT_ID = '101518219'
QQ_CLIENT_SECRET = '418d84ebdc7241efb79536886ae95224'
QQ_REDIRECT_URI = 'http://www.meiduo.site:8000/oauth_callback'
提示:
http://www.meiduo.site:8000/oauth_callback
http://www.meiduo.site:8000/oauth_callback/?code=AE263F12675FA79185B54870D79730A7&state=%2F
class QQAuthUserView(View):
"""用户扫码登录的回调处理"""
def get(self, request):
"""Oauth2.0认证"""
# 接收Authorization Code
code = request.GET.get('code')
if not code:
return http.HttpResponseForbidden('缺少code')
pass
子应用路由
url(r'^oauth_callback/$', views.QQAuthUserView.as_view()),
class QQAuthUserView(View):
"""用户扫码登录的回调处理"""
def get(self, request):
"""Oauth2.0认证"""
# 提取code请求参数
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向QQ服务器请求access_token
access_token = oauth.get_access_token(code)
# 使用access_token向QQ服务器请求openid
openid = oauth.get_open_id(access_token)
except Exception as e:
logger.error(e)
return http.HttpResponseServerError('OAuth2.0认证失败')
pass
编辑 /etc/hosts
sudo vim /etc/hosts
编辑 C:\Windows\System32\drivers\etc\hosts
使用openid查询该QQ用户是否在美多商城中绑定过用户。
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果openid没绑定美多商城用户
pass
else:
# 如果openid已绑定美多商城用户
pass
如果openid已绑定美多商城用户,直接生成状态保持信息,登录成功,并重定向到首页。
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果openid没绑定美多商城用户
pass
else:
# 如果openid已绑定美多商城用户
# 实现状态保持
qq_user = oauth_user.user
login(request, qq_user)
# 响应结果
# 获取界面跳转来源
next = request.GET.get('state')
response = redirect(next)
# 登录时用户名写入到cookie,有效期15天
response.set_cookie('username', qq_user.username, max_age=3600 * 24 * 15)
return response
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果openid没绑定美多商城用户
access_token = generate_eccess_token(openid)
context = {'access_token': access_token}
return render(request, 'oauth_callback.html', context)
else:
# 如果openid已绑定美多商城用户
# 实现状态保持
qq_user = oauth_user.user
login(request, qq_user)
# 响应结果
# 获取界面跳转来源
next = request.GET.get('state')
response = redirect(next)
# 登录时用户名写入到cookie,有效期15天
response.set_cookie('username', qq_user.username, max_age=3600 * 24 * 15)
return response
oauth_callback.html中渲染access_token
安装:pip install itsdangerous
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': '18512345678'})
token = token.decode()
# 检验token
# 验证失败,会抛出itsdangerous.BadData异常
serializer = Serializer(settings.SECRET_KEY, 300)
try:
data = serializer.loads(token)
except BadData:
return None
补充:openid签名处理
oauth.utils.py
def generate_eccess_token(openid):
"""
签名openid
:param openid: 用户的openid
:return: access_token
"""
serializer = Serializer(settings.SECRET_KEY, expires_in=constants.ACCESS_TOKEN_EXPIRES)
data = {'openid': openid}
token = serializer.dumps(data)
return token.decode()
类似于用户注册的业务逻辑
class QQAuthUserView(View):
"""用户扫码登录的回调处理"""
def get(self, request):
"""Oauth2.0认证"""
......
def post(self, request):
"""美多商城用户绑定到openid"""
# 接收参数
mobile = request.POST.get('mobile')
pwd = request.POST.get('password')
sms_code_client = request.POST.get('sms_code')
access_token = request.POST.get('access_token')
# 校验参数
# 判断参数是否齐全
if not all([mobile, pwd, sms_code_client]):
return http.HttpResponseForbidden('缺少必传参数')
# 判断手机号是否合法
if not re.match(r'^1[3-9]\d{9}$', mobile):
return http.HttpResponseForbidden('请输入正确的手机号码')
# 判断密码是否合格
if not re.match(r'^[0-9A-Za-z]{8,20}$', pwd):
return http.HttpResponseForbidden('请输入8-20位的密码')
# 判断短信验证码是否一致
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是否有效:错误提示放在sms_code_errmsg位置
openid = check_access_token(access_token)
if not 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=pwd, mobile=mobile)
else:
# 如果用户存在,检查用户密码
if not user.check_password(pwd):
return render(request, 'oauth_callback.html', {'account_errmsg': '用户名或密码错误'})
# 将用户绑定openid
try:
OAuthQQUser.objects.create(openid=openid, user=user)
except DatabaseError:
return render(request, 'oauth_callback.html', {'qq_login_errmsg': 'QQ登录失败'})
# 实现状态保持
login(request, user)
# 响应绑定结果
next = request.GET.get('state')
response = redirect(next)
# 登录时用户名写入到cookie,有效期15天
response.set_cookie('username', user.username, max_age=3600 * 24 * 15)
return response