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处理:
CSRF或XSRF(Cross-site Request Forgery),即跨站请求伪造。它也被称为:one click attack/session riding,是一种对网站的恶意利用。它伪装成来自受信任用户发起请求,难以防范。
- 用户登录某网站A完成登录认证,网站返回敏感信息的Cookie,即使是会话级的Cookie
- 用户没有关闭浏览器,或认证的Cookie一段时间内不过期还持久化了,用户就访问攻击网站B
- 攻击网站B看似一切正常,但是某些页面里面有一些隐藏运行的代码,或者诱骗用户操作的按钮等
- 这些代码一旦运行就是悄悄地向网站A发起特殊请求,由于网站A的Cookie还有效,且访问的是网站A,则其 Cookie就可以一并发给网站A
- 网站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语句中的limit和offffset子句。 注意使用索引返回的新的结果集,依然是惰性求值,不会立即查询。
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协议是无状态协议,为了解决它产生了cookie和session技术。
传统的session-cookie机制:
浏览器发起第一次请求到服务器,服务器发现浏览器没有提供session id,就认为这是第一次请求,会返回一个新的session id给浏览器端。浏览器只要不关闭,这个session id就会随着每一次请求重新发给服务器端,服务器端查找这个session id,如果查到,就认为是同一个会话。如果没有查到,就认为是新的请求。
session是会话级的,服务器端还可以在这个会话session中创建很多数据session键值对。 这个session id有过期的机制,一段时间如果没有发起请求,认为用户已经断开,服务器端就清除本次会话所有 session。浏览器端也会清除相应的cookie信息。 服务器端保存着大量session信息,很消耗服务器内存,而且如果多服务器部署,可以考虑session复制集群,也可以考虑session共享的问题,比如redis、memcached等方案。
无session方案:
既然服务端就是需要一个ID来表示身份,那么不使用session也可以创建一个ID返回给客户端。但是,要保证客户 端不可篡改该信息。服务端生成一个标识,并使用某种算法对标识签名。 服务端收到客户端发来的标识,需要检查签名。 这种方案的缺点是,加密、解密需要消耗CPU计算资源,无法让浏览器自己主动检查过期的数据以清除。 这种技术称作JWT(Json WEB Token)。
2.1 JWT
JWT(Json WEB Token)是一种采用Json方式安装传输信息的方式。这次使用PyJWT,它是Python对JWT的实现。$ 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))