Django基础知识

Django知识点概述

Web应用

问题1:描述一个Web应用的工作流程。

问题2:描述项目的物理架构。(上图中补充负载均衡(反向代理)服务器、数据库服务器、文件服务器、邮件服务器、缓存服务器、防火墙等,而且每个节点都有可能是多节点构成的集群,如下图所示)

问题3:描述Django项目的工作流程。(如下图所示)

MVC架构模式

问题1:为什么要使用MVC架构模式?(模型和视图解耦合)

问题2:MVC架构中每个部分的作用?(如上图所示)

HTTP请求和响应

HTTP请求

HTTP请求 = 请求行+请求头+空行+[消息体]

HTTP响应 = 响应行+响应头+空行+消息体

  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. JsonResponseHttpResponse的子类型)对象

    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
    

数据模型(Model)

问题1:关系型数据库表的设计应该注意哪些问题?(范式理论)

问题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

        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

        >>> 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')
      
      
  5. 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)
    
  6. 原生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()
    
  7. 模型管理器

    class BookManager(models.Manager):
        
        def title_count(self, keyword):
            return self.filter(title__icontains=keyword).count()
    

视图函数(Controller)

如何设计视图函数

  1. 用户的每个操作对应一个视图函数。

  2. 每个视图函数可以构成一个事务边界。

    • 事务的ACID特性。

    • 事务隔离级别 - 设置事务隔离级别是为了数据库底层依据事务隔离级别为数据加上适当的锁。如果需要保证数据的强一致性,那么关系型数据库仍然是唯一的也是最好的选择,因为可以依赖关系型数据库的锁机制来保护数据。事务隔离级别越高,数据并发访问的问题越少,但是性能越差;事务隔离级别越低,数据并发访问的问题越多,但是性能越好。

      Read Uncommitted < Read Committed < Repeatable Read < Serializable

      数据并发访问会产生5种问题(请参考我的《Java面试题全集(上)》第80题对该问题的讲解):

      • 第1类丢失更新(A事务撤销覆盖B事务更新的数据)和第2类丢失更新(A事务提交覆盖B事务更新的数据)。
      • 脏读(读脏数据)- 一个事务读取到其他尚未提交的事务的数据。
      • 不可重复读 - 一个事务在读取它的查询结果时,被另一个事务更新了它的查询记录导致无法读到数据。
      • 幻读 - 一个事务在读取它的查询结果时,发现读到了被另一个事务提交的新数据。
      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配置

  1. 可以让部分URL只在调试模式下生效。

    from django.conf import settings
    
    urlpatterns = [
        ...
    ]
    
    if settings.DEBUG:
        urlpatterns += [ ... ]
    
  2. 可以使用命名捕获组捕获路径参数。

    url(r'api/code/(?P1[3-9]\d{9})'),
    path('api/code/'),
    
  3. URL配置不关心请求使用的方法(一个视图函数可以处理不同的请求方式)。

  4. 如果使用url函数捕获的路径参数都是字符串,path函数可以指定路径参数类型。

  5. 可以使用include函数引入其他URL配置,捕获的参数会向下传递。

  6. urlpath函数甚至是include函数中都可以用字典向视图传入额外的参数,如果参数与捕获的参数同名,则使用字典中的参数。

  7. 可以用reverse函数实现URL的逆向解析(从名字解析出URL),在模板中也可以用{% url %}实现同样的操作。

    path('', views.index, name='index')
    
    return redirect(reverse('index'))
    return redirect('index')
    

模板(View)

后端渲染

  1. 模板的配置和渲染函数。

    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': {...}})
    
  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. 模板加载器(后面优化部分会讲到)。

    • 文件系统加载器

      TEMPLATES = [{
          'BACKEND': 'django.template.backends.django.DjangoTemplates',
          'DIRS': [os.path.join(BASE_DIR, 'templates')],
      }]
      
    • 应用目录加载器

      TEMPLATES = [{
          'BACKEND': 'django.template.backends.django.DjangoTemplates',
          'APP_DIRS': True,
      }]
      

前端渲染

  1. 前端模板引擎:Handlebars / Mustache。
  2. 前端MV*框架。
    • MVC - AngularJS
    • MVVM - Vue.js

其他视图

  1. 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视频文件
  2. 如何处置生成的内容。

    response['content-type'] = 'application/pdf'
    response['content-disposition'] = 'attachment; filename="xyz.pdf"'
    

    提醒:URL以及请求和响应头中的中文都应该处理成百分号编码。

  3. 生成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

  4. 统计图表。

    • ECharts或Chart.js。
    • 思路:后端只提供JSON格式的数据,前端JavaScript渲染生成图表。

中间件

问题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

内置中间件

  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 - 防范点击劫持攻击

