美多商城项目之用户登录:账号登录、QQ登录

一、账号登录

1.1 用户名登录 

1. 用户名登录逻辑分析

2. 用户名登录接口设计

1.请求方式

选项方案
请求方法POST
请求地址/login/

2.请求参数:表单

参数名类型是否必传说明
usernamestring用户名
passwordstring密码
rememberedstring是否记住用户

3.响应结果:JSON

字段说明
登录失败响应错误提示
登录成功重定向到首页

3. 用户名登录接口定义

class LoginView(View):
    """用户名登录"""

    def post(self, request):
        """
        实现登录逻辑
        :param request: 请求对象
        :return: 登录结果
        """
        pass

4. 用户名登录后端逻辑

import json
from django.contrib.auth import authenticate,login
class LoginView(View):

    def post(self,request):
        # 1.接收参数
        dict = json.loads(request.body.decode())
        username = dict.get('username')
        password = dict.get('password')
        remembered = dict.get('remembered')

        # 2.校验(整体 + 单个)
        if not all([username, password]):
            return JsonResponse({'code': 400,
                                 'errmsg': '缺少必传参数'})

        # 3.验证是否能够登录
        user = authenticate(username=username,
                            password=password)

        # 判断是否为空,如果为空,返回
        if user is None:
            return JsonResponse({'code': 400,
                                 'errmsg': '用户名或者密码错误'})

        # 4.状态保持
        login(request, user)

        # 5.判断是否记住用户
        if remembered != True:
            # 7.如果没有记住: 关闭立刻失效
            request.session.set_expiry(0)
        else:
            # 6.如果记住:  设置为两周有效
            request.session.set_expiry(None)

        # 8.返回json
        return JsonResponse({'code': 0,
                             'errmsg': 'ok'})

1.2 多账号登录

  • Django自带的用户认证后端默认是使用用户名实现用户认证的。

1. 根据登录数据修改用户认证字段

import re
if re.match('^1[3-9]\d{9}$', username):
    # 手机号
    User.USERNAME_FIELD = 'mobile'
else:
    # account 是用户名
    # 根据用户名从数据库获取 user 对象返回.
    User.USERNAME_FIELD = 'username'

 1.3 首页用户名展示

 

1. 首页用户名展示方案

方案一

  • 发送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

2. 用户名写入到cookie

response = JsonResponse({'code': 0,
                             'errmsg': 'ok'})
# 注册时用户名写入到cookie,有效期15天
response.set_cookie('username', user.username, max_age=3600 * 24 * 15)

return response

1.4 退出登录

1. 接口设计和定义

1.请求方式

选项方案
请求方法DELETE
请求地址/logout/

2.请求参数

3.响应结果:JSON

字段说明
code状态码
errmsg错误信息

2. logout()方法介绍

  1. 退出登录:

    • 回顾登录:将通过认证的用户的唯一标识信息,写入到当前session会话中
    • 退出登录:正好和登录相反(清理session会话信息)
  2. logout()方法:

    • Django用户认证系统提供了logout()方法
    • 封装了清理session的操作,帮助我们快速实现登出一个用户
  3. logout()位置:

    • django.contrib.auth.__init__.py文件中
logout(request)

3. logout()方法使用

from django.contrib.auth import logout

class LogoutView(View):
    """退出登录"""

    def delete(self, request):
        """实现退出登录逻辑"""
        # 清理session
        logout(request)
        # 退出登录,重定向到登录页
        response = JsonResponse({'code':0,
                                 'errmsg':'ok'})
        # 退出登录时清除cookie中的username
        response.delete_cookie('username')

        return response

提示:

  • 由于首页中用户名是从cookie中读取的。
  • 所以退出登录时,需要将cookie中用户名清除。

 1.5 判断用户是否登录

1.LoginRequiredMixin判断用户是否登录

Django用户认证系统提供了方法

  • request.user.is_authenticated来判断用户是否登录。如果通过登录验证则返回True。反之,返回False
  • LoginRequiredMixin封装了判断用户是否登录的操作。
