用户功能设计与实现--Django播客系统(六)

用户功能设计与实现–Django播客系统(六)

  • 提供用户注册处理
  • 提供用户登录处理
  • 提供路由配置

用户注册接口设计

  • 接受用户通过Post方法提交的注册信息,提交的数据是JSON格式数据
  • 检查email是否存在与数据库表中,如果存在返回错误状态码,例如4xx,如果不存在,将用户提交的数据存入表中
  • 整个过程都采用AJAX异步过程,用户提交JSON数据,服务端获取数据后处理,返回JSON。
POST /users/ 创建用户

请求体 application/json
{
    "password":"string",
    "name":"string",
    "email":"string"
}

响应
201 创建成功
400 请求数据错误

路由配置

  • 为了避免项目中的urls.py条目过多,也为了让应用自己管理路由,采用多级路由
# djweb/urls.py文件
from django.urls import include
urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r'^$',index),
    re_path(r'^index$',index),#不同路径可以指向同一个函数执行
    re_path(r'^users/',include("user.urls")),
]
  • include函数参数写应用.路由模块,该函数就会动态导入指定的包的模块,从模块里面读取urlpatterns,返回三元组。

  • url函数第二参数如果不是可调用对象,如果是元组或列表,则会从路径中除去已匹配的部分,将剩余部分与应用中的路由模块的urlpatterns进行匹配。

  • 新建user/uls.py文件

# user/uls.py
from django.conf.urls import re_path
from .views import reg

urlpatterns = [
    re_path(r'^$',reg), #/users/
]
  • user/views.py文件中添加reg方法
# user/views.py
from django.shortcuts import render
from django.http import HttpResponse,HttpRequest

def reg(request:HttpRequest):
    return HttpResponse("user.reg")
  • 浏览器中输入http://127.0.0.1:8000/users/测试(这是GET请求),可以看到响应的数据。下面开始完善视图函数。
  • user/views.py中编写视图函数reg

测试JSON数据

  • 使用POST方法,提交数据类型为application/json,json字符串要使用双引号

  • 这个数据是注册用的,由客户端提交。

    1. 数据提交模板为:
    {
        "password":"abc",
        "name":"xdd",
        "email":"[email protected]"
    }
    
    1. 可以使用Postman软件测试。
      django5_001

CSRF处理

  • 在Post数据的时候,发现出现了下面的提示
    django5_002

    1. 原因:默认Django CsrfViewMiddleware中间件会对所有POST方法提交的信息做CSRF校验
  • CSRF或XSRF(Cross-site Request Forgery),即跨站请求伪造。它也被称为:one click attack/session riding。是一种对网站的恶意利用。它伪造成来自受信任用户发起的请求,难以防范。

  • 原理:
    django5_003

    1. 用户登录某网站A完成登录认证,网站返回敏感信息的Cookie,即使是会话级的Cookie
    2. 用户没有关闭浏览器,或认证的Cookie一段时间内不过期还持久化了,用户就访问攻击网站B
    3. 攻击网站B看似一切正常,但是某些页面里面有一些隐藏运行的代码,或者诱骗用户操作的按钮等
    4. 这些代码一旦运行就是悄悄地向网站A发起特殊请求,由于网站A的Cookie还有效,且访问的是网站A,则其Cookie就可以一并发给网站A
    5. 网站A看到这些Cookie就只能认为是登录用户发起的合理合法请求,就会执行
  • CSRF解决

    1. 关闭CSRF中间件(不推荐)

      #djweb/settings.py
      MIDDLEWARE = [
          'django.middleware.security.SecurityMiddleware',
          'django.contrib.sessions.middleware.SessionMiddleware',
          'django.middleware.common.CommonMiddleware',
          # 'django.middleware.csrf.CsrfViewMiddleware', #注释掉
          'django.contrib.auth.middleware.AuthenticationMiddleware',
          'django.contrib.messages.middleware.MessageMiddleware',
          'django.middleware.clickjacking.XFrameOptionsMiddleware',
      ]
      
    2. csrftoken验证

      • 在表单POST提交时,需要发给服务器一个csrf_token
      • 模板中的表单Form中增加{% csrf_token %},它返回到了浏览器端就会为cookie增加csrftoken字段,还会在表单中增加一个名为csrfiddlewaretoken隐藏控件<input type='hidden' name='csrfmiddlewaretoken' value='jZTxU0v5mPoLvugcfLbS1B6vT8COYrKuxMzodWv8oNAr3a4ouWlb5AaYG2tQi3dD' />
      • POST提交表单数据时,需要将csrfmiddlewaretoken一并提交,Cookie中的csrf_token也一并会提交,最终在中间件中比较,相符通过,不相符就看到上面的403提示
    3. 双cookie验证

      • 访问本站先获得csrftoken的cookie
      • 如果使用AJAX进行POST,需要在每一次请求Header中增加自定义字段X-CSRFTOKEN,其值来自cookie中获取的csrftoken值
      • 在服务端比较cookie和X-CSRFTOKEN中的csrftoken,相符通过
  • 现在没有前端代码,为了测试方便,可以选择第一种方法先禁用中间件,测试完成后开启。