表单

  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中的FormModelForm有什么作用?(通常不用来生成表单主要用来验证数据)

问题2:表单上传文件时应该注意哪些问题?(表单的设置、多文件上传、图片预览、Ajax上传文件、上传后的文件如何存储)

Cookie和Session

问题1:使用Cookie能解决什么问题?(用户跟踪,解决HTTP协议无状态问题)

  1. URL重写
  2. 隐藏域(隐式表单域)
  3. Cookie

问题2:Cookie和Session之间关系是什么?(Session的标识会通过Cookie记录)

Session的配置

  1. Session对应的中间件:django.contrib.sessions.middleware.SessionMiddleware

  2. 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'
      
  3. 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
    
  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的序列化。

    SESSION_SERIALIZER = django.contrib.sessions.serializers.JSONSerializer
    
    • JSONSerializer(默认)- 如果想将自定义的对象放到session中,会遇到“Object of type ‘XXX’ is not JSON serializable”的问题。
    • PickleSerializer(1.6以前的默认,但是因为安全问题不推荐使用,但是只要不去反序列化用户构造的恶意的Payload就行了。关于这种方式的安全漏洞,请参考《Python Pickle的任意代码执行漏洞实践和Payload构造》。)

缓存

配置缓存

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)),
]

其他内容

  1. 模板片段缓存。

    • {% load cache %}
    • {% cache %} / {% endcache %}
  2. 使用底层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',
        },
    }
}

日志配置官方示例。

日志分析

  1. Linux相关命令:head、tail、grep、awk、uniq、sort

    tail -10000 access.log | awk '{print $1}' | uniq -c | sort -r
    
  2. 实时日志文件分析:Python + 正则表达式 + Crontab

  3. 《Python日志分析工具》。

  4. 《集中式日志系统ELK》。

    • 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的重放攻击》。

修改配置文件

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()),
]

使用ViewSet

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

认证用户身份

  1. 利用Django自带的User和djangorestframework-jwt。

  2. 自行对用户及其身份验证的摘要进行处理。

    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
    
  3. 请求的时效性问题。(请求中再加上一个随机的令牌)

访问限流

其他问题

问题1:如何设计一套权限系统?(RBAC或ACL)

  1. RBAC - 基于角色的访问控制(用户-角色-权限,都是多对多关系)。
  2. ACL - 访问控制列表(每个用户绑定自己的访问白名单)。

问题2:如何实现异步任务和定时任务?(Celery)

问题3:如何解决JavaScript跨域获取数据的问题?(django-cors-headers)

问题4:网站图片(水印、剪裁)和视频(截图、水印、转码)是如何处理的?(云存储、FFmpeg)

问题5:网站如何架设(静态资源)文件系统?

Celery的应用

Celery 是一个简单、灵活且可靠的,处理大量消息的分布式系统,并且提供维护这样一个系统的必需工具。它是一个专注于实时处理的任务队列,同时也支持任务调度。

推荐阅读:《Celery官方文档中文版》,上面有极为详细的配置和使用指南。

Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis来实现消息队列服务,前者是更好的选择,它对AMQP(高级消息队列协议)做出了非常好的实现。

  1. 安装RabbitMQ。

    docker pull rabbitmq
    docker run -d -p 5672:5672 --name myrabbit rabbitmq
    docker container exec -it myrabbit /bin/bash
    
  2. 创建用户、资源以及分配操作权限。

    rabbitmqctl add_user luohao 123456
    rabbitmqctl set_user_tags luohao administrator
    rabbitmqctl add_vhost vhost1
    rabbitmqctl set_permissions -p vhost1 luohao ".*" ".*" ".*"
    
  3. 创建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)
    
  4. 启动Celery创建woker(消息的消费者)。

    celery -A fang worker -l info &
    
  5. 执行异步任务。

    @app.task()
    def send_email(from, to, cc, subject, content):
        pass
    
    
    async_result = send_email.delay('', [], [], '', '')
    
    if async_result.ready():
        async_result.get()
    
  6. 创建定时任务。

    from celery.schedules import crontab
    from celery.task import periodic_task
    
    
    @periodic_task(run_every=crontab('*', '12,18'))
    def print_dummy_info():
        print('你妈喊你回家吃饭啦')
    
  7. 启动Celery创建执行定时任务的beat(消息的生产者)。

    celery -A fang beat -l info
    
  8. 检查消息队列状况。

    rabbitmqctl list_queues -p vhost1
    

安全保护

问题1:什么是跨站脚本攻击,如何防范?(对提交的内容进行消毒)

问题2:什么是跨站身份伪造,如何防范?(使用随机令牌)

问题3:什么是SQL注射攻击,如何防范?(不拼接SQL语句,避免使用单引号)

问题4:什么是点击劫持攻击,如何防范?(不允许