博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
博客项目之用户功能设计与实现(JWT、bcrypt、CSRF)
阅读量:4132 次
发布时间:2019-05-25

本文共 10054 字,大约阅读时间需要 33 分钟。

1. 用户功能设计与实现

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

1.1 用户注册接口设计

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

路由设置:为了避免项目中的urls.py条目过多,也为了让应用自己管理路由,采用多级路由

urlpatterns = [    # 下面三个有一个即可    url(r'^admin/', admin.site.urls),  # url在 2.x版本 re_path    url(r'^$', index),    url(r'^index$', index),    url(r'^users/', include('user.urls')),  # 多级路由,查看include的原码    url(r'^posts/', include('post.urls'))]

include函数参数写应用.路由模块 ,该函数就会动态导入指定的包的模块,从模块里面读取urlpatterns,返回三元

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

中的路由模块的urlpatterns进行匹配。

# user表中新建urls.pyfrom django.conf.urls import urlfrom .views import reg, test, login, logout, test_send_email# 在views中编写视图函数reg、test、logoin、logout、test_send_emailurlpatterns = [    # 下面三个有一个即可    url(r'^$', reg),    url(r'^test$', test),    url(r'^logout$', logout),    url(r'^login$', login),    url(r'^mail$', test_send_email),    ]

CSRF处理:

CSRFXSRFCross-site Request Forgery),即跨站请求伪造。它也被称为:one click attack/session riding,是一种对网站的恶意利用。它伪装成来自受信任用户发起请求,难以防范。

 

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

那么该怎么解决CSRF呢?

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

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. 表单POST

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

3. Ajax POST如果使用AJAX进行POST,需要在每一次请求Header中增加自定义字段X-CSRFTOKEN,其值来自cookie 中获取的csrftoken

JSON数据处理:simplejson 比标准库方便好用,功能强大。$ pip install simplejson

浏览器端提交的数据放在了请求对象的body中,需要使用simplejson解析,解析的方式同json模块,但是simplejson更方便。

注册代码:

# 用户注册@require_POSTdef reg(request: HttpRequest):    # print(request.GET)    # print(request.POST)    # print(request.content_type)    # print(request.body)    # print(simplejson.loads(request.body))    try:        payload = simplejson.loads(request.body)        email = payload['email']        # 先查询email是否已经存在        u = User.objects.filter(email=email).first()  # objects为默认的管理器        if u:            return JsonResponse({'error': "用户名已存在"}, status=400)        name = payload['name']        password = payload['password'].encode()        print(email, name, password)        # 写入数据库        user = User()        user.email = email        user.name = name        user.password = bcrypt.hashpw(password, bcrypt.gensalt()).decode()        print(user)  # 因为没有提交到数据库,所以数据库中并没有此条记录,所以其id为None        # try:        #     user.save()  # save会自动事务提交,当然提交失败,会自动回滚;此处不需要处理异常了,因为如果此处出现异常,会直接执行下文的except        #        # except:        #     logging.info(e)        #     raise        user.save()  # 自动提交        print(user)        return JsonResponse({'a': 1000}, status=201)  # 如果正常,返回json数据    except Exception as e:        logging.info(e)        # return HttpResponseBadRequest()  # 这里返回实例,这不是异常类,不能raise,python中非异常类的子类是不能raise的        return JsonResponse({'error': "用户名已存在"}, status=400)

邮箱检查:邮箱检查需要查user表,需要使用filter方法。email=email,前面是字段名email,后面是email变量。查询后返回结果,如果查询有结果,则说明该email已经存在,邮箱已经注册,返回400到前端。

用户信息存储:创建User类实例,属性存储数据,最后调用save方法。Django默认是在save()delete()的时候事务自动提交如果提交抛出任何错误,则捕获此异常做相应处理。如果没有异常,则返回201,不要返回任何用户信息。之后可能需要邮箱验证、用户登录等操作。

异常处理:

  • 出现获取输入框提交信息异常,就返回400
  • 查询邮箱存在,返回400
  • save()方法保存数据,有异常,则返回400
  • 注意一点,Django的异常类继承自HttpResponse类,所以不能raise,只能return
  • 前端通过状态码判断是否成功
  • 由于采用Restful实践,所有异常全部返回JSON的错误信息,所以一律使用了JsonResponse

Django日志:Django的日志配置在settings.py中。

# loggingLOGGING = {    'version': 1,    'disable_existing_loggers': False,    'handlers': {        'console': {            'class': 'logging.StreamHandler',        },    },    'loggers': {        'django': {            'handlers': ['console'],            'level': 'DEBUG',        },    },}

配置后,就可以在控制台看到执行的SQL语句。注意,必须DEBUG=True,同时level是DEBUG,否则从控制台看不到SQL语句。