JSON数据处理

  • simplejson标准库方便好用,功能强大。
    1. pip install simplejson
  • 浏览器端提交的数据放在了请求对象的body中,需要使用simplejson解析,解析的方式同Json模块,但是simplejson更方便。

错误处理

  • Django中有很多异常类,定义在django.http下,这些类都继承自HttpResponse。
#user/views.py文件

from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse
import simplejson

def reg(request:HttpRequest):
    print(request.POST)
    print(request.body)
    # print("- " * 30)
    try:
        payload = simplejson.loads(request.body)
        email = payload['email']
        name = payload['name']
        password = payload["password"]
        print(email,name,password)
        return JsonResponse({},status=201) #创建成功返回201
    except Exception as e: #有任何异常,都返回
        print(e)
        return HttpResponseBadRequest() #这里返回实例,这不是异常类
  • 将上面代码增加邮箱检查、用户信息保存功能,就要用到Django的模型操作。
  • 本次采用Restful实践的设计,采用返回错误状态码+JSON错误信息方式。

注册代码 v1

# user/views.py文件

from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse
import simplejson
from .models import User

def reg(request:HttpRequest):

    try:
        payload = simplejson.loads(request.body)
        email = payload['email']
        query = User.objects.filter(email=email)
        print(query)
        print(query.query) #查看sQL语句
        if query.first():
            return JsonResponse({"error":"用户已存在"},status=400);

        name = payload['name']
        password = payload["password"]
        print(email,name,password)

        user = User()
        user.email = email
        user.name = name
        user.password = password
        user.save()

        return JsonResponse({},status=201) #创建成功返回201
    except Exception as e: #有任何异常,都返回
        print(e)
        # 失败返回错误信息和400,所有其他错误一律用户名密码错误
        return JsonResponse({"error":"用户名或密码错误"},status=400)
  1. 邮箱检查
    • 邮箱检查需要查user表,需要使用filter方法。
    • email=email,前面是字段名email,后面是email变量。查询后返回结果,如果查询有结果,则说明该email已经存在,邮箱已经注册,返回400到前端
  2. 用户信息存储
    • 创建User类实例,属性存储数据,最后调用save方法。Django默认是在save()、delete()的时候事务自动提交
    • 如果提交抛出任何错误,则捕获此异常做相应处理。
    • 如果没有异常,则返回201,不要返回任何用户信息。之后可能需要验证、用户登录等操作。
  3. 异常处理
    • 出现获取输入框提交信息异常,就返回400
    • 查询邮箱存在,返回400
    • save()方法保存数据,有异常,则返回400
    • 注意一点,Django的异常类继承自HttpResponse类,所以不能raise,只能return
    • 前端通过状态码判断是否成功
    • 由于采用Restful实战,所有异常全部返回JSON的错误信息,所以一律使用了JsonResponse

