博客项目之用户功能设计与实现(JWT、bcrypt、CSRF)

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.py
from django.conf.urls import url
from .views import reg, test, login, logout, test_send_email
# 在views中编写视图函数reg、test、logoin、logout、test_send_email


urlpatterns = [
    # 下面三个有一个即可
    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_POST
def 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中。

# logging
LOGGING = {
    '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 jwt
import base64
import simplejson
from jwt import algorithms


SECRET_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'=' * r


enc = jwt.encode(payload, SECRET_KEY, algorithm="HS256")  # bytes
print(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']
# <jwt.algorithms.HMACAlgorithm object at 0x0000000002AD1EB8> ~~~~~~~
print(alg, '~~~~~~~')
new_key = alg.prepare_key(SECRET_KEY)
print(new_key)

# 2 获取前两部分 header.payload
signing_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 bcrypt
import datetime


password = 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 60
print(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))

 

 

 

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值