diff --git a/Day91-100/Django知识点概述.md b/Day91-100/Django知识点概述.md deleted file mode 100644 index 951f2e2..0000000 --- a/Day91-100/Django知识点概述.md +++ /dev/null @@ -1,2213 +0,0 @@ -## Django知识点概述 - -### Web应用 - -问题1:描述一个Web应用的工作流程。 - -![](./res/web-application.png) - -问题2:描述项目的物理架构。(上图中补充负载均衡(反向代理)服务器、数据库服务器、文件服务器、邮件服务器、缓存服务器、防火墙等,而且每个节点都有可能是多节点构成的集群,如下图所示) - -![](./res/05.django_massive_cluster.png) - -问题3:描述Django项目的工作流程。(如下图所示) - -![](./res/django_request_response_cycle.png) - -### MVC架构模式 - -问题1:为什么要使用MVC架构模式?(模型和视图解耦合) - -问题2:MVC架构中每个部分的作用?(如下图所示) - -![](./res/mvc.png) - -### HTTP请求和响应 - -#### HTTP请求 = 请求行+请求头+空行+[消息体] - -![](./res/http-request.png) - -#### HTTP响应 = 响应行+响应头+空行+消息体 - -![](./res/http-response.png) - -1. `HTTPRequest`对象的属性和方法: - - - `method` - 获取请求方法 - - `path` / `get_full_path()` - 获取请求路径/带查询字符串的路径 - - `scheme` / `is_secure()` / `get_host()` / `get_port()` - 获取请求的协议/主机/端口 - - `META` / `COOKIES` - 获取请求头/Cookie信息 - - `GET` / `POST` / `FILES` - 获取GET或POST请求参数/上传的文件 - - `get_signed_cookie()` - 获取带签名的Cookie - - `is_ajax()` - 是不是Ajax异步请求 - - `body` / `content_type` / `encoding` - 获取请求的消息体(bytes流)/MIME类型/编码 -2. 中间件添加的属性: - - - `session` / `user` / `site` -3. `HttpResponse`对象的属性和方法: - - - `set_cookie()` / `set_signed_cookie()` / `delete_cookie()` - 添加/删除Cookie - - `__setitem__` / `__getitem__` / `__delitem__` - 添加/获取/删除响应头 - - `charset` / `content` / `status_code` - 响应的字符集/消息体(bytes流)/状态码 - - 1xx:请求已经收到,继续处理 - - 2xx(成功):请求已经成功收到、理解和接收。 - - 3xx(重定向):为完成请求要继续执行后续的操作。 - - 4xx(客户端错误):请求不正确或不能够被受理。 - - 5xx(服务器错误):服务器处理请求失败。 -4. `JsonResponse`(`HttpResponse`的子类型)对象 - - ```Python - >>> from django.http import HttpResponse, JsonResponse - >>> - >>> response = JsonResponse({'foo': 'bar'}) - >>> response.content - >>> - >>> response = JsonResponse([1, 2, 3], safe=False) - >>> response.content - >>> - >>> response = HttpResponse(b'...') - >>> response['cotent-type'] = 'application/pdf'; - >>> response['content-disposition'] = 'inline; filename="xyz.pdf"' - >>> response['content-disposition'] = 'attachment; filename="xyz.pdf"' - >>> - >>> response.set_signed_cookie('foo', 'bar', salt='') - >>> response.status_code = 200 - ``` - -### 数据模型(Model) - -问题1:关系型数据库表的设计应该注意哪些问题(范式理论和逆范式)?如何通过表来创建模型类(反向工程)?如何通过模型类来创建表(正向工程)? - -```Shell -python manage.py makemigrations -python manage.py migrate - -python manage.py inspectdb > /models.py -``` - -问题2:关系型数据库中数据完整性指的是什么?什么时候需要牺牲数据完整性?(实体完整性/参照完整性/域完整性) - -问题3:ORM是什么以及解决了什么问题?(对象模型-关系模型双向转换) - -1. `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` - -2. `ForeignKey`的属性: - - - 重要属性: - - `db_constraint`(提升性能或者数据分片的情况可能需要设置为False) - - - `on_delete` - - * `CASCADE`:级联删除。 - - - `PROTECT`:抛出`ProtectedError`异常,阻止删除引用的对象。 - - `SET_NULL`:把外键设置为`null`,当`null`属性被设置为`True`时才能这么做。 - - `SET_DEFAULT`:把外键设置为默认值,提供了默认值才能这么做。 - - - `related_name` - - ```Python - 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` - -3. `Model`的属性和方法 - - - `objects` / `pk` - - - `save()` / `delete()` - - - `clean()` / `validate_unique()` / `full_clean()` - -4. `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` - - ```Python - >>> 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} - ``` - - ```Python - >>> 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()`方法相当于用`[0]`对`QuerySet`进行切片。 - - - `only()` / `defer()` - - ```Python - >>> 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()` - - ```Python - >>> 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') - - ``` - -5. `Q`对象和`F`对象 - - ```Python - >>> from django.db.models import Q - >>> Emp.objects.filter( - ... Q(name__startswith='张'), - ... Q(sal__lte=5000) | Q(comm__gte=1000) - ... ) # 查询名字以“张”开头且工资小于等于5000或补贴大于等于1000的员工 - ]> - ``` - - ```Python - reporter = Reporters.objects.filter(name='Tintin') - reporter.update(stories_filed=F('stories_filed') + 1) - ``` - -6. 原生SQL查询 - - ```Python - 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() - ``` - -7. 模型管理器 - - ```Python - class BookManager(models.Manager): - - def title_count(self, keyword): - return self.filter(title__icontains=keyword).count() - ``` - -### 视图函数(Controller) - -#### 如何设计视图函数 - -1. 用户的每个操作对应一个视图函数。 - -2. [每个视图函数可以构成一个事务边界](https://docs.djangoproject.com/en/2.1/ref/settings/)。 - - - 事务的ACID特性。 - - - 原子性(Atomicity):事务中各项的操作要么全做要么全不做; - - 一致性(Consistentcy):事务前后系统的状态是一致的; - - 隔离性(Isolation):并发执行的事务无法看到彼此的中间状态; - - 持久性(Duration):事务完成后所做的改动都会被持久化。 - - - 事务隔离级别 - 设置事务隔离级别是为了数据库底层依据事务隔离级别为数据加上适当的锁。如果需要保证数据的强一致性,那么关系型数据库仍然是唯一的也是最好的选择,因为关系型数据库可以通过锁机制来保护数据。事务隔离级别从低到高依次是:Read Uncommitted(读未提交)、Read Committed(读提交)、Repeatable Read(可重复读)、Serializable(串行化)。事务隔离级别越高,数据并发访问的问题越少,但是性能越差;事务隔离级别越低,数据并发访问的问题越多,但是性能越好。 - - - 数据并发访问会产生5种问题(请参考我的[《Java面试题全集(上)》](https://blog.csdn.net/jackfrued/article/details/44921941)第80题对该问题的讲解): - - - 第1类丢失更新(A事务撤销覆盖B事务更新的数据)和第2类丢失更新(A事务提交覆盖B事务更新的数据)。 - - 脏读(读脏数据):一个事务读取到其他尚未提交的事务的数据。 - - 不可重复读: 一个事务在读取它的查询结果时,被另一个事务更新了它的查询记录导致无法读到数据。 - - 幻读:一个事务在读取它的查询结果时,发现读到了被另一个事务提交的新数据。 - - ```SQL - -- 设置全局默认的事务隔离级别 - set global transaction isolation level repeatable read; - -- 设置当前会话的事务隔离级别 - set session transaction isolation level read committed; - -- 查询当前会话的事务隔离级别 - select @@tx_isolation; - ``` - - - Django中的事务控制。 - - - 给每个请求绑定事务环境(反模式)。 - - ```Python - ATOMIC_REQUESTS = True - ``` - - - 使用事务装饰器(简单易用)。 - - ```Python - @transaction.non_atomic_requests - @transaction.atomic - ``` - - - 使用上下文语法(事务控制的范围更加精准)。 - - ```Python - with transaction.atomic(): - pass - ``` - - - 关闭自动提交使用手动提交。 - - ```Python - AUTOCOMMIT = False - ``` - - ```Python - transaction.commit() - transaction.rollback() - ``` - -#### URL配置 - -1. 可以让部分URL只在调试模式下生效。 - - ```Python - from django.conf import settings - - urlpatterns = [ - ... - ] - - if settings.DEBUG: - urlpatterns += [ ... ] - ``` - -2. 可以使用命名捕获组捕获路径参数。 - - ```Python - url(r'api/code/(?P1[3-9]\d{9})'), - path('api/code/'), - ``` - -3. URL配置不关心请求使用的方法(一个视图函数可以处理不同的请求方式)。 - -4. 如果使用`url`函数捕获的路径参数都是字符串,`path`函数可以指定路径参数类型。 - -5. 可以使用`include`函数引入其他URL配置,捕获的参数会向下传递。 - -6. 在`url`和`path`函数甚至是`include`函数中都可以用字典向视图传入额外的参数,如果参数与捕获的参数同名,则使用字典中的参数。 - -7. 可以用`reverse`函数实现URL的逆向解析(从名字解析出URL),在模板中也可以用`{% url %}`实现同样的操作。 - - ```Python - path('', views.index, name='index') - - return redirect(reverse('index')) - return redirect('index') - ``` - - -### 模板(View) - -#### 后端渲染 - -1. 模板的配置和渲染函数。 - - ```Python - 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', - ], - }, - }, - ] - ``` - - ```Python - resp = render(request, 'index.html', {'foo': ...}) - ``` - -2. 模板遇到变量名的查找顺序。 - - - 字典查找(如:`foo['bar']`) - - 属性查找(如:`foo.bar`) - - 方法调用(如:`foo.bar()`) - - 方法不能有必须传值的参数 - - 在模板中不能够给方法传参 - - 如果方法的`alters_data`被设置为`True`则不能调用该方法(避免误操作的风险),模型对象动态生成的`delete()`和`save()`方法都设定了`alters_data = True`。 - - 列表索引查找(如:`foo[0]`) - -3. 模板标签的使用。 - - - `{% if %}` / `{% else %}` / `{% endif %}` - - `{% for %}` / `{% endfor %}` - - `{% ifequal %}` / `{% endifequal %}` / `{% ifnotequal %}` / `{% endifnotequal %}` - - `{# comment #}` / `{% comment %}` / `{% endcomment %}` - -4. 过滤器的使用。 - - - `lower` / `upper` / `first` / `last` / `truncatewords` / `date `/ `time` / `length` / `pluralize` / `center` / `ljust` / `rjust` / `cut` / `urlencode` / `default_if_none` / `filesizeformat` / `join` / `slice` / `slugify` - -5. 模板的包含和继承。 - - - `{% include %}` / `{% block %}` - - `{% extends %}` - -6. 模板加载器(后面优化部分会讲到)。 - - - 文件系统加载器 - - ```Python - TEMPLATES = [{ - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - }] - ``` - - - 应用目录加载器 - - ```Python - TEMPLATES = [{ - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - }] - ``` - - -#### 前端渲染 - -1. 前端模板引擎:Handlebars / Mustache。 -2. 前端MV\*框架。 - - MVC - AngularJS - - MVVM - Vue.js - -#### 其他视图 - -1. MIME(多用途Internet邮件扩展)类型 - 告知浏览器传输的数据类型。 - - | Content-Type | 说明 | - | ---------------- | ------------------------------------------------------------ | - | application/json | [JSON](https://zh.wikipedia.org/wiki/JSON)(JavaScript Object Notation) | - | application/pdf | [PDF](https://zh.wikipedia.org/wiki/PDF)(Portable Document Format) | - | audio/mpeg | [MP3](https://zh.wikipedia.org/wiki/MP3)或其他[MPEG](https://zh.wikipedia.org/wiki/MPEG)音频文件 | - | audio/vnd.wave | [WAV](https://zh.wikipedia.org/wiki/WAV)音频文件 | - | image/gif | [GIF](https://zh.wikipedia.org/wiki/GIF)图像文件 | - | image/jpeg | [JPEG](https://zh.wikipedia.org/wiki/JPEG)图像文件 | - | image/png | [PNG](https://zh.wikipedia.org/wiki/PNG)图像文件 | - | text/html | [HTML](https://zh.wikipedia.org/wiki/HTML)文件 | - | text/xml | [XML](https://zh.wikipedia.org/wiki/XML) | - | video/mp4 | [MP4](https://zh.wikipedia.org/wiki/MP4)视频文件 | - | video/quicktime | [QuickTime](https://zh.wikipedia.org/wiki/QuickTime)视频文件 | - -2. 如何处置生成的内容(inline / attachment)。 - - ```Python - >>> from urllib.parse import quote - >>> - >>> response['content-type'] = 'application/pdf' - >>> filename = quote('Python语言规范.pdf') - >>> filename - 'Python%E8%AF%AD%E8%A8%80%E8%A7%84%E8%8C%83.pdf' - >>> response['content-disposition'] = f'attachment; filename="{filename}"' - ``` - > 提醒:URL以及请求和响应头中的中文都应该处理成[百分号编码](https://zh.wikipedia.org/zh-hans/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81)。 - -3. 生成CSV / Excel / PDF / 统计报表。 - - - 向浏览器传输二进制数据。 - - ```Python - buffer = ByteIO() - - resp = HttpResponse(content_type='...') - resp['Content-Disposition'] = 'attachment; filename="..."' - resp.write(buffer.getvalue()) - ``` - - - 大文件的流式处理:`StreamingHttpResponse`。 - - ```Python - def download_file(request): - file_stream = open('...', 'rb') - # 如果文件的二进制数据较大则最好用迭代器进行处理避免过多的占用服务器内存 - file_iter = iter(lambda: file_stream.read(4096), b'') - resp = StreamingHttpResponse(file_iter) - # 中文文件名要处理成百分号编码 - filename = quote('...', 'utf-8') - resp['Content-Type'] = '...' - resp['Content-Disposition'] = f'attachment; filename="{filename}"' - return resp - ``` - - > 说明:如果需要生成PDF文件,可以需要安装`reportlab`;生成Excel可以使用`openpyxl`或`xlrd`。 - - - [ECharts](http://echarts.baidu.com/)或[Chart.js](https://www.chartjs.org/)。 - - - 思路:后端只提供JSON格式的数据,前端JavaScript渲染生成图表。 - - -### 中间件 - -问题1:中间件背后的设计理念是什么?(分离横切关注功能/拦截过滤器模式) - -问题2:中间件有哪些不同的实现方式?(参考下面的代码) - -问题3:描述Django内置的中间件及其执行顺序。(推荐阅读:[Django官方文档 - 中间件 - 中间件顺序](https://docs.djangoproject.com/zh-hans/2.0/ref/middleware/#middleware-ordering)) - -#### 激活中间件 - -```Python -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', -] -``` - -#### 自定义中间件 - - -```Python -def simple_middleware(get_response): - - def middleware(request): - - response = get_response(request) - - return response - - return middleware -``` - -```Python -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 -``` - -```Python -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 -``` - - -#### 内置中间件 - -1. CommonMiddleware - 基础设置中间件 - - DISALLOWED_USER_AGENTS - 不被允许的用户代理(浏览器) - - APPEND_SLASH - 是否追加`/` - - USE_ETAG - 浏览器缓存相关 - -2. SecurityMiddleware - 安全相关中间件 - - SECURE_HSTS_SECONDS - 强制使用HTTPS的时间 - - SECURE_HSTS_INCLUDE_SUBDOMAINS - HTTPS是否覆盖子域名 - - SECURE_CONTENT_TYPE_NOSNIFF - 是否允许浏览器推断内容类型 - - SECURE_BROWSER_XSS_FILTER - 是否启用跨站脚本攻击过滤器 - - SECURE_SSL_REDIRECT - 是否重定向到HTTPS连接 - - SECURE_REDIRECT_EXEMPT - 免除重定向到HTTPS - -3. SessionMiddleware - 会话中间件 - -4. CsrfViewMiddleware - 防范跨站身份伪造中间件 - -5. XFrameOptionsMiddleware - 防范点击劫持攻击中间件 - - ![](./res/click-jacking.png) - -![](./res/builtin-middlewares.png) - -### 表单 - -1. 用法:通常不要用来生成页面上的表单控件(耦合度太高不容易定制),主要用来验证数据。 -2. Form的属性和方法: - - `is_valid()` / `is_multipart()` - - `errors` / `fields` / `is_bound` / `changed_data` / `cleaned_data` - - `add_error()` / `has_errors()` / `non_field_errors()` - - `clean()` -3. Form.errors的方法: - - `as_data()` / `as_json()` / `get_json_data()` - -问题1:Django中的`Form`和`ModelForm`有什么作用?(通常不用来生成表单主要用来验证数据) - -问题2:表单上传文件时应该注意哪些问题?(表单的设置、多文件上传、图片预览、Ajax上传文件、上传后的文件如何存储) - -### Cookie和Session - -问题1:使用Cookie能解决什么问题?(用户跟踪,解决HTTP协议无状态问题) - -1. URL重写 - - ``` - http://www.abc.com/path/resource?foo=bar - ``` - -2. 隐藏域(隐式表单域) - - ```HTML -
- - - -
- ``` - -3. Cookie - -问题2:Cookie和Session之间关系是什么?(Session的标识通过Cookie保存和传输) - -#### Session的配置 - -1. Session对应的中间件:`django.contrib.sessions.middleware.SessionMiddleware`。 - -2. Session引擎。 - - - 基于数据库(默认方式) - - ```Python - INSTALLED_APPS = [ - 'django.contrib.sessions', - ] - ``` - - - 基于缓存(推荐使用) - - ```Python - SESSION_ENGINE = 'django.contrib.sessions.backends.cache' - SESSION_CACHE_ALIAS = 'session' - ``` - - - 基于文件(基本不考虑) - - - 基于Cookie(不靠谱) - - ```Python - SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' - ``` - -3. Cookie相关的配置。 - - ```Python - 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 - ``` - -4. 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可能会影响网站的使用) - -5. session的序列化。 - - ```Python - SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer' - ``` - - - JSONSerializer(1.6及以后默认)- 如果想将自定义的对象放到session中,会遇到“Object of type 'XXX' is not JSON serializable”的问题。 - - PickleSerializer(1.6以前的默认)- 因为安全问题不推荐使用,但是只要不去反序列化用户构造的恶意的Payload其实也没有什么风险。关于这种方式的安全漏洞,可以参考《[Python Pickle的任意代码执行漏洞实践和Payload构造》](http://www.polaris-lab.com/index.php/archives/178/)一文或《软件架构-Python语言实现》上关于这个问题的讲解。 - - 说明:如果使用了django_redis整合Redis作为session的存储引擎,那么由于django_redis又封装了一个PickleSerializer来提供序列化,所以不会存在上述的问题,且Redis中保存的value是pickle序列化之后的结果。 - - -### 缓存 - -#### 配置缓存 - - -```Python -CACHES = { - # 默认缓存 - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': [ - 'redis://1.2.3.4:6379/0', - ], - 'KEY_PREFIX': 'fang', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - 'CONNECTION_POOL_KWARGS': { - 'max_connections': 1000, - }, - 'PASSWORD': '1qaz2wsx', - } - }, - # 页面缓存 - 'page': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': [ - 'redis://1.2.3.4:6379/1', - ], - 'KEY_PREFIX': 'fang:page', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - 'CONNECTION_POOL_KWARGS': { - 'max_connections': 500, - }, - 'PASSWORD': '1qaz2wsx', - } - }, - # 会话缓存 - 'session': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': [ - 'redis://1.2.3.4:6379/2', - ], - 'KEY_PREFIX': 'fang:session', - 'TIMEOUT': 1209600, - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - 'CONNECTION_POOL_KWARGS': { - 'max_connections': 2000, - }, - 'PASSWORD': '1qaz2wsx', - } - }, - # 验证码缓存 - 'code': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': [ - 'redis://1.2.3.4:6379/3', - ], - 'KEY_PREFIX': 'fang:code:tel', - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - 'CONNECTION_POOL_KWARGS': { - 'max_connections': 500, - }, - 'PASSWORD': '1qaz2wsx', - } - }, -} -``` -#### 全站缓存 - -```Python -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' -``` -#### 视图层缓存 - -```Python -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 -``` - -```Python -from django.views.decorators.cache import cache_page - -urlpatterns = [ - url(r'^foo/([0-9]{1,2})/$', cache_page(60 * 15)(my_view)), -] -``` -#### 其他内容 - -1. 模板片段缓存。 - - - `{% load cache %}` - - `{% cache %}` / `{% endcache %}` - -2. 使用底层API访问缓存。 - - ```Python - >>> from django.core.cache import cache - >>> - >>> cache.set('my_key', 'hello, world!', 30) - >>> cache.get('my_key') - >>> cache.clear() - ``` - - ```Python - >>> from django.core.cache import caches - >>> cache1 = caches['page'] - >>> cache2 = caches['page'] - >>> cache1 is cache2 - True - >>> cache3 = caches['session'] - >>> cache2 is cache3 - False - ``` - - ```Python - >>> from django_redis import get_redis_connection - >>> - >>> redis_client = get_redis_connection() - >>> redis_client.hgetall() - ``` - - -### 日志 - -#### 日志级别 - -NOTSET < DEBUG < INFO < WARNING < ERROR < FATAL - -#### 日志配置 - -```Python -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', - }, - } -} -``` - -[日志配置官方示例](https://docs.djangoproject.com/zh-hans/2.0/topics/logging/#s-examples)。 - -#### 日志分析 - -1. Linux相关命令:head、tail、grep、awk、uniq、sort - - ```Shell - tail -10000 access.log | awk '{print $1}' | uniq -c | sort -r - ``` - -2. 实时日志文件分析:Python + 正则表达式 + Crontab - -3. [《Python日志分析工具》](https://github.com/jkklee/web_log_analyse)。 - -4. [《集中式日志系统ELK》](https://www.ibm.com/developerworks/cn/opensource/os-cn-elk/index.html)。 - - - ElasticSearch:搜索引擎,实现全文检索。 - - Logstash:负责从指定节点收集日志。 - - Kibana:日志可视化工具。 - -### RESTful - -问题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的重放攻击》](https://help.aliyun.com/knowledge_detail/50041.html)。 - -#### 使用djangorestframework - -安装djangorestfrmework(为了描述方便,以下统一简称为drf)。 - -```Shell -pip install djangorestframework -``` - -配置drf。 - -```Python -INSTALLED_APPS = [ - - 'rest_framework', - -] - -REST_FRAMEWORK = { - # 配置默认页面大小 - 'PAGE_SIZE': 5, - # 配置默认的分页类 - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', -} -``` - -#### 编写序列化器 - -```Python -from rest_framework import serializers - - -class HouseTypeSerializer(serializers.ModelSerializer): - - class Meta: - model = HouseType - fields = '__all__' - - -class DistrictSerializer(serializers.ModelSerializer): - - class Meta: - model = District - fields = ('distid', 'name', 'intro') - - -class AgentSerializer(serializers.ModelSerializer): - # 如果属性需要通过代码来获取就定义为SerializerMethodField - # 获取estates属性的方法应该命名为get_estates - estates = serializers.SerializerMethodField() - - @staticmethod - def get_estates(agent): - ret_value = [] - # 对于多对一外键关联(ForeignKey)可以用select_related提前加载关联属性 - # 通过这种方式可以使用内连接或左外连接直接获取关联属性避免1+N查询问题 - items = agent.estates.all().select_related('district') - for item in items: - ret_value.append({"estateid": item.estateid, - "name": item.name, - "district": DistrictSerializer(item.district).data}) - return ret_value - - class Meta: - model = Agent - fields = ('agentid', 'name', 'tel', 'certificated', 'estates') -``` - -#### 方法1:使用装饰器 - -```Python -@api_view(['GET']) -@cache_page(timeout=None) -def provinces(request): - query_set = District.objects.filter(parent__isnull=True) - serializer = DistrictSerializer(query_set, many=True) - return JsonResponse(serializer.data, safe=False) - - -@api_view(['GET']) -@cache_page(timeout=120) -def districts(request, pid): - query_set = District.objects.filter(parent__distid=pid) - serializer = DistrictSerializer(query_set, many=True) - return JsonResponse(serializer.data, safe=False) - -``` - -```Python -urlpatterns = [ - path('districts/', views.provinces, name='districts'), - path('districts/', views.districts, name='district'), -] -``` - -> 说明:上面使用了Django自带的视图装饰器(@cache_page)来实现对API接口返回数据的缓存。 - -#### 方法2:使用APIView及其子类 - -更好的复用代码,不要重“复发明轮子”。 - -```Python -from rest_framework.generics import ListAPIView -from rest_framework.response import Response -from rest_framework_extensions.cache.decorators import cache_response - - -def customize_cache_key(view_instance, view_method, request, args, kwargs): - """自定义缓存的key的函数""" - full_path = request.get_full_path() - return f'fangall:api:{full_path}' - - -class AgentDetailView(ListAPIView): - queryset = Agent.objects.all() - serializer_class = AgentDetailSerializer - pagination_class = None - - @cache_response(key_func=customize_cache_key) - def get(self, request, agentid, *args, **kwargs): - query_set = Agent.objects.filter(agentid=agentid)\ - .prefetch_related("estates").last() - serializer = AgentDetailSerializer(query_set) - return Response(serializer.data) -``` - -```Python -urlpatterns = [ - path('agents/', views.AgentDetailView.as_view(), name='agent'), -] -``` - -> 说明:上面使用了drf_extensions提供的@cache_response实现了对接口数据的缓存,并使用自定义的函数来生成缓存中的key。 - -#### 方法3:使用ViewSet及其子类 - -```Python -class HouseTypeViewSet(CacheResponseMixin, viewsets.ModelViewSet): - queryset = HouseType.objects.all() - serializer_class = HouseTypeSerializer - pagination_class = None -``` - -```Python -router = DefaultRouter() -router.register('housetypes', views.HouseTypeViewSet) - -urlpatterns += router.urls -``` - -> 说明:上面使用了drf_extensions提供的CacheResponseMixin混入类实现了对接口数据的缓存。 - -drf提供了基于Bootstrap定制的页面来显示接口返回的JSON数据,当然也可以使用[POSTMAN](https://www.getpostman.com/)这样的工具对API接口进行测试。 - -#### 补充说明 - -在这里顺便提一下跟前端相关的几个问题。 - -问题1:如何让浏览器能够发起DELETE/PUT/PATCH? - -```HTML -
- - - -
-``` - -```Python -if request.method == 'POST' and '_method' in request.POST: - request.method = request.POST['_method'].upper() -``` - -```HTML - -``` - -问题2:如何解决多个JavaScript库之间某个定义(如$函数)冲突的问题? - -```HTML - - - -``` - -```HTML - - - -``` - -问题3:jQuery对象与原生DOM对象之间如何转换? - -```HTML - - - -``` - -#### 身份认证 - -查看drf中APIView类的代码可以看出,drf默认的认证方案是 `DEFAULT_AUTHENTICATION_CLASSES`,如果修改authentication_classes就可以自行定制身份认证的方案。 - -```Python -class APIView(View): - - # The following policies may be set at either globally, or per-view. - renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES - parser_classes = api_settings.DEFAULT_PARSER_CLASSES - authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES - throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES - permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES - content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS - metadata_class = api_settings.DEFAULT_METADATA_CLASS - versioning_class = api_settings.DEFAULT_VERSIONING_CLASS - - # 此处省略下面的代码 -``` - -```Python -DEFAULTS = { - # Base API policies - 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer', - ), - 'DEFAULT_PARSER_CLASSES': ( - 'rest_framework.parsers.JSONParser', - 'rest_framework.parsers.FormParser', - 'rest_framework.parsers.MultiPartParser' - ), - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.BasicAuthentication' - ), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.AllowAny', - ), - 'DEFAULT_THROTTLE_CLASSES': (), - 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', - 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata', - 'DEFAULT_VERSIONING_CLASS': None, - - # 此处省略下面的代码 -} -``` - -自定义认证类,继承`BaseAuthentication`并重写`authenticate(self, request)`方法,通过请求中的userid和token来确定用户身份。如果认证成功,该方法应返回一个二元组(用户和令牌的信息),否则产生异常。也可以重写 `authenticate_header(self, request)`方法来返回一个字符串,该字符串将用于`HTTP 401 Unauthorized`响应中的WWW-Authenticate响应头的值。如果未重写该方法,那么当未经身份验证的请求被拒绝访问时,身份验证方案将返回`HTTP 403 Forbidden`响应。 - -```Python -class Authentication(BaseAuthentication): - - def authenticate(self, request): - try: - userid = request.GET['userid'] - token = request.GET['token'] - user = User.objects.filter(userid=userid, token=token).first() - if not user: - raise AuthenticationFailed('用户身份信息认证失败') - return user, user - except KeyError: - raise NotAuthenticated('请提供当前用户身份认证信息') - - def authenticate_header(self, request): - pass -``` - -使用自定义的认证类。 - -```Python -class AgentDetailView(ListAPIView): - queryset = Agent.objects.all() - serializer_class = AgentDetailSerializer - authentication_classes = [Authentication, ] - pagination_class = None - - @cache_response(key_func=customize_cache_key) - def get(self, request, agentid, *args, **kwargs): - query_set = Agent.objects.filter(agentid=agentid)\ - .prefetch_related("estates").last() - serializer = AgentDetailSerializer(query_set) - return Response(serializer.data) -``` - -#### 授予权限 - -权限检查总是在视图的最开始处运行,在任何其他代码被允许进行之前。最简单的权限是允许通过身份验证的用户访问,并拒绝未经身份验证的用户访问,这对应于dfr中的`IsAuthenticated`类,可以用它来取代默认的`AllowAny`类。权限策略可以在Django的drf配置中用`DEFAULT_PERMISSION_CLASSES`全局设置。 - -```Python -REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ) -} -``` - -也可以在基于`APIView`类的视图上设置身份验证策略。 - -```Python -from rest_framework.permissions import IsAuthenticated -from rest_framework.views import APIView - -class ExampleView(APIView): - permission_classes = (IsAuthenticated, ) - # 此处省略其他代码 -``` - -或者在基于`@api_view`装饰器的视图函数上设置。 - -```Python -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import IsAuthenticated - -@api_view(['GET']) -@permission_classes((IsAuthenticated, )) -def example_view(request, format=None): - # 此处省略其他代码 -``` - -自定义权限需要继承`BasePermission`并实现以下方法中的一个或两个,下面是BasePermission的代码。 - -```Python -@six.add_metaclass(BasePermissionMetaclass) -class BasePermission(object): - """ - A base class from which all permission classes should inherit. - """ - - def has_permission(self, request, view): - """ - Return `True` if permission is granted, `False` otherwise. - """ - return True - - def has_object_permission(self, request, view, obj): - """ - Return `True` if permission is granted, `False` otherwise. - """ - return True -``` - -如果请求被授予访问权限,则方法应该返回True,否则返False。下面的例子演示了阻止黑名单中的IP地址访问接口数据(这个在反爬虫的时候很有用哟)。 - -```Python -from rest_framework import permissions - - -class BlacklistPermission(permissions.BasePermission): - """ - Global permission check for blacklisted IPs. - """ - - def has_permission(self, request, view): - ip_addr = request.META['REMOTE_ADDR'] - blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists() - return not blacklisted -``` - -如果要实现更为完整的权限验证,可以考虑RBAC或ACL。 - -1. RBAC - 基于角色的访问控制(用户-角色-权限-资源,都是多对多关系)。 -2. ACL - 访问控制列表(每个用户绑定自己的访问白名单)。 - -#### 访问限流 - -可以修改dfr配置的`DEFAULT_THROTTLE_CLASSES` 和 `DEFAULT_THROTTLE_RATES`两个值来设置全局默认限流策略。例如: - -```Python -REST_FRAMEWORK = { - 'DEFAULT_THROTTLE_CLASSES': ( - 'rest_framework.throttling.AnonRateThrottle', - 'rest_framework.throttling.UserRateThrottle' - ), - 'DEFAULT_THROTTLE_RATES': { - 'anon': '100/day', - 'user': '1000/day' - } -} -``` - -`DEFAULT_THROTTLE_RATES`中使用的频率描述可能包括`second`、`minute`、`hour`或`day`。 - -如果要为接口单独设置限流,可以在每个视图或视图集上设置限流策略,如下所示: - -```Python -from rest_framework.throttling import UserRateThrottle -from rest_framework.views import APIView - - -class ExampleView(APIView): - throttle_classes = (UserRateThrottle, ) - # 此处省略下面的代码 -``` - -或 - -```Python -@api_view(['GET']) -@throttle_classes([UserRateThrottle, ]) -def example_view(request, format=None): - # 此处省略下面的代码 -``` - -当然也可以通过继承`BaseThrottle`来自定义限流策略,需要重写`allow_request`和`wait`方法。 - -### 异步任务和计划任务 - -#### Celery的应用 - -Celery 是一个简单、灵活且可靠的,处理大量消息的分布式系统,并且提供维护这样一个系统的必需工具。它是一个专注于实时处理的任务队列,同时也支持任务调度。 - -推荐阅读:[《Celery官方文档中文版》](http://docs.jinkan.org/docs/celery/),上面有极为详细的配置和使用指南。 - -![](./res/celery.png) - -Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis来实现消息队列服务,前者是更好的选择,它对AMQP(高级消息队列协议)做出了非常好的实现。 - -1. 安装RabbitMQ。 - - ```Shell - docker pull rabbitmq - docker run -d -p 5672:5672 --name myrabbit rabbitmq - docker container exec -it myrabbit /bin/bash - ``` - -2. 创建用户、资源以及分配操作权限。 - - ```Shell - rabbitmqctl add_user luohao 123456 - rabbitmqctl set_user_tags luohao administrator - rabbitmqctl add_vhost vhost1 - rabbitmqctl set_permissions -p vhost1 luohao ".*" ".*" ".*" - ``` - -3. 创建Celery实例。 - - ```Python - project_name = '...' - project_settings = '%s.settings' % project_name - - # 注册环境变量 - os.environ.setdefault('DJANGO_SETTINGS_MODULE', project_settings) - - app = celery.Celery( - project_name, - broker='amqp://luohao:123456@120.77.222.217:5672/vhost1' - ) - - # 从默认的配置文件读取配置信息 - app.config_from_object('django.conf:settings') - - # Celery加载所有注册的应用 - app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) - ``` - -4. 启动Celery创建woker(消息的消费者)。 - - ```Shell - celery -A worker -l debug & - ``` - -5. 执行异步任务。 - - ```Python - @app.task - def send_email(from, to, cc, subject, content): - pass - - - send_email.delay('', [], [], '', '') - ``` - -6. 创建定时任务。 - - ```Python - # 配置定时任务(计划任务) - app.conf.update( - timezone=settings.TIME_ZONE, - enable_utc=True, - # 定时任务(计划任务)相当于是消息的生产者 - # 如果只有生产者没有消费者那么消息就会在消息队列中积压 - # 将来实际部署项目的时候生产者、消费者、消息队列可能都是不同节点 - beat_schedule={ - 'task1': { - 'task': 'common.tasks.show_msg', - 'schedule': crontab(), - 'args': ('刘强东,奶茶妹妹喊你回家喝奶啦', ) - }, - }, - ) - ``` - - ```Python - @app.task - def show_msg(content): - print(content) - ``` - -7. 启动Celery创建执行定时任务的beat(消息的生产者)。 - - ```Shell - celery -A beat -l info - ``` - -8. 检查消息队列状况。 - - ```Shell - rabbitmqctl list_queues -p vhost1 - ``` - -### 其他问题 - -问题1:如何解决JavaScript跨域获取数据的问题?(django-cors-headers) - -```Python -INSTALLED_APPS = [ - 'corsheaders', -] -MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', -] -# 配置跨域白名单 -# CORS_ORIGIN_WHITELIST = () -# 配置是否跨域读取Cookie信息 -# CORS_ALLOW_CREDENTIALS = True -``` - -问题2:网站图片(水印、剪裁)和视频(截图、水印、转码)是如何处理的?(云存储、FFmpeg) - -问题3:网站如何架设(静态资源)文件系统?(FastDFS、云存储、CDN) - -### 安全保护 - -问题1:什么是跨站脚本攻击,如何防范?(对提交的内容进行消毒) - -问题2:什么是跨站身份伪造,如何防范?(使用随机令牌) - -问题3:什么是SQL注射攻击,如何防范?(不拼接SQL语句,避免使用单引号) - -问题4:什么是点击劫持攻击,如何防范?(不允许`