Django日志

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'level': "DEBUG",
        },
    },
}
  • 配置后,就可以在控制台看到执行的SQL语句。
  • 注意,必须DEBUG=True,同时level是DEBUG,否则从控制台看不到SQL语句。

模型操作

管理器对象

  • Django会为模型类提供一个objects对象,它是django.db.models.manager.Manager类型,用于与数据库交互。当定义模型类的时候没有指定管理器,则Django会为模型类提供一个objects的管理器。

  • 如果在模型类中手动指定管理器后,Django不再提供默认的objects的管理器了。

  • 管理器是Django的模型进行数据库查询操作的接口,Django应用的每个模型都至少拥有一个管理器。

  • Django ORM

    1. 数据的校验validation是在对象的Save、update方法上
      django5_004
    2. 对模型对象的CRUD,被Django ORM转换成相应的SQL语句操作不同的数据源。

查询

  • 查询集
    1. 查询会返回结果的集,它是django.db.models.query.QuerySet类型。
    2. 它是惰性求值,和sqlalchemy一样。结果就是查询的集。
    3. 它是可迭代对象。
  1. 惰性求值
    • 创建查询集不会带来任何数据库的访问,直到调用方法使用数据时,才会访问数据库。在迭代、序列化、if语句中都会立即求值。
  2. 缓存:
    • 每一个查询集都包含一个缓存,来最小化对数据库的访问。
    • 新建查询集,缓存为空。首次对查询集求值时,会发生数据库查询,Django会把查询的结果存在这个缓存中,并返回请求的结果,接下来对查询集求值将使用缓存的结果。
    • 观察下面的2个例子是要看真正生成的语句了
      1. 没有使用缓存,每次都要去查库,查了2次库

        [user.name for user in User.objects.all()]
        [user.name for user in User.objects.all()]
        
      2. 下面的语句使用缓存,因为使用同一个结果集

        qs = User.objects.all()
        [user.name for user in qs]
        [user.name for user in qs]
        

限制查询集(切片)

  • 查询集对象可以直接使用索引下标的方式(不支持负索引),相当于SQL语句中的limit和offset子句。
  • 注意使用索引返回的新的结果集,依然是惰性求值,不会立即查询。
qs = User.objects.all()[20:40]
# LIMIT 20 OFFSET 20
qs = User.objects.all()[20:30]
# LIMIT 10 OFFSET 20

过滤器

  1. 返回查询集的方法,称为过滤器,如下:
名称 说明
all()
filter() 过滤,返回满足条件的数据
exclude() 排除,排除满足条件的数据
order_by() 排序,注意参数是字符串
values() 返回一个对象字典的列表,列表的元素是字典,字典内是字段和值的键值对
  • filter(k1=v1).filter(k2=v2)等价于filter(k1=v1,k2=v2)
  • filter(pk=10)这里pk指的就是主键,不用关心主键字段名,当然也可以使用主键名filter(emp_no=10)
mgr = User.objects
print(mgr.all())
print(mgr.values())
print(mgr.filter(pk=1).values())
print(mgr.exclude(pk=4))
print(mgr.exclude(id=1).values())
print(mgr.exclude(id=6).order_by("-id"))
print(mgr.exclude(id=6).order_by("-id").values())
print(mgr.exclude(id=6).values().order_by("-pk"))
  • 返回单个值的方法
名称 说明
get() 仅返回单个满足条件的对象
如果未能返回对象则抛出DoesNotExist异常;如果能返回多条,抛出MultipleObjectsReturned异常
count() 返回当前查询的总条数
first() 返回第一个对象
last() 返回最后一个对象
exit() 判断查询集中是否有数据,如果有则返回True
email = "[email protected]"
mgr = User.objects
print(mgr.get(pk=4)) #使用主键查询
print(mgr.get(email=email)) #只能返回一个结果
print(mgr.filter(id=1).get())

print(mgr.first()) #使用limit 1查询,返回实例或None
print(mgr.filter(pk=4,email=email).first()) # and条件

