用户目录
账号登录
用户名登录
1. 用户名登录逻辑分析
错误响应也用html,是否记住密码控制,浏览器记住用户的时间长短,勾选了,记住用户的时间长一点,没勾选,记住浏览器关闭就不记住用户
2. 用户名登录接口设计
- 请求方式
选项 | 方案 |
---|---|
请求方法 | POST |
请求地址 | /login/ |
- 请求参数:表单
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
username | string | 是 | 用户名 |
password | string | 是 | 密码 |
remembered | string | 否 | 是否记住用户 |
remember 用于控制状态保持的长短 |
- 响应结果:HTML
字段 | 说明 |
---|---|
登录失败 | 响应错误提示 |
登录成功 | 重定向到首页 |
3. 用户名登录接口定义
class LoginView(View):
"""用户名登录"""
def get(self, request):
"""
提供登录界面
:param request: 请求对象
:return: 登录界面
"""
pass
def post(self, request):
"""
实现登录逻辑
:param request: 请求对象
:return: 登录结果
"""
pass
写完视图就去写url,将对应的模板和js 拷贝进来
4. 用户名登录后端逻辑
from django.contrib.auth import login, authenticate
class LoginView(View):
"""用户名登录"""
def get(self, request):
"""
提供登录界面
:param request: 请求对象
:return: 登录界面
"""
return render(request, 'login.html')
def post(self, request):
"""
实现登录逻辑
:param request: 请求对象
:return: 登录结果
"""
# 接受参数
username = request.POST.get('username')
password = request.POST.get('password')
remembered = request.POST.get('remembered')
# 校验参数
# 判断参数是否齐全
if not all([username, password]):
return http.HttpResponseForbidden('缺少必传参数')
# 判断用户名是否是5-20个字符
if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', username):
return http.HttpResponseForbidden('请输入正确的用户名或手机号')
# 判断密码是否是8-20个数字
if not re.match(r'^[0-9A-Za-z]{8,20}$', password):
return http.HttpResponseForbidden('密码最少8位,最长20位')
# 认证登录用户
# 查到就返回用户,查不到就返回None
user = authenticate(username=username, password=password)
if user is None:
return render(request, 'login.html', {'account_errmsg': '用户名或密码错误'})
# 实现状态保持
login(request, user)
# 设置状态保持的周期,复选框被勾选会传递一个on字符串
if remembered != 'on':
# 没有记住用户:浏览器会话结束就过期
#request.session.set_expiry 可以设置状态保持的时间
request.session.set_expiry(0)
else:
# 记住用户:状态保持为两周,但是状态保持的时间应该
#短一些,这里设置为两周是为了回顾知识点
# 传None, 默认状态保持的时间为两周
request.session.set_expiry(None)
# 响应登录结果,建议使用反向解析实现
return redirect(reverse('contents:index'))
5. 知识要点
- 登录的核心思想:认证和状态保持
- 通过用户的认证,确定该登录用户是美多商场的注册用户。
- 通过状态保持缓存用户的唯一标识信息,用于后续是否登录的判断。
6.测试
debug时关注传入数据的情况和程序的执行流程
分别测试勾选记住登录和不记住登录的两种情况
观察session_id 的时间,可以得知状态保持的时间
是不是清除掉session_id,用户就无法记录登录状态了,之后就可以再次进行登录
多账号登录
用户认证系统默认用username 认证用户,因此需要自定义用户认证方案,以实现多账号登录
ctrl + 左击 点击报名,进入包的__init__ 文件
-
Django自带的用户认证后端默认是使用用户名实现用户认证的。
-
用户认证后端位置:django.contrib.auth.backends.ModelBackend。
认证的时候会进入此代码
会捕获用户查不到异常,未捕获到异常会校验密码以及认证 -
如果想实现用户名和手机号都可以认证用户,就需要自定义用户认证后端。
-
自定义用户认证后端步骤
- 在users应用中新建utils.py文件
- 新建类,继承自ModelBackend
- 重写认证authenticate()方法
- 分别使用用户名和手机号查询用户
- 返回查询到的用户实例
1. 自定义用户认证后端
users.utils.py
from django.contrib.auth.backends import ModelBackend
import re
from .models import User
def get_user_by_account(account):
"""
根据account查询用户
:param account: 用户名或者手机号
:return: user
"""
try:
if re.match('^1[3-9]\d{9}$', account):
# 手机号登录
user = User.objects.get(mobile=account)
else:
# 用户名登录
user = User.objects.get(username=account)
except User.DoesNotExist:
return None
else:
return user
class UsernameMobileAuthBackend(ModelBackend):
"""自定义用户认证后端"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
重写认证方法,实现多账号登录
:param request: 请求对象
:param username: 用户名
:param password: 密码
:param kwargs: 其他参数
:return: user
"""
# 根据传入的username获取user对象。username可以是手机号也可以是账号
user = get_user_by_account(username)
# 校验user是否存在并校验密码是否正确
if user and user.check_password(password):
return user
2. 配置自定义用户认证后端
- Django自带认证后端源码
- 配置自定义用户认证后端(在dev.py)
# 指定自定义的用户认证后端
AUTHENTICATION_BACKENDS = ['users.utils.UsernameMobileAuthBackend']
# 从包名写起,写到类名结束
3. 测试自定义用户认证后端
4. 知识要点
- Django自带的用户认证系统只会使用用户名去认证一个用户。
- 所以我们为了实现多账号登录,就可以自定义认证后端,采用其他的唯一信息去认证一个用户。
首页用户名展示
1. 首页用户名展示方案
方案一
jinja2 模板引擎自带user对象
- 模板中 request 变量直接渲染用户名
- 缺点:不方便做首页静态化
后期这个首页要做成静态资源,不能通过渲染得到页面
{% if user.is_authenticated %}
<div class="login_btn fl">
欢迎您:<em>{{ user.username }}</em>
<span>|</span>
<a href="#">退出</a>
</div>
{% else %}
<div class="login_btn fl">
<a href="login.html">登录</a>
<span>|</span>
<a href="register.html">注册</a>
</div>
{% endif %}
方案二
- 发送ajax请求获取用户信息
- 缺点:需要发送网络请求
一个页面上发送网络请求的数量尽可能少
<div class="login_btn fl">
{# ajax渲染 #}
</div>
方案三
- Vue读取cookie渲染用户信息
<div v-if="username" class="login_btn fl">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a href="#">退出</a>
</div>
<div v-else class="login_btn fl">
<a href="login.html">登录</a>
<span>|</span>
<a href="register.html">注册</a>
</div>
结论:
-
对比此三个方案,我们在本项目中选择 方案三
实现步骤: -
注册或登录后,用户名写入到cookie
-
Vue渲染主页用户名
2. 用户名写入到cookie
响应注册结果
# redict 就可以返回response
response = redirect(reverse('contents:index'))
# 注册时用户名写入到cookie,有效期15天
response.set_cookie('username', user.username, max_age=3600 * 24 * 15)
return response
# 响应登录结果,在登录和注册时都会做这个工作
response = redirect(reverse('contents:index'))
# 登录时用户名写入到cookie,有效期15天
response.set_cookie('username', user.username, max_age=3600 * 24 * 15)
return response
3. Vue渲染首页用户名
- index.html
<!--只有当这一坨代码中,变量渲染完成,才显示此代码块-->
<div v-cloak>
<div v-if="username" class="login_btn fl">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a href="#">退出</a>
</div>
<div v-else class="login_btn fl">
<a href="login.html">登录</a>
<span>|</span>
<a href="register.html">注册</a>
</div>
</div v-cloak>
- index.js
mounted(){
// 获取cookie中的用户名
//来自于Common.js
this.username = getCookie('username');
},
退出登录
1. logout()方法介绍
- 退出登录:
- 回顾登录:将通过认证的用户的唯一标识信息,写入到当前session会话中
- 退出登录:正好和登录相反(清理session会话信息)
- logout()方法:
- Django用户认证系统提供了logout()方法
- 封装了清理session的操作,帮助我们快速实现登出一个用户
- logout()位置:
- django.contrib.auth.init.py文件中
logout(request)
2. logout()方法使用
class LogoutView(View):
"""退出登录"""
def get(self, request):
"""实现退出登录逻辑"""
# 清理session
logout(request)
# 退出登录,重定向到登录页
response = redirect(reverse('contents:index'))
# 退出登录时清除cookie中的username
response.delete_cookie('username')
return response
url 用的就是reverse
反向解析生成url,会直接填写在url中
3. 知识要点
- 退出登录的核心思想就是清理登录时缓存的状态保持信息。
- 由于首页中用户名是从cookie中读取的。所以退出登录时,需要将cookie中用户名清除。
判断用户是否登录
这个代码在很多地方都要用到,需要封装
1. 展示用户中心界面
class UserInfoView(View):
"""用户中心"""
def get(self, request):
"""提供个人信息界面"""
return render(request, 'user_center_info.html')
需求:
-
当用户登录后,才能访问用户中心。
-
如果用户未登录,就不允许访问用户中心,将用户引导到登录界面。
实现方案: -
需要判断用户是否登录。
-
根据是否登录的结果,决定用户是否可以访问用户中心。
2. is_authenticate 判断用户是否登录
介绍:
- Django用户认证系统提供了方法request.user.is_authenticated()来判断用户是否登录。
- 如果通过登录验证则返回True。反之,返回False。
- 缺点:登录验证逻辑很多地方都需要,所以该代码需要重复编码好多次。
class UserInfoView(View):
"""用户中心"""
def get(self, request):
"""提供个人信息界面"""
if request.user.is_authenticated():
return render(request, 'user_center_info.html')
else:
return redirect(reverse('users:login'))
3. login_required装饰器 判断用户是否登录
- Django用户认证系统提供了装饰器login_required来判断用户是否登录。
- 内部封装了is_authenticate
- 位置:django.contrib.auth.decorators
- 如果通过登录验证则进入到视图内部,执行视图逻辑。
- 如果未通过登录验证则被重定向到 LOGIN_URL配置项指定的地址。
- 如下配置:表示当用户未通过登录验证时,将用户重定向到登录页面。
LOGIN_URL = '/login/'
下面前三种方法不好用
- 装饰as_view()方法返回值
提示:
-
login_required装饰器可以直接装饰函数视图,但是本项目使用的是类视图。
-
as_view()方法的返回值就是将类视图转成的函数视图。
结论: -
要想使用login_required装饰器装饰类视图,可以间接的装饰as_view()方法的返回值,以达到预期效果。
url(r'^info/$', login_required(views.UserInfoView.as_view()), name='info'),
class UserInfoView(View):
"""用户中心"""
def get(self, request):
"""提供个人信息界面"""
return render(request, 'user_center_info.html')
- 定义View子类封装login_required装饰器
- 提示:LoginRequired(object)依赖于视图类View,复用性很差。
url(r'^info/$', views.UserInfoView.as_view(), name='info'),
class LoginRequired(View):
"""验证用户是否登陆"""
@classmethod
def as_view(cls, **initkwargs):
# 自定义as_view()方法中,调用父类的as_view()方法
view = super().as_view()
return login_required(view)
class UserInfoView(LoginRequired):
"""用户中心"""
def get(self, request):
"""提供个人信息界面"""
return render(request, 'user_center_info.html')
- 定义obejct子类封装login_required装饰器
- 提示:LoginRequired(object)不依赖于任何视图类,复用性更强。
url(r'^info/$', views.UserInfoView.as_view(), name='info'),
class LoginRequired(object):
"""验证用户是否登陆"""
@classmethod
def as_view(cls, **initkwargs):
# 自定义as_view()方法中,调用父类的as_view()方法
view = super().as_view()
return login_required(view)
class UserInfoView(LoginRequired, View):
"""用户中心"""
def get(self, request):
"""提供个人信息界面"""
return render(request, 'user_center_info.html')
- 定义验证用户是否登录扩展类
扩展类的生成为了:1. 封装代码 2. 扩展认证功能
- 提示:定义扩展类方便项目中导入和使用(meiduo_mall.utils.views.py)
类方法也可以由实例对象调用
class LoginRequiredMixin(object):
"""验证用户是否登录扩展类"""
@classmethod
def as_view(cls, **initkwargs):
# 自定义的as_view()方法中,调用父类的as_view()方法
view = super().as_view()
return login_required(view)
class UserInfoView(LoginRequiredMixin, View):
"""用户中心"""
def get(self, request):
"""提供个人信息界面"""
return render(request, 'user_center_info.html')
4. 登录时next参数的使用
1. next参数的效果
next 控制登录成功后跳转到的地址
http://127.0.0.1:8000/login/?next=/info/
- next参数的作用
用户进入某个页面失败,返回登录页时,会自动在新的登录请求上附上本次请求失败的url路径
- 由Django用户认证系统提供,搭配login_required装饰器使用。
- 记录了用户未登录时访问的地址信息,可以帮助我们实现在用户登录成功后直接进入未登录时访问的地址。但是提取url并且进行访问的工作需要我们自己做
# 响应登录结果
# 取get参数时,存在返回数据,不存在就None 或者 "" 具体我也不知道是哪个
next = request.GET.get('next')
if next:
response = redirect(next)
else:
response = redirect(reverse('contents:index'))
5. 知识要点
- 判断用户是否登录依然使用状态保持信息实现。
- 项目中很多接口都是需要用户登录才能访问的,所以为了方便编码,我们将判断用户登录的操作封装到装饰器中。
- 登录时next参数的作用是为了方便用户从哪里进入到登录页面,登录成功后就回到哪里。
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
点击小图标为注册成为开发者的入口
没有注册点开这样
个人接入填一些信息和自拍照之后经过审核
审核完发邮件
应用管理可以申请应用
可以创建应用
点击创建网站应用
填写工具,学习教育 创建应用
这个表会有运营做
回调地址为登录后跳转到的地址
这样的域名说明都是做了备案的,之后创建应用
审核通过之后,appID 为qq开的服务
next 为了从哪来回哪去,不太清楚next 为什么能回调回去,scope说明可以使用的接口
这个地址是用来获取二维码页面
扫描完二维码,就可以获取到code和原始
的state值,这个code 会在10分钟内过期
openID 标识用户,将这个ID和美多用户绑定起来
每一步都是向一个服务器发get请求
拿到用户ID过程即完成
每一步都是为了后一步做准备
获取扫码页面->code->token->id(OAuth 2.0 认证,为了保护第三方平台的安全),许多第三方平台都用这个
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登录流程分析
-
知识要点
- 当我们在对接第三方平台的接口时,一定要认真阅读第三方平台提供的文档。文档中一定会有接口的使用说明,方便我们开发。
定义QQ登录模型类
QQ登录成功后,我们需要将QQ用户和美多商场用户关联到一起,方便下次QQ登录时使用,所以我们选择使用MySQL数据库进行存储。
1. 定义模型类基类
为了给项目中模型类补充数据创建时间和更新时间两个字段,我们需要定义模型类基类。 在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的表
2. 定义QQ登录模型类(只需要拷贝过来就行)
创建一个新的应用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.C harField(max_length=64, verbose_name='openid', db_index=True)
class Meta:
db_table = 'tb_oauth_qq'
verbose_name = 'QQ登录用户数据'
verbose_name_plural = verbose_name
3. 迁移QQ登录模型类
$ python manage.py makemigrations
$ python manage.py migrate
QQ登录工具QQLoginTool(封装了文档中的过程)
并没有封装对接qq互联的SDK,
1. QQLoginTool介绍
- 该工具封装了QQ登录时对接QQ互联接口的请求操作。
可用于快速实现QQ登录。如果有SDK,应该是操作非常简单,填写一些参数就行,所以可能需要自己做,官方没有提供,民间可能提供这样的SDK
2. QQLoginTool安装
pip install QQLoginTool
3. QQLoginTool使用说明
- 导入
from QQLoginTool.QQtool import OAuthQQ
# 导入的是一个类
- 初始化OAuthQQ对象
# client_id 为appid client_secret 为App key
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET, redirect_uri=settings.QQ_REDIRECT_URI, state=next)
- 获取QQ登录扫码页面,扫码后得到
# Authorization Code
login_url = oauth.get_qq_url()
- 通过Authorization Code获取Access Token
access_token = oauth.get_access_token(code)
- 通过Access Token获取OpenID
openid = oauth.get_open_id(access_token)
OAuth2.0认证获取openid
待处理业务逻辑
# 提取code请求参数
# 使用code向QQ服务器请求access_token
# 使用access_token向QQ服务器请求openid
# 使用openid查询该QQ用户是否在美多商城中绑定过用户
# 如果openid已绑定美多商城用户,直接生成JWT token,并返回
# 如果openid没绑定美多商城用户,创建用户并绑定到openid
1. 获取QQ登录扫码页面
- 请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /qq/login/ |
- 请求参数:查询参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
next | string | 否 | 用于记录QQ登录成功后进入的网址 |
这个参数用于给state
3. 响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
login_url QQ登录扫码页面链接 |
- 后端逻辑实现
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})
- QQ登录参数
QQ_CLIENT_ID = '101518219'
QQ_CLIENT_SECRET = '418d84ebdc7241efb79536886ae95224'
QQ_REDIRECT_URI = 'http://www.meiduo.site:8000/oauth_callback'
扫完码会出现无法访问的问题,主要因为我们的电脑无法解析
www.meiduo.site
需要在当前机器上为其绑定IP
2. 接收Authorization Code
提示:
- 用户在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
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()),
3. OAuth2.0认证获取openid
获取完code之后,这两部分由服务器和qq互联沟通
- 使用code向QQ服务器请求access_token
- 使用access_token向QQ服务器请求openid
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
在第二次请求就不用传code,因为第一次写完code就一直在地址栏中带着
4. 本机绑定www.meiduo.site域名
- ubuntu系统或者Mac系统
编辑 /etc/hosts
报这个错应该是本机认识这个域名了,需要添加allowed hosts域名
出现这个错误应该是当前url没有绑定视图
- Windows系统
编辑 C:\Windows\System32\drivers\etc\hosts
openid是否绑定用户的处理
1. 判断openid是否绑定过用户
使用openid查询该QQ用户是否在美多商城中绑定过用户。
try:
# get不到用户会抛出异常
oauth_user = OAuthQQUser.objects.get(openid=openid)
# 抛出异常之后代码好像
except OAuthQQUser.DoesNotExist:
# 如果openid没绑定美多商城用户
pass
else:
# 如果openid已绑定美多商城用户
pass
2. openid已绑定用户的处理
reverse
如果openid已绑定美多商城用户,直接生成状态保持信息,登录成功,并重定向到首页。
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果openid没绑定美多商城用户
pass
else:
# 如果openid已绑定美多商城用户
# 实现状态保持,直接点外键名就可以获取外键对应的数据对象
# python 好像没有什么作用域的问题,赋予了就可以使用
qq_user = oauth_user.user
login(request, qq_user)
# 响应结果
next = request.GET.get('state')
response = redirect(next)
# 登录时用户名写入到cookie,有效期15天,max_age 单位是秒
response.set_cookie('username', qq_user.username, max_age=3600 * 24 * 15)
return response
3. openid未绑定用户的处理
- 为了能够在后续的绑定用户操作中前端可以使用openid,在这里将openid签名后响应给前端。
- openid属于用户的隐私信息,所以需要将openid签名处理,避免暴露。
- openid 也可以放在session 中,但是要为这个数据准备个钥匙发送给用户
签名能把能看懂的变成看不懂的
签名是可逆的,可以签名也可以还原
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)
# 重定向到主页
response = redirect(reverse('contents:index'))
# 登录时用户名写入到cookie,有效期15天
response.set_cookie('username', qq_user.username, max_age=3600 * 24 * 15)
return response
oauth_callback.html中渲染access_token
<input v-model="access_token" type="hidden" name="access_token" value="{{ access_token }}">
4. 补充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类型
# dumps 接收一个字典
# 返回值常称为token,返回一个二进制数据
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()
openid绑定用户实现
类似于用户注册的业务逻辑
- 当用户输入的手机号对应的用户已存在
- 直接将该已存在用户跟openid绑定
- 当用户输入的手机号对应的用户不存在
- 新建一个用户,并跟openid绑定
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)
# oauth_qq_user = OAuthQQUser(user=user,openid=openid)
# oauth_qq_user.save()
# create 封装了上面两个方法
except DatabaseError:
return render(request, 'oauth_callback.html', {'qq_login_errmsg': 'QQ登录失败'})
# 实现状态保持
login(request, user)
# 响应绑定结果,从哪来就回哪去
next = request.GET.get('state')
# next 为获取数据
response = redirect(next)
# 登录时用户名写入到cookie,有效期15天
response.set_cookie('username', user.username, max_age=3600 * 24 * 15)
return response
笔记
- 从现在开始只专注于后端逻辑的编写
- 在分析业务逻辑的过程中 不断的设计接口
- 后端校验参数只能比前端多,不能比前端少
- 后端接收参数,校验参数,返回结果,都是固定的套路,主逻辑根据具体的业务需求变化
- 后端校验不会写就用前端来改写
- 403 好似处理非法请求
- 表单提交的应该用html返回
- 账号或密码错误,当具体提示账号或密码错误时,会提示黑客具体是账号错了,还是密码错了
- 在前端渲染内容的时候,考虑渲染的位置,样式,数据
10.在测试的时候,在要测试函数的前面打一个断点 - debug过程中可以动态增加或删除断点
- 在程序分支处打断点,在进入的分支打断点
- 如果想看源代码,一定去看源代码
- 避免频繁发送短信验证码有前端和后端逻辑,添加标记,以及标记的使用,设置标志的有效期,在发短信的时候,根据此标记做判断
- 不能让Django程序执行耗时操作,要把那些耗时的业务逻辑解耦出去
- 自定义根本原因是系统提供的功能不符合实际的需求
- None进行布尔运算为false
self.login_url 有值就取,没有的话 取 settings.LOGIN_URL- 任何第三方平台都有开发文档,通过一定步骤进行连接
- token 口令
- 对于一些所有表都要用到的字段可以放到,基类当中,设计新的模型类继承这些字段就可以
一个子应用要不用注册,是看有没有迁移,或者是模板渲染,需要注册而未注册会报上面的错误- 可以先写出要做的事,写出代码来,根据报错写出代码来
- 常量一般封装在配置文件中
25.
后面在做的时候就没有这些错误了 - 查询参数在地址栏,我觉得应该是get请求
- 凡是网络操作一定要try,访问外部进程的要try
- python未进行异常处理程序会中止执行
- debug 可以在不确定的几个分支上都打上断点
- 面对陌生的逻辑可以先写最有把握的代码之后根据代码提示补全代码
- 对于想不起来的导入,可以先把要用的代码调用写上,之后智能导入
可以加载项目的环境变量,如果安装了IPython 也会启动IPython
通过quit 退出这个环境- 密码的加密是不可逆的
- 表单可以get方式提交,或者以post方式提交,get方式提交数据会放在地址栏,post方式提交参数放在请求体
- 标准库模块可以直接导入
- 序列化对象和反序列化对象的参数必须是一模一样的
- 对于user.objects.get 可能会抛出异常,对于可能抛出异常的代码,考虑使用try,并且也可通过此方法实现if-else
- 在日志中输出短信验证码可以不用查看手机,直接通过日志查看验证码