from django.contrib.auth.mixins import LoginRequiredMixin

class UserInfoView(LoginRequiredMixin, View):
    """用户中心"""

    def get(self, request):
        """提供个人信息界面"""
        return http.JsonResponse({
            'code': 0, 
            'errmsg': '个人中心',
             "info_data":{
                    "username":"itcast",
                    "mobile": "18310820688",
                    "email": "",
                    "email_active": 'true'
                }
            })

2. 判断用户是否登录并返回JSON

重要提示:

  • 只有用户登录时才能让其绑定邮箱。
  • 此时前后端交互的数据类型是JSON,所以需要判断用户是否登录并返回JSON给用户。

实现方案:自定义LoginRequiredJSONMixin扩展类

  • 新建meiduo_mall/utils/views.py文件
from django.contrib.auth.mixins import LoginRequiredMixin
from django import http

class LoginRequiredJSONMixin(LoginRequiredMixin):
    """Verify that the current user is authenticated."""

    def handle_no_permission(self):
        return http.JsonResponse({'code': 400, 'errmsg': '用户未登录'})
handle_no_permission说明:我们只需要改写父类中的处理方式 至于如何判断用户是否登录 在父类中已经判断了

LoginRequiredJSONMixin的使用

from meiduo_mall.utils.views import LoginRequiredJSONMixin

class UserInfoView(LoginRequiredJSONMixin, View):
    """添加邮箱"""

    def put(self, request):
        """实现添加邮箱逻辑"""
        # 判断用户是否登录并返回JSON
        pass

二、QQ登录 

2.1 QQ登录开发文档

QQ登录:即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。

1. QQ互联开发者申请步骤

若想实现QQ登录,需要成为QQ互联的开发者,审核通过才可实现。

2. QQ互联应用申请步骤

成为QQ互联开发者后,还需创建应用,即获取本项目对应与QQ互联的应用ID。

3. 网站对接QQ登录步骤

QQ互联提供有开发文档,帮助开发者实现QQ登录。

4. QQ登录流程分析

2.1 定义QQ登录模型类

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的表

上面代码中相关参数解读:

auto_now_add:创建或添加对象时自动添加时间, 修改或更新对象时, 不会更改时间

auto_now:凡是对对象进行操作(创建/添加/修改/更新),时间都会随之改变

abstract:声明该模型类仅继承使用,数据库迁移时不会创建 BaseModel 的表

2. 定义QQ登录模型类

创建一个新的应用oauth,用来实现QQ第三方认证登录。

 

设置路由

path('', include('apps.oauth.urls')),
from django.conf.urls import url
from . import views
urlpatterns = [

]

oauth/models.py中定义QQ身份(openid)与用户模型类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', 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

 

 2.3 QQ登录工具QQLoginTool

1. QQLoginTool介绍

  • 该工具封装了对接QQ互联的请求操作。
  • 可用于快速实现QQ登录的一种工具包。

2. QQLoginTool安装

pip install QQLoginTool

3. 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)

2.4 OAuth2.0认证获取openid

待处理业务逻辑

# 提取code请求参数
# 使用code向QQ服务器请求access_token
# 使用access_token向QQ服务器请求openid
# 使用openid查询该QQ用户是否在美多商城中绑定过用户
# 如果openid已绑定美多商城用户,直接生成JWT token,并返回
# 如果openid没绑定美多商城用户,创建用户并绑定到openid

1. 获取QQ登录扫码页面

1.请求方式

选项方案
请求方法GET
请求地址/qq/authorization/

2.请求参数:查询参数

参数名类型是否必传说明
nextstring用于记录QQ登录成功后进入的网址

3.响应结果:JSON

字段说明
code状态码
errmsg错误信息
login_urlQQ登录扫码页面链接

4.后端逻辑实现