字段查询(Field Lookup)表达式

  • 字段查询表达式可以作为filter(),exclude(),get()的参数,实现where子句。
  • 语法:属性名称__比较运算符=值
  • 注意:属性名和运算符之间使用双下划线
  • 比较运算符如下
名称 举例 说明
exact filter(isdeleted=False)
filter(isdeleted__exact=False)
严格等于,可省略不写
contains exclude(title__contains="天") 是否包含,大小写敏感,等价于like '%天%'
statswith
endswith
filter(title__startswith='天') 以什么开头或结尾,大小写敏感
isnull
isnotnull
filter(title__isnull=False) 是否为null
iexact
icontains
istartswith
iendswith
i的意思是忽略大小写
in filter(pk__in=[1,2,3,100]) 是否在指定范围数据中
gt,gte,lt,lte filter(id__get=3)
filter(pk__lte=6)
filter(pub_date__get=date(2000,1,1))
大于,小于等
year、month、day
week_day
hour、minute
second
filter(pub_date__year=2000) 对日期类型属性处理
mgr = User.objects
print(mgr.filter(id__exact=4))
print(mgr.filter(email__contains='xdd'))
print(mgr.filter(email__istartswith='xdda'))
print(mgr.filter(id__in=[1,4]))
print(mgr.filter(id__gt=3))

Q对象

  • 虽然Django提供传入条件的方式,但是不方便,它还提供了Q对象来解决。
  • Q对象是django.db.models.Q,可以使用&、|操作符来组成逻辑表达式。~表示not。
from django.db.models import Q
mgr = User.objects
print(mgr.filter(Q(pk__lt=6))) #不如直接写filter(pk__lt=6)

print(mgr.filter(pk__lt=6).filter(pk__gt=1)) #与
print(mgr.filter(Q(pk__lt=6) & Q(pk__gt=1))) #与
print(mgr.filter(Q(pk=4) | Q(pk=1))) #或
print(mgr.filter(~Q(pk__lt=6))) #非
  • 可使用&|和Q对象来构造复杂的逻辑表达式
  • 过滤器函数可以使用一个或多个Q对象
  • 如果混用关键字参数和Q对象,那么Q对象必须位于关键字参数的面前。所有参数都将and和一起

新增、更新、删除方法

  • 更新数据
user = User(email='test3',name='test3') #没有主键
user.save() #会新建一个数据

user = User(id=100,email='test4',name='test4') #有自增主键,如果不存在,则是插入
user.save()
user = User(id=100,email='test4',name='test4') # 有自增主键,如果存在,则是更新
user.save()
  • update在查询集上同时更新数据
# 更新所有查询的结果
User.objects.filter(id__get=4).update(password='xyz') #将pk大于4的查询结果更新,所有用户的密码修改
  • delete删除查询集数据
ret = User.objects.filter(id__gt=4).delete()
print(ret)

# 运行结果
# DELETE FROM `user` WHERE `user`.`id` > 4 ; args=(4,)
# (3,{"user.User":3})

