一 QQ登录开发文档
QQ登录:即我们所说的 第三⽅登录,是指⽤户可以不在本项⽬中输⼊密码,⽽直接 通过第三⽅的验证,成功登录本项⽬。
1.1 QQ互联开发者申请步骤
若想实现QQ登录,需要成为 QQ互联的开发者,审核通过 才可实现。
1.2 QQ互联应⽤申请步骤
成为QQ互联开发者后,还需 创建应⽤,即 获取本项⽬对应与QQ互联的应⽤ID。
1.3 ⽹站接⼊QQ登录功能实现
QQ互联提供有 开发⽂档,帮助开发者实现QQ登录。
1.4 QQ登录流程分析
1.5 知识要点
当我们在对接第三⽅平台的接⼝时,⼀定要认真阅读 第三⽅平台提供的⽂档。⽂档 中⼀定会有接⼝的使⽤说明,⽅便我们开发。
二 定义QQ登录模型类
QQ登录成功后,我们需要 将QQ⽤户和芒果头条⽤户 关联 到⼀起,⽅便下次QQ登录 时使⽤,所以我们选择 使⽤MySQL数据库进⾏存储。
2.1 定义模型类基类
为了给项⽬中模型类补充 数据创建时间和更新时间两个字段,我们需要 定义模型类 基类。 在mgproject.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:
# 说明是抽象模型类, ⽤于继承使⽤,数据库迁移时不会 创建BaseModel的表
abstract = True
2.2 定义QQ登录模型类
创建⼀个新的应⽤oauth,⽤来实现QQ第三⽅认证登录。
# oauth
re_path(r'^oauth/', include('oauth.urls')),
在oauth/models.py中 定义QQ身份(openid)与⽤户模型类Users的关联关系。
from django.db import models
from mgproject.utils.models import BaseModel
# Create your models here
class OAuthQQUser(BaseModel):
"""QQ登录⽤户数据"""
user = models.ForeignKey('userapp.Users',
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
2.3 迁移QQ登录模型类
$ python manage.py makemigrations
$ python manage.py migrate
三 QQ登录工具AgentLogin
3.1 AgentLogin介绍
⽬前 只⽀持 腾讯QQ,微信,微博 的第三⽅登录
该⼯具封装了QQ登录 时对接QQ互联接⼝的 请求操作。可⽤于快速实现QQ登录功能。
3.2 AgentLogin安装
pip install AgentLogin
3.3 AgentLogin 使⽤说明
from AgentLogin import AgentLogin
# 获取扫码⻚⾯地址
qq_url = AgentLogin.qq_url(client_id, redirect_uri)
# <a href="{{ qq_url }}">QQ登录<a>
# client_id:QQ互联上应⽤的APPID
# redirect_uri: QQ互联上应⽤的⽹站回调域
# 获取⽤户名和openid
AgentLogin.qq(client_id, client_secret, url, code)
# 获取⽤户所有信息
AgentLogin.all_qq(client_id, client_secret, url, code)
# client_id: QQ互联上应⽤的 APPID
# client_secret: QQ互联上应⽤的APP Key
# url: QQ互联上应⽤的⽹站回调域
# code: 从QQ服务器得到code
# 注意此code会在10分钟内过期
AgentLogin.all_qq(settings.QQ_CLIENT_ID, settings.QQ_APP_KEY,
settings.QQ_REDIRECT_URI, code)
四 通过OAuth2.0认证 获取openid
- 提取code 请求参数
- 使⽤code向QQ服务器请求 access_token
- 使⽤access_token向QQ服务器请求 openid
- 使⽤openid查询 该QQ⽤户是否芒果头条中 绑定过⽤户
- 如果openid已绑定 芒果头条⽤户,直接⽣成JWT token,并返回
- 如果openid没绑定 芒果头条⽤户,创建⽤户并绑定到openid
4.1 获取QQ登录扫码⻚⾯
4.1.1 请求⽅式
选项 | ⽅案 |
请求⽅法 | GET |
请求地址 | /qq/login/ |
4.1.2 请求参数:⽆
4.1.3 响应结果:JSON
字段 | 说明 |
code | 状态码 |
errmsg | 错误信息 |
login_url | QQ登录扫码⻚⾯链接 |
4.1.4 后端逻辑实现
class QQLoginURLView(View):
"""
提供QQ登录⻚⾯⽹址
"""
def get(self, request):
# 获取QQ登录⻚⾯⽹址
qq_login_url = AgentLogin.qq_url(settings.QQ_CLIENT_ID,
settings.QQ_REDIRECT_URI)
return http.JsonResponse({'code': 200, 'errmsg': 'OK',
'qq_login_url': qq_login_url})
4.1.5 QQ登录参数
# QQ登录的配置参数
QQ_CLIENT_ID = '101917966'
QQ_REDIRECT_URI = 'http://www.nagle.cn:8083/about'
QQ_APP_KEY = '20fcc768255829c08fa4efbe8acf0001'
4.2 接收Authorization Code
提示:
- ⽤户在QQ登录成功后,QQ会 将⽤户重定向到我们配置的回调⽹址。
- 在QQ 重定向到回调⽹址时,会传给我们⼀个Authorization Code。
- 我们需要拿到Authorization Code并 完成OAuth2.0认证获取openid。
- 在本项⽬中,我们申请QQ登录开发资质时配置的 回调⽹址 为:
- http://www.nagle.cn:8083/about
- QQ互联重定向的 完整⽹址 为:
- http://www.nagle.cn/about/? code=991088ECBF489B38CFBDF1BB4B093EC9
class QQLoginUserView(View):
"""⽤户扫码登录的回调处理"""
def get(self, request):
"""Oauth2.0认证"""
# 接收Authorization Code
code = request.GET.get('code')
if not code:
raise Forbbiden('缺少code')
pass
re_path(r'^about/$', views.QQLoginUserView.as_view()),
4.3 OAuth2.0认证 获取openid
import logging
from django import http
logger = logging.getLogger('django')
class QQLoginUserView(View):
"""⽤户扫码登录的回调处理"""
def get(self, request):
"""Oauth2.0认证"""
# 接收Authorization Code
code = request.GET.get('code')
if not code:
return http.HttpResponseForbidden('缺少code')
try:
nickname, openid = AgentLogin.qq(
settings.QQ_CLIENT_ID, settings.QQ_APP_KEY,
settings.QQ_REDIRECT_URI, code)
except Exception as e:
logger.error(e)
return http.HttpResponseServerError('OAuth2.0认证失败')
pass
4.4 本机绑定www.nagle.cn域名
1.ubuntu系统或者Mac系统
sudo vi /etc/hosts
127.0.0.1 www.nagle.cn
4.5 修改 dev.py 配置⽂件
ALLOWED_HOSTS = ['www.nagle.cn','127.0.0.1']
4.6 修改服务器端⼝号
4.7 配置回调地址路由
# oauth/urls.py
re_path('^about/$',views.QQAuthUserView.as_view()),
4.8 创建类视图
class QQAuthUserView(View):
def get(self, request):
"""
获取openid
:param request:
:return:
"""
# 获取code参数
code = request.GET.get('code', '')
# 校验参数
if not code:
return http.HttpResponseForbidden('缺少code参数值')
# 调⽤接⼝⽅法获取openid
nickname, openid = AgentLogin.qq(
settings.QQ_CLIENT_ID, settings.QQ_APP_KEY,
settings.QQ_REDIRECT_URI, code)
return HttpResponse(openid)
五 QQ用户 是否绑定项目用户 的处理
5.1 判断 openid是否绑定过⽤户
使⽤openid 查询该QQ⽤户是否在芒果头条中绑定过⽤户。
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果openid没绑定芒果头条⽤户
pass
else:
# 如果openid已绑定芒果头条⽤户
pass
5.2 openid 已绑定⽤户的处理
如果openid已绑定芒果头条⽤户,直接⽣成状态保持信息,登录成功,并 重定向到⾸⻚。
class QQOauthUser(View):
def get(self, request):
# 获取code参数
code = request.GET.get('code', '')
if code == '':
return http.HttpResponseForbidden('缺少参数')
# 获取QQ⽤户名和openid
nickname, openid = AgentLogin.qq(
settings.QQ_CLIENT_ID, settings.QQ_APP_KEY,
settings.QQ_REDIRECT_URI, code)
try:
# 查询当前QQ⽤户是否绑定芒果头条⽤户
qq_user = QQAuthUser.objects.get(openid=openid)
except QQAuthUser.DoesNotExist:
# 没绑定芒果头条⽤户
pass
else:
# 已绑定芒果头条⽤户
# 实现状态保持
mg_user = qq_user.user
login(request, mg_user)
# 响应结果
return redirect(reverse('newsapp:index'))
5.3 openid 未绑定⽤户的处理
- 为了能够在后续的绑定⽤户操作中前端可以使⽤openid,在这⾥将openid签名后响应给前端。
- openid属于⽤户的隐私信息,所以需要将openid签名处理,避免暴露。
class QQOauthUser(View):
def get(self, request):
# 获取code参数
code = request.GET.get('code', '')
if code == '':
return http.HttpResponseForbidden('缺少参数')
# 获取QQ⽤户名和openid
nickname, openid = AgentLogin.qq(
settings.QQ_CLIENT_ID, settings.QQ_APP_KEY,
settings.QQ_REDIRECT_URI, code)
try:
# 查询当前QQ⽤户是否绑定芒果头条⽤户
qq_user = QQAuthUser.objects.get(openid=openid)
except QQAuthUser.DoesNotExist:
# 没绑定芒果头条⽤户
# 加密openid数据
sec_openid = generate_secret_openid(openid)
return render(request, 'oauth/oauth_user.html',
{'sec_openid': sec_openid})
else:
# 已绑定芒果头条⽤户
# 实现状态保持
mg_user = qq_user.user
login(request, mg_user)
# 响应结果
return redirect(reverse('newsapp:index'))
# oauth/oauth_user.html
<input type="hidden" name="sec_openid" value="{{ sec_openid }}">
5.4 itsdangerous的使⽤
- itsdangerous模块的 参考资料链接
- 安装: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({'uname': 'zhangsan'})
token = token.decode()
# 检验token
# 验证失败,会抛出itsdangerous.BadData异常
serializer = Serializer(settings.SECRET_KEY, 300)
try:
data = serializer.loads(token)
except BadData:
return None
- openid签名处理 (对openid进⾏ 加密)
# oauth/utils.py
def generate_secret_openid(openid):
"""
签名openid
:param openid: ⽤户的openid
:return: access_token
"""
# 创建序列化器对象给数据加密
serializer = Serializer(settings.SECRET_KEY, expires_in=600)
data = {'openid': openid}
token = serializer.dumps(data)
return token.decode()
六 QQ用户绑定项目 用户实现
类似于⽤户注册的业务逻辑
- 当⽤户输⼊的⼿机号对应的 ⽤户已存在
- 直接 将该已存在⽤户跟openid绑定
- 当⽤户输⼊的⼿机号对应的 ⽤户不存在
- 新建⼀个⽤户,并跟openid绑定
# /qq/login/ POST
# oauth/views.py
class QQLoginUserView(View):
"""⽤户扫码登录的回调处理"""
def get(self, request):
"""Oauth2.0认证"""
......
def post(self, request):
"""
QQ⽤户登录成功后绑定芒果头条⽤户
"""
# 接收参数
phone = request.POST.get('phone')
pwd = request.POST.get('password')
sms_code_client = request.POST.get('msgcode')
sec_openid = request.POST.get('sec_openid')
# 校验参数
# 判断参数是否⻬全
if not all([phone, pwd, sms_code_client, sec_openid]):
return http.HttpResponseForbidden('缺少必传参数')
# 判断⼿机号是否合法
if not re.match(r'^1[35789]\d{9}$', phone):
return http.HttpResponseForbidden('请输⼊正确的⼿机号码')
# 判断密码是否合格
if not re.match(r'^[0-9A-Za-z]{3,8}$', pwd):
return http.HttpResponseForbidden('请输⼊3,8位的密码')
# 判断短信验证码是否⼀致
redis_conn = get_redis_connection('verify_code')
sms_code_server = redis_conn.get('sms_%s' % phone)
if sms_code_server is None:
return render(request, 'oauth/oauth_user.html',
{'sms_code_errmsg': '⽆效的短信验证码'})
if sms_code_client != sms_code_server.decode():
return render(request, 'oauth/oauth_user.html',
{'sms_code_errmsg': '输⼊短信验证码有误'})
# 判断openid是否有效:错误提示放在sms_code_errmsg位置
openid = check_secret_openid(sec_openid)
if not openid:
return render(request, 'oauth/oauth_user.html',
{'openid_errmsg': '⽆效的openid'})
# 保存注册数据
try:
user = Users.objects.get(phone=phone)
except Users.DoesNotExist:
# ⽤户不存在,新建⽤户
user = Users.objects.create_user(username=phone,
password=pwd, phone=phone)
else:
# 如果⽤户存在,检查⽤户密码
if not user.check_password(pwd):
return render(request, 'oauth/oauth_user.html',
{'account_errmsg': '⽤户名或密码错误'})
# 将⽤户绑定openid
try:
QQAuthUser.objects.create(openid=openid, user=user)
except DatabaseError:
return render(request, 'oauth/oauth_user.html',
{'qq_login_errmsg': 'QQ登录失败'})
# 实现状态保持
login(request, user)
# 响应绑定结果
return redirect(reverse('newsapp:index'))
七 添加邮箱前端逻辑
7.1 前端逻辑处理
1.user_center.js
methods: {
// 获取指定名称的cookie值
getCookie: function(name) {
var value = '; ' + document.cookie;
var parts = value.split('; ' + name + '=');
if (parts.length === 2) {
// 返回cookie值
return parts.pop().split(';').shift();
}
// 如果没有找到对应的cookie,返回null
return null;
},
// 保存邮箱
save_email: function() {
// 使用axios发送post请求
axios.post('/emails/', {
'email': this.email,
'userid': this.userid
}, {
headers: {
// 从cookie中获取csrftoken并设置到请求头中,解决跨域问题
'X-CSRFToken': this.getCookie('csrftoken')
}
}).then(response => {
// 请求成功后的处理
if (response.data.code == '200') {
// 如果返回码为200,表示成功,刷新页面
location.reload();
this.error_email = false;
} else {
// 否则标记邮箱保存出错
this.error_email = true;
}
}).catch(error => {
// 请求失败后的处理,打印错误信息
console.log(error.response);
});
}
}
2.user_center.html
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td width="30%">⽤户名</td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td>⼿机号</td>
<td>{{ user.phone }}</td>
</tr>
<tr>
<td>邮箱</td>
<td>
{% if user.email %}
{{ user.email}}
{% else %}
<input type="text" v-model="email" style="width: 290px;"
placeholder="输⼊邮箱地址">
<button @click="save_email">保存</button>
<span class="error-tip" v-show="error_email">
${error_email_msg}</span>
{% endif %}
</td>
</tr>
</table>
<script>
// 别忘了加""号
let userid = "{{ user.id }}";
</script>
<script type="text/javascript"
src="{% static 'js/userapp/user_center.js' %}"></script>
7.2 添加邮箱后端逻辑
7.2.1 添加邮箱后端接⼝设计
选项 | ⽅案 |
请求⽅法 | POST |
请求地址 | /emails/ |
7.2.2 请求参数:json参数
参数名 | 类型 | 是否必传 | 说明 |
string | 是 | 邮箱地址 | |
userid | string | 是 | 当前登录⽤户id |
7.2.3 响应结果:JSON
字段 | 说明 |
code | 状态码 |
errmsg | 错误信息 |
7.3 添加邮箱后端实现
7.3.1 配置路由
# userapp/urls.py
re_path('^emails/$',views.EmailView.as_view()),
7.3.2 创建视图
# userapp/views.py
class EmailView(View):
def post(self, request):
params_str = request.body.decode()
if params_str:
p_dict = json.loads(params_str)
count = Users.objects.filter(id=p_dict['userid']).update(
email=p_dict['email'])
if count:
return JsonResponse({'code': 200, 'errormsg': 'OK'})
return JsonResponse({'code': 500, 'errormsg': '保存邮箱地址失败!'})