1.2模型操作

1.2.1管理器对象

Django会为模型类提供一个objects对象,它是django.db.models.manager.Manager类型,用于与数据库交互。当定义模型类的时候没有指定管理器,则Django会为模型类提供一个objects的管理器。 如果在模型类中手动指定管理器后,Django不再提供默认的objects的管理器了。 管理器是Django的模型进行数据库查询操作的接口,Django应用的每个模型都至少拥有一个管理器。

1.2.2查询

查询集:查询会返回结果的集,它是django.db.models.query.QuerySet类型。它是惰性求值,和sqlalchemy一样。结果就是查询的集。 它是可迭代对象。

  • 惰性求值:创建查询集不会带来任何数据库的访问,直到调用方法使用数据时,才会访问数据库。在迭代、序列化、if语句中 都会立即求值。
  • 缓存:每一个查询集都包含一个缓存,来最小化对数据库的访问。 新建查询集,缓存为空。首次对查询集求值时,会发生数据库查询,Django会把查询的结果存在这个缓存中,并返回请求的结果,接下来对查询集求值将使用缓存的结果。

限制查询集(切片):查询集对象可以直接使用索引下标的方式(不支持负索引),相当于SQL语句中的limitoffffset子句。 注意使用索引返回的新的结果集,依然是惰性求值,不会立即查询。

1.2.3 过滤器

filter(k1=v1).filter(k2=v2) 等价于 filter(k1=v1, k2=v2)

filter(pk=10) 这里pk指的就是主键, 不用关心主键字段名,当然也可以使用使用主键名 filter(emp_no=10)

返回单个的值:

1.2.4 字段查询表达式

字段查询表达式可以作为fifilter()exclude()get()的参数,实现where子句

语法: 属性名称__比较运算符=

注意:属性名和运算符之间使用双下划线

1.2.5 Q对象

虽然Django提供传入条件的方式,但是不方便,它还提供了Q对象来解决。Q对象是django.db.models.Q,可以使用&|操作符来组成逻辑表达式。 ~ 表示not

可使用&|Q对象来构造复杂的逻辑表达式,过滤器函数可以使用一个或多个Q对象 ,如果混用关键字参数和Q对象,那么Q对象必须位于关键字参数的前面。所有参数都将and在一起。

1.2.6 新增、更新、删除方法

更新数据:

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__gt=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})

2.注册接口的设计完善

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

传统的session-cookie机制:

浏览器发起第一次请求到服务器,服务器发现浏览器没有提供session id,就认为这是第一次请求,会返回一个新的session id给浏览器端。浏览器只要不关闭,这个session id就会随着每一次请求重新发给服务器端,服务器端查找这个session id,如果查到,就认为是同一个会话。如果没有查到,就认为是新的请求。

session是会话级的,服务器端还可以在这个会话session中创建很多数据session键值对。 这个session id有过期的机制,一段时间如果没有发起请求,认为用户已经断开,服务器端就清除本次会话所有 session。浏览器端也会清除相应的cookie信息。 服务器端保存着大量session信息,很消耗服务器内存,而且如果多服务器部署,可以考虑session复制集群,也可以考虑session共享的问题,比如redismemcached等方案。

无session方案:

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

2.1 JWT

JWTJson WEB Token)是一种采用Json方式安装传输信息的方式。这次使用PyJWT,它是PythonJWT的实现。$ pip install pyjwt

import jwtimport base64import simplejsonfrom jwt import algorithmsSECRET_KEY = 'k*)_*v2%04niq0#5xc6fkl@p0pqjn2=hrm^yw3vdxloom2v7+2'payload = {    'user': 'sun',    'school': 'mag'}def add_eq(b: bytes):    """为base64编码补齐等号"""    r = 4 - len(b) % 4    return b + b'=' * renc = jwt.encode(payload, SECRET_KEY, algorithm="HS256")  # bytesprint(enc)header, pd, sig = enc.split(b'.')print(header, pd, sig, sep='\n')print('header = ', base64.urlsafe_b64encode(header))new_pd = base64.urlsafe_b64decode(add_eq(pd))print('payload =', new_pd)print(simplejson.loads(new_pd))print('sig =', base64.urlsafe_b64encode(sig))# 根据jwt算法重新生成签名# 1 获取算法对象alg = algorithms.get_default_algorithms()['HS256']# 
~~~~~~~print(alg, '~~~~~~~')new_key = alg.prepare_key(SECRET_KEY)print(new_key)# 2 获取前两部分 header.payloadsigning_input, _, _ = enc.rpartition(b'.')print(signing_input)# 3 使用key得到签名signature = alg.sign(signing_input, new_key)print('+++++++++++++++++++++++++++++++++++++++++++')print(signature)print(base64.urlsafe_b64encode(signature).decode().strip("="))# wMJbLvqCEV2rMXwtf7ibrRVUYN4os8D00t9f-LfUFHo

