问题1:描述一个Web应用的工作流程。
问题2:描述项目的物理架构。(上图中补充负载均衡(反向代理)服务器、数据库服务器、文件服务器、邮件服务器、缓存服务器、防火墙等,而且每个节点都有可能是多节点构成的集群,如下图所示)
问题3:描述Django项目的工作流程。(如下图所示)
问题1:为什么要使用MVC架构模式?(模型和视图解耦合)
问题2:MVC架构中每个部分的作用?(如上图所示)
HTTP请求 = 请求行+请求头+空行+[消息体]
HTTP响应 = 响应行+响应头+空行+消息体
HTTPRequest
对象的属性和方法:
method
- 获取请求方法path
/ get_full_path()
- 获取请求路径/带查询字符串的路径scheme
/ is_secure()
/ get_host()
/ get_port()
- 获取请求的协议/主机/端口META
/ COOKIES
- 获取请求头/Cookie信息GET
/ POST
/ FILES
- 获取GET或POST请求参数/上传的文件get_signed_cookie()
- 获取带签名的Cookieis_ajax()
- 是不是Ajax异步请求body
/ content_type
/ encoding
- 获取请求的消息体(bytes流)/MIME类型/编码中间件添加的属性:
session
/ user
/ site
HttpResponse
对象的属性和方法:
set_cookie()
/ set_signed_cookie()
/ delete_cookie()
- 添加/删除Cookie__setitem__
/ __getitem__
/ __delitem__
- 添加/获取/删除响应头charset
/ content
/ status_code
- 响应的字符集/消息体(bytes流)/状态码
JsonResponse
(HttpResponse
的子类型)对象
class HouseJsonEncoder(JsonEncoder):
def default(self, o):
# 定义如何将对象转成dict类型并返回这个字典
pass
>>> from django.http import JsonResponse
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
>>> response = JsonResponse([1, 2, 3], safe=False)
>>> response = JsonResponse(house, encoder=HouseJsonEncoder)
>>> response = HttpResponse('')
>>> response['cotent-type'] = 'application/pdf';
>>> response['content-disposition'] = 'inline; filename="xyz.pdf"'
>>> response['content-disposition'] = 'attachment; filename="xyz.pdf"'
>>> response.set_signed_cookie('', '', salt='')
>>> response.status_code = 200
问题1:关系型数据库表的设计应该注意哪些问题?(范式理论)
问题2:关系型数据库中数据完整性指的是什么?(实体完整性/参照完整性/域完整性)
问题3:ORM是什么以及解决了什么问题?(对象模型-关系模型双向转换)
Field
及其子类的属性:
db_column
/ db_tablespace
null
/ blank
/ default
primary_key
db_index
/ unqiue
choices
/ help_text
/ error_message
/ editable
/ hidden
CharField
: max_length
DateField
: auto_now
/ auto_now_add
DecimalField
: max_digits
/ decimal_places
FileField
: storage
/ upload_to
ImageField
: height_field
/ width_field
ForeignKey
的属性:
重要属性:
db_constraint
(提升性能或者数据分片的情况可能需要设置为False)
on_delete
CASCADE
:级联删除。
PROTECT
:抛出ProtectedError
异常,阻止删除引用的对象。
SET_NULL
:把外键设置为null
,当null
属性被设置为True
时才能这么做。
SET_DEFAULT
:把外键设置为默认值,提供了默认值才能这么做。
related_name
class Dept(models.Model):
pass
class Emp(models.Model):
dept = models.ForeignKey(related_name='+', ...)
Dept.objects.get(no=10).emp_set.all()
Emp.objects.filter(dept__no=10)
说明:
related_name
设置为'+'
,可以防止一对多外键关键从“一”的一方查询“多”的一方。
其他属性:
to_field
/ limit_choices_to
/ swappable
Model
的属性和方法
objects
/ pk
save()
/ delete()
clean()
/ validate_unique()
/ full_clean()
QuerySet
的方法
get()
/ all()
/ values()
说明:
values()
返回的QuerySet
中不是模型对象而是字典
count()
/ order_by()
/ exists()
/ reverse()
filter()
/ exclude()
exact
/ iexact
:精确匹配/忽略大小写的精确匹配查询
contains
/ icontains
/ startswith / istartswith / endswith / iendswith
:基于like
的模糊查询
in
:集合运算
gt
/ gte
/ lt
/ lte
:大于/大于等于/小于/小于等于关系运算
range
:指定范围查询(SQL中的between…and…
)
year
/ month
/ day
/ week_day
/ hour
/ minute
/ second
:查询时间日期
isnull
:查询空值(True
)或非空值(False
)
search
:基于全文索引的全文检索
regex
/ iregex
:基于正则表达式的模糊匹配查询
aggregate()
/ annotate()
Avg
/ Count
/ Sum
/ Max
/ Min
>>> from django.db.models import Avg
>>> Emp.objects.aggregate(avg_sal=Avg('sal'))
(0.001) SELECT AVG(`TbEmp`.`sal`) AS `avg_sal` FROM `TbEmp`; args=()
{'avg_sal': 3521.4286}
>>> Emp.objects.values('dept').annotate(total=Count('dept'))
(0.001) SELECT `TbEmp`.`dno`, COUNT(`TbEmp`.`dno`) AS `total` FROM `TbEmp` GROUP BY `TbEmp`.`dno` ORDER BY NULL LIMIT 21; args=()
first()
/ last()
说明:调用
first()
方法相当于用[0]
对QuerySet
进行切片。
only()
/ defer()
>>> Emp.objects.filter(pk=7800).only('name', 'sal')
(0.001) SELECT `TbEmp`.`empno`, `TbEmp`.`ename`, `TbEmp`.`sal` FROM `TbEmp` WHERE `TbEmp`.`empno` = 7800 LIMIT 21; args=(7800,)
]>
>>> Emp.objects.filter(pk=7800).defer('name', 'sal')
(0.001) SELECT `TbEmp`.`empno`, `TbEmp`.`job`, `TbEmp`.`mgr`, `TbEmp`.`comm`, `TbEmp`.`dno` FROM `TbEmp` WHERE `TbEmp`.`empno` = 7800 LIMIT 21; args=(7800,)
]>
create()
/ update()
/ raw()
>>> Emp.objects.filter(dept__no=20).update(sal=F('sal') + 100)
(0.011) UPDATE `TbEmp` SET `sal` = (`TbEmp`.`sal` + 100) WHERE `TbEmp`.`dno` = 20; args=(100, 20)
>>>
>>> Emp.objects.raw('select empno, ename, job from TbEmp where dno=10')
Q
对象和F
对象
>>> from django.db.models import Q
>>> Emp.objects.filter(
... Q(name__startswith='张'),
... Q(sal__lte=5000) | Q(comm__gte=1000)
... ) # 查询名字以“张”开头且工资小于等于5000或补贴大于等于1000的员工
]>
reporter = Reporters.objects.filter(name='Tintin')
reporter.update(stories_filed=F('stories_filed') + 1)
原生SQL查询
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("UPDATE TbEmp SET sal=sal+10 WHERE dno=30")
cursor.execute("SELECT ename, job FROM TbEmp WHERE dno=10")
row = cursor.fetchall()
模型管理器
class BookManager(models.Manager):
def title_count(self, keyword):
return self.filter(title__icontains=keyword).count()
用户的每个操作对应一个视图函数。
每个视图函数可以构成一个事务边界。
事务的ACID特性。
事务隔离级别 - 设置事务隔离级别是为了数据库底层依据事务隔离级别为数据加上适当的锁。如果需要保证数据的强一致性,那么关系型数据库仍然是唯一的也是最好的选择,因为可以依赖关系型数据库的锁机制来保护数据。事务隔离级别越高,数据并发访问的问题越少,但是性能越差;事务隔离级别越低,数据并发访问的问题越多,但是性能越好。
Read Uncommitted < Read Committed < Repeatable Read < Serializable
数据并发访问会产生5种问题(请参考我的《Java面试题全集(上)》第80题对该问题的讲解):
set global transaction isolation level repeatable read;
set session transaction isolation level read committed;
select @@tx_isolation;
Django中的事务控制。
给每个请求绑定事务环境(反模式)。
ATOMIC_REQUESTS = True
使用事务装饰器(简单易用)。
@transaction.non_atomic_requests
@transaction.atomic
使用上下文语法(事务控制的范围更加精准)。
with transaction.atomic():
pass
关闭自动提交使用手动提交。
AUTOCOMMIT = False
transaction.commit()
transaction.rollback()
可以让部分URL只在调试模式下生效。
from django.conf import settings
urlpatterns = [
...
]
if settings.DEBUG:
urlpatterns += [ ... ]
可以使用命名捕获组捕获路径参数。
url(r'api/code/(?P1[3-9]\d{9})'),
path('api/code/'),
URL配置不关心请求使用的方法(一个视图函数可以处理不同的请求方式)。
如果使用url
函数捕获的路径参数都是字符串,path
函数可以指定路径参数类型。
可以使用include
函数引入其他URL配置,捕获的参数会向下传递。
在url
和path
函数甚至是include
函数中都可以用字典向视图传入额外的参数,如果参数与捕获的参数同名,则使用字典中的参数。
可以用reverse
函数实现URL的逆向解析(从名字解析出URL),在模板中也可以用{% url %}
实现同样的操作。
path('', views.index, name='index')
return redirect(reverse('index'))
return redirect('index')
模板的配置和渲染函数。
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
resp = render(request, 'foo.html', {'foo': {...}})
模板遇到变量名的查找顺序。
foo['bar']
)foo.bar
)foo.bar()
)
alters_data
被设置为True
则不能调用该方法(避免误操作的风险),模型对象动态生成的delete()
和save()
方法都设定了alters_data = True
。foo[0]
)模板标签的使用。
{% if %}
/ {% else %}
/ {% endif %}
{% for %}
/ {% endfor %}
{% ifequal %}
/ {% endifequal %}
/ {% ifnotequal %}
/ {% endifnotequal %}
{# comment #}
/ {% comment %}
/ {% endcomment %}
过滤器的使用。
lower
/ upper
/ first
/ last
/ truncatewords
/ date
/ time
/ length
/ pluralize
/ center
/ ljust
/ rjust
/ cut
/ urlencode
/ default_if_none
/ filesizeformat
/ join
/ slice
/ slugify
模板的包含和继承。
{% include %}
/ {% block %}
{% extends %}
模板加载器(后面优化部分会讲到)。
文件系统加载器
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
}]
应用目录加载器
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
}]
MIME类型。
Content-Type | 说明 |
---|---|
application/json | JSON(JavaScript Object Notation) |
application/pdf | PDF(Portable Document Format) |
audio/mpeg | MP3或其他MPEG音频文件 |
audio/vnd.wave | WAV音频文件 |
image/gif | GIF图像文件 |
image/jpeg | JPEG图像文件 |
image/png | PNG图像文件 |
text/html | HTML文件 |
text/xml | XML |
video/mp4 | MP4视频文件 |
video/quicktime | QuickTime视频文件 |
如何处置生成的内容。
response['content-type'] = 'application/pdf'
response['content-disposition'] = 'attachment; filename="xyz.pdf"'
提醒:URL以及请求和响应头中的中文都应该处理成百分号编码。
生成CSV / Excel / PDF。
向浏览器传输二进制数据。
buffer = ByteIO()
resp = HttpResponse(content_type='...')
resp['Content-Disposition'] = 'attachment;filename="..."'
resp.write(buffer.getvalue())
大文件的流式处理:StreamingHttpResponse
。
class EchoBuffer(object):
def write(self, value):
return value
def some_streaming_csv_view(request):
rows = (["Row {}".format(idx), str(idx)] for idx in range(65536))
writer = csv.writer(EchoBuffer())
resp = StreamingHttpResponse((writer.writerow(row) for row in rows),
content_type="text/csv")
resp['Content-Disposition'] = 'attachment; filename="data.csv"'
return resp
生成PDF:需要安装reportlab
。
生成Excel:需要安装openpyxl
。
统计图表。
问题1:中间件背后的设计理念是什么?(分离横切关注功能/拦截过滤器模式)
问题2:中间件有哪些不同的实现方式?(参考下面的代码)
问题4:描述Django内置的中间件及其执行顺序。
推荐阅读:Django官方文档 - 中间件 - 中间件顺序。
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',
'common.middlewares.block_sms_middleware',
]
def simple_middleware(get_response):
def middleware(request):
response = get_response(request)
return response
return middleware
class MyMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
response = view_func(*view_args, **view_kwargs)
return response
class MyMiddleware(object):
def __init__(self):
pass
def process_request(request):
pass
def process_view(request, view_func, view_args, view_kwargs):
pass
def process_template_response(request, response):
pass
def process_response(request, response):
pass
def process_exception(request, exception):
pass
CommonMiddleware
/
SecurityMiddleware
SessionMiddleware
CsrfViewMiddleware - 防范跨站身份伪造
XFrameOptionsMiddleware - 防范点击劫持攻击
is_valid()
/ is_multipart()
errors
/ fields
/ is_bound
/ changed_data
/ cleaned_data
add_error()
/ has_errors()
/ non_field_errors()
clean()
as_data()
/ as_json()
/ get_json_data()
问题1:Django中的Form
和ModelForm
有什么作用?(通常不用来生成表单主要用来验证数据)
问题2:表单上传文件时应该注意哪些问题?(表单的设置、多文件上传、图片预览、Ajax上传文件、上传后的文件如何存储)
问题1:使用Cookie能解决什么问题?(用户跟踪,解决HTTP协议无状态问题)
问题2:Cookie和Session之间关系是什么?(Session的标识会通过Cookie记录)
Session对应的中间件:django.contrib.sessions.middleware.SessionMiddleware
。
Session引擎。
基于数据库(默认方式)
INSTALLED_APPS = [
'django.contrib.sessions',
]
基于缓存(推荐使用)
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'session'
基于文件(基本不考虑)
基于Cookie(不靠谱)
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
Cookie相关的配置。
SESSION_COOKIE_NAME = 'djang_session_id'
SESSION_COOKIE_AGE = 1209600
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = False
SESSION_COOKIE_HTTPONLY = True
session的属性和方法。
session_key
/ session_data
/ expire_date
__getitem__
/ __setitem__
/ __delitem__
/ __contains__
set_expiry()
/ get_expiry_age()
/ get_expiry_date()
- 设置/获取会话超期时间flush()
- 销毁会话set_test_cookie()
/ test_cookie_worked()
/ delete_test_cookie()
- 测试浏览器是否支持Cookie(提示用户如果浏览器禁用Cookie可能会影响网站的使用)session的序列化。
SESSION_SERIALIZER = django.contrib.sessions.serializers.JSONSerializer
CACHES = {
# 默认缓存
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://120.77.222.217:6379/0',
],
'KEY_PREFIX': 'fang',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 500,
},
'PASSWORD': '1qaz2wsx',
}
},
# 页面缓存
'page': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://120.77.222.217:6379/1',
],
'KEY_PREFIX': 'fang:page',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 1000,
},
'PASSWORD': '1qaz2wsx',
}
},
# 会话缓存
'session': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://120.77.222.217:6379/2',
],
'KEY_PREFIX': 'fang:session',
'TIMEOUT': 1209600,
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 1000,
},
'PASSWORD': '1qaz2wsx',
}
},
# 验证码缓存
'code': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://120.77.222.217:6379/3',
],
'KEY_PREFIX': 'fang:code:tel',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 500,
},
'PASSWORD': '1qaz2wsx',
}
},
}
MIDDLEWARE_CLASSES = [
'django.middleware.cache.UpdateCacheMiddleware',
...
'django.middleware.common.CommonMiddleware',
...
'django.middleware.cache.FetchFromCacheMiddleware',
]
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 300
CACHE_MIDDLEWARE_KEY_PREFIX = 'djang:cache'
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie
@cache_page(timeout=60 * 15, cache='page')
@vary_on_cookie
def my_view(request):
pass
from django.views.decorators.cache import cache_page
urlpatterns = [
url(r'^foo/([0-9]{1,2})/$', cache_page(60 * 15)(my_view)),
]
模板片段缓存。
{% load cache %}
{% cache %}
/ {% endcache %}
使用底层API访问缓存。
>>> from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
>>> cache.clear()
>>> from django.core.cache import caches
>>> cache1 = caches['myalias']
>>> cache2 = caches['myalias']
>>> cache1 is cache2
True
>>> from django_redis import get_redis_connection
>>> redis_client = get_redis_connection()
>>> redis_client.hgetall()
NOTSET < DEBUG < INFO < WARNING < ERROR < FATAL
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
# 配置日志格式化器
'formatters': {
'simple': {
'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
'verbose': {
'format': '%(asctime)s %(levelname)s [%(process)d-%(threadName)s] '
'%(module)s.%(funcName)s line %(lineno)d: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
}
},
# 配置日志过滤器
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
# 配置日志处理器
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'filters': ['require_debug_true'],
'formatter': 'simple',
},
'file1': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'fang.log',
'when': 'W0',
'backupCount': 12,
'formatter': 'simple',
'level': 'INFO',
},
'file2': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'error.log',
'when': 'D',
'backupCount': 31,
'formatter': 'verbose',
'level': 'WARNING',
},
},
# 配置日志器
'loggers': {
'django': {
'handlers': ['console', 'file1', 'file2'],
'propagate': True,
'level': 'DEBUG',
},
}
}
日志配置官方示例。
Linux相关命令:head、tail、grep、awk、uniq、sort
tail -10000 access.log | awk '{print $1}' | uniq -c | sort -r
实时日志文件分析:Python + 正则表达式 + Crontab
《Python日志分析工具》。
《集中式日志系统ELK》。
问题1:RESTful架构到底解决了什么问题?(URL具有自描述性、资源表述与视图的解耦和、互操作性利用构建微服务以及集成第三方系统、无状态性提高水平扩展能力)
问题2:项目在使用RESTful架构时有没有遇到一些问题或隐患?(对资源访问的限制、资源从属关系检查、避免泄露业务信息、防范可能的攻击)
补充:下面的几个和安全性相关的响应头在前面讲中间件的时候提到过的。
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- X-XSS-Protection: 1; mode=block;
- Strict-Transport-Security: max-age=31536000;
问题3:如何保护API中的敏感信息以及防范重放攻击?(摘要和令牌)
推荐阅读:《如何有效防止API的重放攻击》。
INSTALLED_APPS = [
'rest_framework',
]
REST_FRAMEWORK = {
'PAGE_SIZE': 5,
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
}
from rest_framework import serializers
class ProvinceSerializer(serializers.ModelSerializer):
class Meta:
model = Province
fields = '__all__'
@csrf_exempt
def list_provinces(request):
if request.method == 'GET':
serializer = ProvinceSerializer(Province.objects.all(), many=True)
elif request.method = 'POST':
serializer = ProvinceSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)
@api_view(['GET', 'POST'])
def list_provinces(request):
if request.method == 'GET':
serializer = ProvinceSerializer(Province.objects.all(), many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = ProvinceSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
def list_cities_by_prov(request, prov_id):
serializer = CitySerializer(City.objects.filter(prov__prov_id=prov_id), many=True)
return Response(serializer.data)
urlpatterns = [
path('provinces/', views.list_provinces),
path('provinces/', views.list_cities_by_prov),
]
问题1:如何让浏览器能够发起DELETE/PUT/PATCH?
if request.method == 'POST' and '_method' in request.POST:
request.method = request.POST['_method'].upper()
问题2:如何解决多个JavaScript库之间某个定义(如$函数)冲突的问题?
问题3:jQuery对象与原生DOM对象之间如何转换?
更好的复用代码,不要重“复发明轮子”。
class CityView(APIView):
def get(self, request, pk, *args, **kwargs):
try:
serializer = CitySerializer(City.objects.get(pk=pk))
return Response(serializer.data)
except City.DoesNotExist:
raise Http404
def put(self, request, pk, *args, **kwargs):
try:
city = City.objects.get(pk=pk)
serializer = CitySerializer(city, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except City.DoesNotExist:
raise Http404
def delete(self, request, pk, *args, **kwargs):
try:
city = City.objects.get(pk=pk)
city.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except City.DoesNotExist:
raise Http404
urlpatterns = [
path('cities/', views.CityView.as_view()),
]
class DistrictViewSet(viewsets.ReadOnlyModelViewSet):
queryset = District.objects.all()
serializer_class = DistrictSerializer
class DistrictViewSet(viewsets.ModelViewSet):
queryset = District.objects.all()
serializer_class = DistrictSerializer
router = routers.DefaultRouter()
router.register('districts', views.DistrictViewSet)
urlpatterns += router.urls
利用Django自带的User和djangorestframework-jwt。
自行对用户及其身份验证的摘要进行处理。
sha1_proto = hashlib.sha1()
def check_user_sign(get_response):
def middleware(request):
if request.path.startswith('/api'):
data = request.GET if request.method == 'GET' else request.data
try:
user_id = data['user_id']
user_token = cache.get(f'fang:user:{user_id}')
user_sign = data['user_sign']
hasher = sha1_proto.copy()
hasher.update(f'{user_id}:{user_token}'.encode('utf-8'))
if hasher.hexdigest() == user_sign:
return get_response(request)
except KeyError:
pass
return JsonResponse({'msg': '身份验证失败拒绝访问'})
else:
return get_response(request)
return middleware
请求的时效性问题。(请求中再加上一个随机的令牌)
问题1:如何设计一套权限系统?(RBAC或ACL)
问题2:如何实现异步任务和定时任务?(Celery)
问题3:如何解决JavaScript跨域获取数据的问题?(django-cors-headers)
问题4:网站图片(水印、剪裁)和视频(截图、水印、转码)是如何处理的?(云存储、FFmpeg)
问题5:网站如何架设(静态资源)文件系统?
Celery 是一个简单、灵活且可靠的,处理大量消息的分布式系统,并且提供维护这样一个系统的必需工具。它是一个专注于实时处理的任务队列,同时也支持任务调度。
推荐阅读:《Celery官方文档中文版》,上面有极为详细的配置和使用指南。
Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis来实现消息队列服务,前者是更好的选择,它对AMQP(高级消息队列协议)做出了非常好的实现。
安装RabbitMQ。
docker pull rabbitmq
docker run -d -p 5672:5672 --name myrabbit rabbitmq
docker container exec -it myrabbit /bin/bash
创建用户、资源以及分配操作权限。
rabbitmqctl add_user luohao 123456
rabbitmqctl set_user_tags luohao administrator
rabbitmqctl add_vhost vhost1
rabbitmqctl set_permissions -p vhost1 luohao ".*" ".*" ".*"
创建Celery实例。
project_name = 'fang'
project_settings = '%s.settings' % project_name
# 注册环境变量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', project_settings)
app = celery.Celery(
project_name,
broker='amqp://jackfrued:[email protected]:5672/myvhost'
)
# 从默认的配置文件读取配置信息
app.config_from_object('django.conf:settings')
# Celery加载所有注册的应用
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
启动Celery创建woker(消息的消费者)。
celery -A fang worker -l info &
执行异步任务。
@app.task()
def send_email(from, to, cc, subject, content):
pass
async_result = send_email.delay('', [], [], '', '')
if async_result.ready():
async_result.get()
创建定时任务。
from celery.schedules import crontab
from celery.task import periodic_task
@periodic_task(run_every=crontab('*', '12,18'))
def print_dummy_info():
print('你妈喊你回家吃饭啦')
启动Celery创建执行定时任务的beat(消息的生产者)。
celery -A fang beat -l info
检查消息队列状况。
rabbitmqctl list_queues -p vhost1
问题1:什么是跨站脚本攻击,如何防范?(对提交的内容进行消毒)
问题2:什么是跨站身份伪造,如何防范?(使用随机令牌)
问题3:什么是SQL注射攻击,如何防范?(不拼接SQL语句,避免使用单引号)
问题4:什么是点击劫持攻击,如何防范?(不允许加载非同源站点内容)
签名数据的API
>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign('hello, world!')
>>> value
'hello, world!:BYMlgvWMTSPLxC-DqxByleiMVXU'
>>> signer.unsign(value)
'hello, world!'
>>>
>>> signer = Signer(salt='1qaz2wsx')
>>> signer.sign('hello, world!')
'hello, world!:9vEvG6EA05hjMDB5MtUr33nRA_M'
>>>
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign('hello, world!')
>>> value
'hello, world!:1fpmcQ:STwj464IFE6eUB-_-hyUVF3d2So'
>>> signer.unsign(value, max_age=5)
Traceback (most recent call last):
File "", line 1, in
File "/Users/Hao/Desktop/fang.com/venv/lib/python3.6/site-packages/django/core/signing.py", line 198, in unsign
'Signature age %s > %s seconds' % (age, max_age))
django.core.signing.SignatureExpired: Signature age 21.020604848861694 > 5 seconds
>>> signer.unsign(value, max_age=120)
'hello, world!'
CSRF令牌和小工具
{% csrf_token %}
@csrf_exempt
@csrf_protect
@require_csrf_token
@ensure_csrf_cookie
哈希摘要(签名)
>>> import hashlib
>>>
>>> md5_hasher = hashlib.md5()
>>> md5_hasher.update('hello, world!'.encode())
>>> md5_hasher.hexdigest()
'3adbbad1791fbae3ec908894c4963870'
>>>
>>> sha1_hasher = hashlib.sha1()
>>> sha1_hasher.update('hello, world!'.encode())
>>> sha1_hasher.update('goodbye, world!'.encode())
>>> sha1_hasher.hexdigest()
'1f09d30c707d53f3d16c530dd73d70a6ce7596a9'
加密和解密(对称加密和非对称加密)
pip install rsa
pip install pycrypto
>>> pub_key, pri_key = rsa.newkeys(1024)
>>> message = 'hello, world!'
>>> crypto = rsa.encrypt(message.encode(), pub_key)
>>> crypto
b'Ou{gH\xa9\xa8}O\xe3\x1d\x052|M\x9d9?\xdc\xd8\xecF\xd3v\x9b\xde\x8e\x12\xe6M\xebvx\x08\x08\x8b\xe8\x86~\xe4^)w\xf2\xef\x9e\x9fOg\x15Q\xb7\x7f\x1d\xcfV\xf1\r\xbe^+\x8a\xbf }\x10\x01\xa4U9b\x97\xf5\xe0\x90T\'\xd4(\x9b\x00\xa5\x92\x17\xad4\xb0\xb0"\xd4\x16\x94*s\xe1r\xb7L\xe2\x98\xb7\x7f\x03\xd9\xf2\t\xee*\xe6\x93\xe6\xe1o\xfd\x18\x83L\x0cfL\xff\xe4\xdd%\xf2\xc0/\xfb'
>>> origin = rsa.decrypt(crypto, pri_key).decode()
>>> origin
'hello, world!'
目标:测试函数和对象的方法(程序中最基本的单元)。
实施:通过对实际输出和预期输出的比对以及各种的断言条件来判定被测单元是否满足设计需求。
class UtilTest(TestCase):
def setUp(self):
self.pattern = re.compile(r'\d{6}')
def test_gen_mobile_code(self):
for _ in range(100):
self.assertIsNotNone(self.pattern.match(gen_mobile_code()))
def test_to_md5_hex(self):
md5_dict = {
'123456': 'e10adc3949ba59abbe56e057f20f883e',
'123123123': 'f5bb0c8de146c67b44babbf4e6584cc0',
'1qaz2wsx': '1c63129ae9db9c60c3e8aa94d3e00495',
}
for key, value in md5_dict.items():
self.assertEqual(value, to_md5_hex(key))
class ModelTest(TestCase):
def test_save_province(self):
pass
def test_del_province(self):
pass
def test_update_province(self):
pass
def test_get_all_provinces(self):
pass
def test_get_province_by_id(self):
pass
class RentViewTest(TestCase):
def test_index_view(self):
resp = self.client.get(reverse('index'))
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.context['num'], 5)
配置测试数据库
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': 'localhost',
'PORT': 3306,
'NAME': 'House',
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASS'],
'TEST': {
'NAME': 'House_for_testing',
'CHARSET': 'utf8',
},
}
}
python manage.py test
python manage.py test common
python manage.py test common.tests.UtilsTest
python manage.py test common.tests.UtilsTest.test_to_md5_hex
pip install coverage
coverage run --source= --omit= manage.py test common
coverage report
Name Stmts Miss Cover
---------------------------------------------------
common/__init__.py 0 0 100%
common/admin.py 1 0 100%
common/apps.py 3 3 0%
common/forms.py 16 16 0%
common/helper.py 32 32 0%
common/middlewares.py 19 19 0%
common/migrations/__init__.py 0 0 100%
common/models.py 71 2 97%
common/serializers.py 14 14 0%
common/tests.py 14 8 43%
common/urls_api.py 3 3 0%
common/urls_user.py 3 3 0%
common/utils.py 22 7 68%
common/views.py 69 69 0%
---------------------------------------------------
TOTAL 267 176 34%
问题1:性能测试的指标有哪些?
ab - Apache Benchmark
ab -c 10 -n 1000 http://www.baidu.com/
...
Benchmarking www.baidu.com (be patient).....done
Server Software: BWS/1.1
Server Hostname: www.baidu.com
Server Port: 80
Document Path: /
Document Length: 118005 bytes
Concurrency Level: 10
Time taken for tests: 0.397 seconds
Complete requests: 100
Failed requests: 98
(Connect: 0, Receive: 0, Length: 98, Exceptions: 0)
Write errors: 0
Total transferred: 11918306 bytes
HTML transferred: 11823480 bytes
Requests per second: 252.05 [#/sec] (mean)
Time per request: 39.675 [ms] (mean)
Time per request: 3.967 [ms] (mean, across all concurrent requests)
Transfer rate: 29335.93 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 6 7 0.6 7 9
Processing: 20 27 22.7 24 250
Waiting: 8 11 21.7 9 226
Total: 26 34 22.8 32 258
Percentage of the requests served within a certain time (ms)
50% 32
66% 34
75% 34
80% 34
90% 36
95% 39
98% 51
99% 258
100% 258 (longest request)
mysqlslap
mysqlslap -a -c 100 -h 1.2.3.4 -u root -p
mysqlslap -a -c 100 --number-of-queries=1000 --auto-generate-sql-load-type=read -h <负载均衡服务器IP地址> -u root -p
mysqlslap -a --concurrency=50,100 --number-of-queries=1000 --debug-info --auto-generate-sql-load-type=mixed -h 1.2.3.4 -u root -p
sysbench
sysbench --test=threads --num-threads=64 --thread-yields=100 --thread-locks=2 run
sysbench --test=memory --num-threads=512 --memory-block-size=256M --memory-total-size=32G run
jmeter
请查看《使用JMeter进行性能测试》。
LoadRunner / QTP
请参考《Django项目上线指南》。
配置缓存来缓解数据库的压力,并有合理的机制应对缓存穿透和缓存雪崩。
开启模板缓存来加速模板的渲染。
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
# 'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
'loaders': [(
'django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
], ),
],
},
},
]
用惰性求值、迭代器、defer()
、only()
等缓解内存压力。
用select_related()
和prefetch_related()
执行预加载避免“1+N查询问题”。
用ID生成器代替自增主键(性能更好、适用于分布式环境)。
>>> my_uuid = uuid.uuid1()
>>> my_uuid
UUID('63f859d0-a03a-11e8-b0ad-60f81da8d840')
>>> my_uuid.hex
'63f859d0a03a11e8b0ad60f81da8d840'
避免不必要的外键列上的约束(除非必须保证参照完整性),更不要使用触发器之类的机制。
使用索引来优化查询性能(索引放在要用于查询的字段上)。
select * from tb_goods where gname like '%iPhone%';
select * from tb_goods where gname like 'iPhone%';
Goods.objects.filter(name_icontains='iPhone')
Goods.objects.filter(name__istartswith='iPhone');
使用存储过程(存储在服务器端编译过的一组SQL语句)。
drop procedure if exists sp_avg_sal_by_dept;
create procedure sp_avg_sal_by_dept(deptno integer, out avg_sal float)
begin
select avg(sal) into avg_sal from TbEmp where dno=deptno;
end;
call sp_avg_sal_by_dept(10, @a);
select @a;
>>> from django.db import connection
>>> cursor = connection.cursor()
>>> cursor.callproc('sp_avg_sal_by_dept', (10, 0))
>>> cursor.execute('select @_sp_avg_sal_by_dept_1')
>>> cursor.fetchone()
(2675.0,)
使用explain
来分析查询性能 - 执行计划。
explain select * from ...;
请参考网易出品的《深入浅出MySQL》上面对应部分的讲解(已经有第二版)。
使用慢查询日志来发现性能低下的查询。
mysql> show variables like 'slow_query%';
+---------------------------+----------------------------------+
| Variable_name | Value |
+---------------------------+----------------------------------+
| slow_query_log | OFF |
| slow_query_log_file | /mysql/data/localhost-slow.log |
+---------------------------+----------------------------------+
mysql> show variables like 'long_query_time';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
mysql> set global slow_query_log='ON';
mysql> set global slow_query_log_file='/usr/local/mysql/data/slow.log';
mysql> set global long_query_time=1;
[mysqld]
slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log
long_query_time = 1
请参考《Python项目性能调优》。