注册接口设计完善

  • 认证:HTTP协议是无状态协议,为了解决它产生了cookie和session技术。

  • 传统的session-cookie机制

    1. 浏览器发起第一次请求到服务器,服务器发现浏览器没有提供session id,就认为这是第一次请求,会返回一个新的session id给浏览器,浏览器只要不关闭,这个session id就会随着每一次请求重新发给服务端,服务器端查找这个session id,如果查到,就认为是同一个会话。如果没有查到,就认为是新的请求。
    2. session是会话级的,服务端还可以在这个会话session中创建很多数据session键值对。
    3. 这个session id有过期的机制,一段时间如果没有发起请求,认为用户已经断开,服务端就清除本次会话所有session。浏览器端也会清除相应的cookie信息。
    4. 服务端保存着大量session信息,很消耗服务器内存,而且如果多服务器部署,可以考虑session复制集群,也可以考虑session共享的问题,比如redis、memcached等方案。
  • 无ssession方案

    1. 既然服务端就是需要一个ID来表示身份,那么不使用session也可以创建一个ID返回给客户端。但是,要保证客户端不可篡改该信息。
    2. 服务端生成一个标识,并使用某种算法对标识签名。
    3. 服务端收到客户端发来的标识,需要检查签名。
    4. 这种方案的缺点是,加密、解密需要消耗CPU计算资源,无法让浏览器自己主动检查过期的数据以清除。这种技术称作JWT(json WEB Token)。
  • JWT

    1. JWT(Json WEB Token)是一种采用Json方式安装传输信息的方式。本次使用PyJWT,它是Python对JWT的实现。
    2. 安装pip install pyjwt
    3. jwt原理
    import jwt
    import base64
    
    key = "secret"
    token = jwt.encode({'payload':'abc123'},key,'HS256')
    print(token)
    print(jwt.decode(token,key,algorithms=["HS256"]))
    # b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXlsb2FkIjoiYWJjMTIzIn0.lZc1PBGdbYDKq9k43tNTt1f0MHy4DjwT8NGTnHEIaVE'
    # token分为3部分,用.断开
    
    header,payload,signature = token.split(b'.')
    print("- "*30)
    print(header,payload,signature,sep='\n')
    print("- "*30)
    
    def addeq(b:bytes):
        """ 为base64编码补齐等号"""
        rest = 4 - len(b) % 4
        return b + b'='* rest
    
    print("header=",base64.urlsafe_b64decode(addeq(header)))
    print("payload=",base64.urlsafe_b64decode(addeq(payload)))
    print("signature=",base64.urlsafe_b64decode(addeq(signature)))
    print(" ="*30)
    
    # 根据jwt算法,重新生成签名
    # 1 获取算法对象
    from jwt import algorithms
    alg = algorithms.get_default_algorithms()["HS256"]
    newkey = alg.prepare_key(key) # key 为secret
    
    # 2获取前两部分 header.payload
    signing_input,_,_ = token.rpartition(b".")
    print(signing_input)
    
    
    # 3使用key签名
    signature = alg.sign(signing_input,newkey)
    print("--------------")
    print(signature)
    print(base64.urlsafe_b64encode(signature))
    

    django5_005

    1. 由此,可知jwt生成的token分为三部分
      1. header,由数据类型、加密算法构成
      2. payload,负载就是要传输的数据,一般来说放入python对象即可,会被json序列化的
      3. signature,签名部分。是签名2部分数据分别base64编码后使用点号链接后,加密算法使用key计算好一个结果,再被base64编码,得到签名
      • 所有数据都是明文传输的,只是做了base64,如果是敏感信息,请不要使用jwt。
      • 数据签名的目的不是为了隐藏数据,而是保证数据不被篡改。如果数据篡改了,发回到服务器端,服务器使用自己的key再计算一遍,然后进行签名比对,一定对不上签名。
    2. jwt使用场景
      • 认证:这是jwt最常用的场景,一旦用户登录成功,就会得到wt,然后请求中就可以带上这个jwt。服务器中jwt验证通过,就可以被允许访问资源。甚至可以在不同域名中传递,在单点登录(Single Sign On)中应用广泛。
      • 数据交换:jwt可以防止数据被篡改,它还可以使用公钥、私钥加密,确保请求的发送者是可信的
  • 密码

    1. 使用邮箱+密码方式登录。
    2. 邮箱要求唯一就行了,但是,密码存储需要加密。早期,都是用明文的密码存储。后来,使用MD5存储,但是,目前也不安全,网上有很多MD5的网站,使用反查方式找到密码。
    3. 加盐,使用hash(password+salt)的结果存入数据库中,就算拿到数据库的密码反查,也没有用了。如果是固定加盐,还是容易被找到规律,或者从源码中泄露。随机加盐,每一次盐都变,就增加了破解的难度。
    4. 暴力破解:什么密码都不能保证不被暴力破解,例如穷举。所以要使用慢hash算法,例如bcrypt,就会让每一次计算都很慢,都是秒即的,这样穷举的时间就会很长,为了一个密码破解的时间在当前CPU或者GPU的计算能力下可能需要几十年以上。
  • bcrypt

    1. 安装pip install bcrypt
    import bcrypt
    import datetime
    
    password = b'12345xdd'
    
    # 每次拿到盐都不一样
    print(1,bcrypt.gensalt())
    print(2,bcrypt.gensalt())
    
    salt = bcrypt.gensalt()
    # 拿到的盐不同,计算等到的密文相同
    print("===============same salt ===============")
    x = bcrypt.hashpw(password,salt)
    y = bcrypt.hashpw(password,salt)
    print(3,x)
    print(4,y)
    
    # 每次拿到的盐不同,计算生成的密文也不一样
    print("============different salt===============")
    xx = bcrypt.hashpw(password,bcrypt.gensalt())
    yy = bcrypt.hashpw(password,bcrypt.gensalt())
    print(5,x)
    print(6,y)
    
    # 校验
    print(bcrypt.checkpw(password,xx),len(xx))
    print(bcrypt.checkpw(password+b' ',xx),len(xx))
    
    # 计算时长
    start = datetime.datetime.now()
    y3 = bcrypt.hashpw(password,bcrypt.gensalt())
    delta = (datetime.datetime.now() - start).total_seconds()
    print(10,'duration={}'.format(delta))
    
    # 检验时长
    start = datetime.datetime.now()
    y4 = bcrypt.checkpw(password,xx)
    delta = (datetime.datetime.now() - start).total_seconds()
    print(y4)
    print(11,'duration={}'.format(delta))
    
    start = datetime.datetime.now()
    y5 = bcrypt.checkpw(b'1',xx)
    delta = (datetime.datetime.now() - start).total_seconds()
    print(y5)
    print(12,'duration={}'.format(delta))
    

    django5_006

    1. 从耗时看出,bcrypt加密、验证非常耗时,所有如果穷举,非常耗时。而且碰巧攻破一个密码,由于盐不一样,还等穷举另一个。
    salt=b'$2b$12$jwBD7mg9stvIPydF2bqoPO'
    b'$2b$12$jwBD7mg9stvIPydF2bqoPOodPwWYVvdmZb5uWWuWvlf9iHqNlKSQO'
    
    $是分隔符
    $2b$,加密算法
    12,表示2^12 key expansion rounds
    这是盐b'jwBD7mg9stvIPydF2bqoPO',22个字符,Base64
    这是密文b'odPwWYVvdmZb5uWWuWvlf9iHqNlKSQO',31个字符,Base64
    