由此,可知jwt生成的token分为三部分:

  • header,由数据类型、加密算法构成 ;
  • payload,负载就是要传输的数据,一般来说放入python对象即可,会被json序列化的 ;
  • signature,签名部分。是前面2部分数据分别base64编码后使用点号连接后,加密算法使用key计算好一个结 果,再被base64编码,得到签名。

所有数据都是明文传输的,只是做了base64,如果是敏感信息,请不要使用jwt数据签名的目的不是为了隐藏数据,而是保证数据不被篡改。如果数据篡改了,发回到服务器端,服务器使 用自己的key再计算一遍,然后进行签名比对,一定对不上签名。

Jwt使用场景:

认证:这是Jwt最常用的场景,一旦用户登录成功,就会得到Jwt,然后请求中就可以带上这个Jwt。服务器中Jwt证通过,就可以被允许访问资源。甚至可以在不同域名中传递,在单点登录(Single Sign On)中应用广泛。

数据交换:Jwt可以防止数据被篡改,它还可以使用公钥、私钥加密,确保请求的发送者是可信的

密码:

使用邮箱 + 密码方式登录。邮箱要求唯一就行了,但是,密码如何存储?

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

bcrypt : $ pip install bcrypt

import bcryptimport datetimepassword = b'sqsltr520'# 每次拿盐都不一样print(1, bcrypt.gensalt())# 1 b'$2b$12$mMOjkZgJV52F8y6IOCC0EO'print(2, bcrypt.gensalt())# 2 b'$2b$12$EA5K14e7.qgobq3SFG7e7u'# 拿到的盐相同,计算得到的密文相同print('=========================')salt = bcrypt.gensalt()x = bcrypt.hashpw(password, salt)y = bcrypt.hashpw(password, salt)print(3, x)# 3 b'$2b$12$/z3KsBywn3gF1tIIXViNX.0MmHKbXqebaBRhQrIzKUv2N.E8pTknW'print(4, y)# 4 b'$2b$12$/z3KsBywn3gF1tIIXViNX.0MmHKbXqebaBRhQrIzKUv2N.E8pTknW'# 每次拿到的盐不同,生成的密文就不一样print('~~~~~~~~~~~~~~~~~~')m = bcrypt.hashpw(password, bcrypt.gensalt())n = bcrypt.hashpw(password, bcrypt.gensalt())print(5, m)# 5 b'$2b$12$wY/JiHJBNd7H73zJx/OL1OWaHyVn2TjmRzi10Ol9d2pDmOgDBbmuO'print(6, n)# 6 b'$2b$12$mDcxX4Ol3p8YpnYNjJ83o.ORqhVsz9ptxobKPum1CsuhyTO7/aS6u'# 校验print(bcrypt.checkpw(password, x), len(x))  # True 60print(bcrypt.checkpw(password + b' ', x), len(x))  # False 60# 计算时长(加密)start = datetime.datetime.now()p = bcrypt.hashpw(password, bcrypt.gensalt())delta = (datetime.datetime.now() - start).total_seconds()print(7, 'duration={}'.format(delta))# 校验时长start = datetime.datetime.now()q = bcrypt.checkpw(password, x)delta = (datetime.datetime.now() - start).total_seconds()print(8, 'duration={}'.format(delta))# 修改了密码start = datetime.datetime.now()x = bcrypt.checkpw(b'1', x)delta = (datetime.datetime.now() - start).total_seconds()print(9, 'duration={}'.format(delta))

 

 

 

 

转载地址:http://rvfvi.baihongyu.com/

你可能感兴趣的文章
qt实现点击出现窗口,点击其他任何地方窗口消失
查看>>
QML DropArea拖拉文件事件
查看>>
CORBA links
查看>>
读后感:&gt;
查看>>
ideas about sharing software
查看>>
different aspects for software
查看>>
To do list
查看>>
Study of Source code
查看>>
如何使用BBC英语学习频道
查看>>
spring事务探索
查看>>
浅谈Spring声明式事务管理ThreadLocal和JDKProxy
查看>>
初识xsd
查看>>
java 设计模式-职责型模式
查看>>
构造型模式
查看>>
svn out of date 无法更新到最新版本
查看>>
java杂记
查看>>
RunTime.getRuntime().exec()
查看>>
Oracle 分组排序函数
查看>>
删除weblogic 域
查看>>
VMware Workstation 14中文破解版下载(附密钥)(笔记)
查看>>