from django import http
from django.conf import settings
from django.views import View
from QQLoginTool.QQtool import OAuthQQ
import logging
logger = logging.getLogger('django')

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': 0,
                                  'errmsg': 'OK',
                                  'login_url':login_url})

5.QQ登录参数

# QQ登录参数
# 我们申请的 客户端id
QQ_CLIENT_ID = '101474184'
# 我们申请的 客户端秘钥
QQ_CLIENT_SECRET = 'c6ce949e04e12ecc909ae6a8b09b637c'
# 我们申请时添加的: 登录成功后回调的路径
QQ_REDIRECT_URI = 'http://www.meiduo.site:8080/oauth_callback.html'

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
path('oauth_callback/', views.QQAuthUserView.as_view()),
class QQAuthUserView(View):
    """用户扫码登录的回调处理"""

    def get(self, request):
        """Oauth2.0认证"""
        # 接收Authorization Code
        code = request.GET.get('code')
        if not code:
            return http.HttpResponseBadRequest('缺少code')
        pass

3. OAuth2.0认证获取openid

  1. 使用code向QQ服务器请求access_token
  2. 使用access_token向QQ服务器请求openid
class QQAuthUserView(View):
    """用户扫码登录的回调处理"""

    def get(self, request):
        """Oauth2.0认证"""
        # 提取code请求参数
        code = request.GET.get('code')
        if not code:
            return http.HttpResponseBadRequest('缺少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)
            http.JsonResponse({'code': 400, 'errmsg': 'oauth2.0认证失败, 即获取qq信息失败'})
        pass

 2.5 openid是否绑定用户的处理

1. 判断openid是否绑定过用户

使用openid查询该QQ用户是否在美多商城中绑定过用户。

try:
    oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
    # 如果openid没绑定美多商城用户
    pass
else:
    # 如果openid已绑定美多商城用户
    pass

2. openid已绑定用户的处理

如果openid已绑定美多商城用户,直接生成状态保持信息,登录成功,并重定向到首页。

 try:
    # 查看是否有 openid 对应的用户
    oauth_qq = OAuthQQUser.objects.get(openid=openid)

except OAuthQQUser.DoesNotExist:
    # 如果 openid 没绑定美多商城用户
    # 请查看:  openid 未绑定用户的处理
    pass
else:
    # 如果 openid 已绑定美多商城用户
    # 根据 user 外键, 获取对应的 QQ 用户(user)
    user = oauth_qq.user

    # 实现状态保持
    login(request, user)

    # 创建重定向到主页的对象
    response = http.JsonResponse({'code': 0, 'errmsg': 'ok'})

    # 将用户信息写入到 cookie 中,有效期14天
    response.set_cookie('username', user.username, max_age=3600 * 24 * 14)

    # 返回响应
    return response

3. openid未绑定用户的处理

  • 为了能够在后续的绑定用户操作中前端可以使用openid,在这里将openid签名后响应给前端。
  • openid属于用户的隐私信息,所以需要将openid签名处理,避免暴露。
try:
    # 查看是否有 openid 对应的用户
    oauth_qq = OAuthQQUser.objects.get(openid=openid)

except OAuthQQUser.DoesNotExist:
    # 如果 openid 没绑定美多商城用户,进入这里: 
    # 使用加密类加密 openid
    access_token = generate_access_token({'openid':openid})
    # 注意: 这里一定不能返回 0 的状态码. 否则不能进行绑定页面
    return http.JsonResponse({'code':300,'errmsg':'ok','access_token':access_token})
else:
    ...

4. 补充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
from itsdangerous import BadData
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings


def generate_access_token(openid):
    """
    签名openid
    :param openid: 用户的openid
    :return: access_token
    """
    serializer = Serializer(settings.SECRET_KEY, expires_in=3600)
    data = {'openid': openid}
    token = serializer.dumps(data)
    return token.decode()

def check_access_token(access_token):
    """
    提取openid
    :param access_token: 签名后的openid
    :return: openid or None
    """
    serializer = Serializer(settings.SECRET_KEY, expires_in=3600)
    try:
        data = serializer.loads(access_token)
    except BadData:
        return None
    else:
        return data.get('openid')

2.6 openid绑定用户实现

类似于用户注册的业务逻辑

  • 当用户输入的手机号对应的用户已存在
    • 直接将该已存在用户跟openid绑定
  • 当用户输入的手机号对应的用户不存在
    • 新建一个用户,并跟openid绑定
from apps.oauth.models import OAuthQQUser
from django.contrib.auth import login
import json
import re
from apps.users.models import User
from django.db import DatabaseError
from django_redis import get_redis_connection
from .utils import check_access_token
class QQUserView(View):
    """用户扫码登录的回调处理"""

    def get(self, request):
        """Oauth2.0认证"""
        pass

    def post(self, request):
        """美多商城用户绑定到openid"""

        # 1.接收参数
        data_dict = json.loads(request.body.decode())
        mobile = data_dict.get('mobile')
        password = data_dict.get('password')
        sms_code_client = data_dict.get('sms_code')
        access_token = data_dict.get('access_token')

        # 2.校验参数
        # 判断参数是否齐全
        if not all([mobile, password, sms_code_client]):
            return http.JsonResponse({'code': 400,
                                      'errmsg': '缺少必传参数'})

        # 判断手机号是否合法
        if not re.match(r'^1[3-9]\d{9}$', mobile):
            return http.JsonResponse({'code': 400,
                                      'errmsg': '请输入正确的手机号码'})

        # 判断密码是否合格
        if not re.match(r'^[0-9A-Za-z]{8,20}$', password):
            return http.JsonResponse({'code': 400,
                                      'errmsg': '请输入8-20位的密码'})

        # 3.判断短信验证码是否一致
        # 创建 redis 链接对象:
        redis_conn = get_redis_connection('code')

        # 从 redis 中获取 sms_code 值:
        sms_code_server = redis_conn.get('sms_%s' % mobile)

        # 判断获取出来的有没有:
        if sms_code_server is None:
            # 如果没有, 直接返回:
            return http.JsonResponse({'code': 400,
                                      'errmsg': '验证码失效'})
        # 如果有, 则进行判断:
        if sms_code_client != sms_code_server.decode():
            # 如果不匹配, 则直接返回:
            return http.JsonResponse({'code': 400,
                                      'errmsg': '输入的验证码有误'})

            # 调用我们自定义的函数, 检验传入的 access_token 是否正确:
        # 错误提示放在 sms_code_errmsg 位置
        openid = check_access_token(access_token)
        if not openid:
            return http.JsonResponse({'code': 400,
                                      'errmsg': '缺少openid'})
        # 4.保存注册数据
        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 http.JsonResponse({'code': 400,
                                          'errmsg': '输入的密码不正确'})
        # 5.将用户绑定 openid
        try:
            OAuthQQUser.objects.create(openid=openid,
                                       user=user)
        except DatabaseError:
            return http.JsonResponse({'code': 400,
                                      'errmsg': '往数据库添加数据出错'})
        # 6.实现状态保持
        login(request, user)

        # 7.创建响应对象:
        response = http.JsonResponse({'code': 0,
                                      'errmsg': 'ok'})

        # 8.登录时用户名写入到 cookie,有效期14天
        response.set_cookie('username',
                            user.username,
                            max_age=3600 * 24 * 14)

        # 9.响应
        return response

补充:openid签名处理

  • oauth.utils.py
from itsdangerous import BadData
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
from apps.oauth import constants

def generate_access_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()

def check_access_token(access_token):
    """
    提取openid
    :param access_token: 签名后的openid
    :return: openid or None
    """
    serializer = Serializer(settings.SECRET_KEY, expires_in=constants.ACCESS_TOKEN_EXPIRES)
    try:
        data = serializer.loads(access_token)
    except BadData:
        return None
    else:
        return data.get('openid')
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值