注册代码 v2

  1. 全局变量

    • 项目的settings.py文件实际上就是全局变量的配置 文件。
    • SECRET_KEY一个强KEY
    from django.conf import settings
    print(settings.SECRET_KEY)
    
  • 使用jwt和bcrypt,修改注册代码
from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse
import simplejson
from .models import User
import jwt
import datetime
import bcrypt
from django.conf import settings

# 对id签名
def gen_token(user_id):
    # 时间戳用来判断是否过期,以便重发token或重新登录
    return  jwt.encode({
        "user_id":user_id,
        "timestamp":int(datetime.datetime.now().timestamp()) #取整
    },settings.SECRET_KEY).decode()

def reg(request:HttpRequest):

    try:
        payload = simplejson.loads(request.body)
        email = payload['email']
        query = User.objects.filter(email=email)
        print(query)
        print(query.query) #查看sQL语句
        if query.first():
            return JsonResponse({"error":"用户已存在"},status=400);

        name = payload['name']
        password = payload["password"].encode()
        print(email,name,password)

        # 密码加密
        password = bcrypt.hashpw(password,bcrypt.gensalt()).decode()
        print(password)

        user = User()
        user.email = email
        user.name = name
        user.password = password
        user.save()

        return JsonResponse({"token":gen_token(user.id)},status=201) #创建成功返回201
    except Exception as e: #有任何异常,都返回
        print(e)
        # 失败返回错误信息和400,所有其他错误一律用户名密码错误
        return JsonResponse({"error":"用户名或密码错误"},status=400)
  • save方法会自动提交事务

django5_007

  • 数据库中内容
    django5_008
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章