Django笔记——05 日常开发中常见的复杂场景

1、遗留系统集成

  • 问题:
    已经有内部系统在运行了,缺少管理功能,希望能有一个权力后台,比如:人事系统,CRM,ERP的产品,缺少部分数据的维护功能

  • 诉求:
    3分钟生成一个管理后台;可以灵活定制页面;不影响正在运行的业务系统


为已有数据库生成管理后台

  • 创建项目: $ django-admin startproject empmanager
  • 编辑 settings.py 中的数据库配置, vim ~/settings.py
DATABASES = { 
'default’: { 
'ENGINE': 'django.db.backends.mysql', 'NAME': 'mydatabase', 
'USER': 'mydatabaseuser', 'PASSWORD': 'mypassword', 'HOST': '127.0.0.1', 'PORT': 
'5432', 
} }
  • 生成 model 类: ./manage.py inspectdb > models.py

  • 创建一个空的项目 empmanager
  • 进入settings.py修改DATABASES ,改成生产环境的数据库
  • 创建一个应用candidates
  • 用python manage.py inspectdb 生成model
  • python manage.py inspectdb > candidates/models.py 导出到models中去
  • 也可以在manage.py的时候指定相应的表,比如:python manage.py inspectdb candidate 回车,就只把candidate这张表打印出来;python manage.py inspectdb candidate,jobsjob 也可以选择多张表

2、Django的中间键(Middleware)

中间键时注入在Django 请求/响应 处理流程中的钩子框架, 能对request/response 作处理


广泛的使用场景:

  • 登陆认证,安全拦截
  • 日志记录,性能上报
  • 缓存处理,监控告警

自定义中间件的2种方法:

  • 使用函数实现
  • 使用类实现

函数实现:

def simple_middleware(get_response):

# One-time configuration and initialization

	def middleware(request):
	
		# Code to be executed for each request before
		# the view (and later middleware) are called
		
		response = get_response(request)
		
		# Code to be executed for each request/response after
		# the view is called
		
		return response
	return middleware

类实现:
Django提供的get_response方法
可能是一个真实的视图,也可能是请求处理链中的下一个中间件

class SimpleMiddleware:
	def __init__(self, get_response):
		self.get_response = get_response
		
		# One-time configuration and initialization
		
	def __call__(self, request):
	
		# Code to be executed for each request before
		# the view (and later middleware) are called

		response = self.get_response(request)
		
		# Code to be executed for each request/response after
		# the view is called
		
		return response

在处理的过程中,既有request的请求,又有最后返回的response对象,在这个过程中可以对request和response进行处理。

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',  # 安全的中间件,做通常的安全拦截处理
    'django.contrib.sessions.middleware.SessionMiddleware',  # Session的中间件,处理用户的登陆信息
    'django.middleware.common.CommonMiddleware',   # Common中间件,常用的处理
    'django.middleware.csrf.CsrfViewMiddleware',  # Csrf中间件,用来处理跨站攻击
    'django.contrib.auth.middleware.AuthenticationMiddleware',   # 处理用户的认证登陆
    'django.contrib.messages.middleware.MessageMiddleware',  # 用来处理消息,当用户操作时候,需要给他什么提示时,用这个把消息传递到用户的页面上
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

3、创建请求日志、性能日志记录的中间件

中间件用来记录用户访问的url,访问url的时候传过来的参数以及url处理过程中耗费了多少时间,记录到日志文件中

  • 定义实现中间件: def performance_logger_middleware(get_response)
  • 记录请求 URL, 参数, 响应时间
  • 注册 middleware 到 settings 中
  • 配置 日志文件路径

1、创建一个文件performance.py

logger = logging.getLogger(__name__)


def performance_logger_middleware(get_response):
    def middleware(request):
        start_time = time.time() # 在处理请求之前把开始时间记录下来
        response = get_response(request)  # get_response对接收到的请求做处理
        duration = time.time() - start_time   # 处理完成后的时间-开始时间得到耗时
        response["X-Page-Duration-ms"] = int(duration * 1000)  # 耗时通过response的头返回出去,在浏览器inspect工具,可以看到浏览器里面页面访问耗时
        logger.info("%s %s %s", duration, request.path, request.GET.dict()) # 记录到日志中
        return response  # 处理完之后吧response返回

    return middleware

2、添加到settings的MIDDLEWARE 里面
中间件是从上到下依次执行的,由于是记录耗时,所以该中间件应该放在第一个位置

MIDDLEWARE = [
    'interview.performance.performance_logger_middleware'
    ...
    ]

3、在settings的LOGGING中添加日志记录配置
interview.performance与performance

# 日志记录
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': { # exact format is not important, this is the minimum information
            'format': '%(asctime)s %(name)-12s %(lineno)d %(levelname)-8s %(message)s',
        },
    },


    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
        },

        'mail_admins': { # Add Handler for mail_admins for `warning` and above
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
        },
        'file': {
            #'level': 'INFO',
            'class': 'logging.FileHandler',
            'formatter': 'simple',
            'filename': os.path.join(BASE_DIR, 'recruitment.admin.log'),
        },

        'performance': {
            #'level': 'INFO',
            'class': 'logging.FileHandler',
            'formatter': 'simple',
            'filename': os.path.join(BASE_DIR, 'recruitment.performance.log'),
        },
    },



    'root': {
        'handlers': ['console', 'file'],
        'level': 'INFO',
    },



    'loggers': {
        "interview.performance": {
            "handlers": ["console", "performance"],
            "level": "INFO",
            "propagate": False,
        },
    },
}

4、在Django中支持多语言

步骤:

  • 代码中使用 gettext, gettext_lazy 函数获取多语言资源对应的文本内容
  • 生成多语言资源文件
  • 翻译多语言内容
  • 生成二进制多语言资源文件

  • Model,以及 Django 的 python 代码里面使用多语言
from django.utils.translation import gettext_lazy  as _
class MyThing(models.Model):
	name = models.CharField(help_text=_('This is the help text'))
  • 生成文本格式的多语言资源文件 .po 文件
    • django-admin makemessages -l zh_HANS -l en
    • 翻译 .po 文件中的内容到不同语言
  • 编译生成可以高效使用的二进制文件 (.mo) 文件
    • django-admin compilemessages

模板导入{% load i18n %}


用translate 指令来获取某一个key对应的资源

{% translate "匠果科技开放职位" %} 

当需要翻译的字符串里面包含变量的时候,或者多段文本的时候,可以使用blocktranslate

{% blocktranslate with user_name=user.username %} 终于等到你 {{ user_name }}, 期待加入我们,用技术去探索一个新世界 {% endblocktranslate %}

生成多语言文件
使用命令之前要确保工程目录下面有一个local文件,local是用来存放多语言资源文件的

在终端运行的:-l来指定语言

django-admin makemessages -l zh_HANS -l en

把多语言编译成二进制格式

django-admin compilemessages

这时会生成po文件


url添加多语言url支持

 path('i18n/', include('django.conf.urls.i18n')),

在settings里面添加多语言的配置

LANGUAGES = [
    ('zh-hans', _('Chinese')),
    ('en', _('English')),
]

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

中间件添加:

'django.middleware.locale.LocaleMiddleware',

在HTML中添加form表单让用户选择语言

{% csrf_token %}

5、防止XSS跨站脚本攻击

Django笔记——05 日常开发中常见的复杂场景_第1张图片


加一个有XSS安全漏洞的页面

'''
    直接返回  HTML 内容的视图 (这段代码返回的页面有 XSS 漏洞,能够被攻击者利用)
'''
def detail_resume(request, resume_id):
    try:
        resume = Resume.objects.get(pk=resume_id)
        content = "name: %s 
introduction: %s
" % (resume.username, resume.candidate_introduction) return HttpResponse(content) except Resume.DoesNotExist: raise Http404("resume does not exist")

在url中:

if settings.DEBUG: # 只有开始DEBUG才将url添加到urlpatterns中去,在生产环境下这个url是访问不到的
    # 有XSS漏洞的视图
    urlpatterns += [
        url(r'^detail_resume/(?P\d+)/$', views.detail_resume, name='detail_resume'),
    ]

XSS攻击过程:
正常用户访问页面,在访问页面填入数据的时候,往里面填入一段JavaScript脚本,当其他用户访问这个人的个人信息时,里面含有这段代码的信息就会自动执行,是以访问者的身份来调用代码
Django笔记——05 日常开发中常见的复杂场景_第2张图片

访问:http://127.0.0.1:8000/detail_resume/4/
Django笔记——05 日常开发中常见的复杂场景_第3张图片


其他视图用的是render函数返回一个页面内容,然后页面里面把请求的request带上,用render时,django会自动把HTML的内容转译,所有的脚本都不会再执行,所以要解决XSS,可以在HttpResponse那里做一个转译

# 该视图用来访问一个简历的详情
def detail_resume(request, resume_id):
    try:
        resume = Resume.objects.get(pk=resume_id)
        content = "name: %s 
introduction: %s
" % (resume.username, resume.candidate_introduction) return HttpResponse(html.escape(content)) except Resume.DoesNotExist: raise Http404("resume does not exist")

6、CSRF跨站请求伪造和SQL注入攻击

  • CSRF(Cross-site request forgery,简称:CSRF 或 XSRF)
  • 恶意攻击者在用户不知情的情况下,使用用户的身份来操作
  • 黑客的准备步骤
    • 黑客创建一个 请求网站 A 类的 URL 的 Web 页面,放在恶意网站 B 中 ,这个文件包含了一个创建
      用户的表单。这个表单加载完毕就会立即进行提交。
    • 黑客把这个恶意 Web 页面的 URL 发送至超级管理员,诱导超级管理员打开这个 Web 页面。

Django笔记——05 日常开发中常见的复杂场景_第4张图片


模拟攻击:

创建一个HTML页面让管理员能够添加HR账号

view里面添加一个create_hr_user的视图

# 这个 URL 仅允许有 创建用户权限的用户访问
@csrf_exempt  # 这个标记表示视图不去处理csrf的攻击
@permission_required('auth.user_add') # 这个页面是有权限创建用户的用户才能进入,表示使用这个用户需要有添加用户的权限
def create_hr_user(request):
    if request.method == "GET":
        return render(request, 'create_hr.html', {}) # 如果是get请求,则将create_hr这个页面展示出来
    if request.method == "POST":  # 如果是post请求
        username = request.POST.get("username") # 取到用户密码
        password = request.POST.get("password")

        hr_group = Group.objects.get(name='HR') # 取到hr这个群组
        user = User(is_superuser=False, username=username, is_active=True, is_staff=True) # 创建一个用户
        user.set_password(password)
        user.save() # 保存创建用户
        user.groups.add(hr_group) # 将用户设置为HR角色

        messages.add_message(request, messages.INFO, 'user created %s' % username) # 添加消息
        return render(request, 'create_hr.html') # 渲染表单
    return render(request, 'create_hr.html')

url

# 管理员创建 HR 账号的 页面:
path('create_hr_user/', views.create_hr_user, name='create_hr_user'),

ps:这段代码有很多额外知识点需要知道,比如权限的标记permission_required,还有用户的创建

 user = User(is_superuser=False, username=username, is_active=True, is_staff=True) # 创建一个用户

还有创建用户添加群组

user.groups.add(hr_group) # 将用户设置为HR角色

还有添加消息

messages.add_message(request, messages.INFO, 'user created %s' % username) # 添加消息

模拟攻击过程:
黑客可以在自己的网站上创建一个页面,这个页面提供一个url,诱导管理员访问这个url,管理员访问这个url的时候,url自动提交,自动提交的时候,url会调用之前的招聘管理系统页面,去直接创建一个账号

Django笔记——05 日常开发中常见的复杂场景_第5张图片

Django笔记——05 日常开发中常见的复杂场景_第6张图片


取消这个标记,并且在HTML的form表单下面添加{% csrf_token %}即可

@csrf_exempt  # 这个标记表示视图不去处理csrf的攻击

视图里面的render方法,带有request,context,会把csrf_token 服务端产生的token传到客户端的HTML页面上,用户在浏览器里提交请求,会自动吧token带回到服务端,服务端收到这个token的时候,会去做校验,校验通过,是服务端产生的,则认为该请求合法。


SQL注入攻击

Django笔记——05 日常开发中常见的复杂场景_第7张图片

7、Django与Celery集成

  • Celery一个分布式的任务队列
    • 简单: 几行代码可以创建一个简单的 Celery 任务
    • 高可用:工作机会自动重试
    • 快速:可以执行一分钟上百万的任务
    • 灵活:每一块都可以扩展

Django笔记——05 日常开发中常见的复杂场景_第8张图片

Celery是一个分布式的任务队列,把大量的任务分布到不同的机器上去,通过集群来运行大量的任务

8、文件图片的上传

场景/目标:

  • 投递简历的页面, 可以上传个人的照片, 以及附件简历
  • 上传的文件存储在服务器上,文件服务可以扩展

存储方案选型

  • 使用服务器本地磁盘
  • 自建分布式文件服务器
  • 阿里云 OSS

使用本地存储的操作过程
1、设置图片、文件存储路径 & URL 映射

  • settings 里面添加 /media 路径, urls.py 中添加图片路径映射

2、 准备 model, form, view 和 HTML 表单模板

  • model 里面添加图片/文件字段(如 个人照片, 个人简历字段到 Resume)
  • form.py 中增加图片,附件字段
  • 创建简历的视图中展示 picture, attachment 字段
  • HTML 表单模板中增加 enctype 属性 (resume_form.html )

3、 变更数据库

4、 Admin里面 添加展示字段, 简历列表中加上照片展示


使用阿里云 OSS 存储:

  • 安装 OSS 库
  • OSS 的依赖添加 django_oss_storage 到 APPS
  • settings 里面添加 OSS 设置

使用本地存储:

在settings:这两个属性是上传图片,上传文件的资源所存放的路径

MEDIA_ROOT =  os.path.join(BASE_DIR, 'media') 
MEDIA_URL = '/media/'

urls:document_root是文档的路径,加到静态资源里去,然后添加到urlpatterns 中来

if settings.DEBUG:
        urlpatterns += static(settings.MEDIA_URL,
                              document_root=settings.MEDIA_ROOT)

model:添加两个字段

picture = models.ImageField(upload_to='images/', blank=True, verbose_name=_('个人照片')) 
attachment = models.FileField(upload_to='file/', blank=True, verbose_name=_('简历附件'))

form.py

class ResumeForm(ModelForm):
    class Meta:
        model = Resume

        fields = ["username", "city", "phone",
        "email", "apply_position", "born_address", "gender", "picture", "attachment",
        "bachelor_school", "master_school", "major", "degree", 
        "candidate_introduction", "work_experience", "project_experience"]       

HTML:enctype指定最后传输的数据的编码格式是用form-data来传输,这样文件图片就能上传了

admin:

# 将picture展示在列表中
def image_tag(self, obj):
    if obj.picture:  # 将picture这个字段做格式化,它是一个url
        return format_html(''.format(obj.picture.url)) # 将拿到的url用HTML封装起来
    return ""
image_tag.allow_tags = True
image_tag.short_description = 'Image'      

OSS 存储:

安装:

pip install django-oss-storage

django-oss-storage添加到应用中

settings:

# 阿里云 CDN 存储静态资源文件 & 阿里云存储上传的图片/文件
# STATICFILES_STORAGE = 'django_oss_storage.backends.OssStaticStorage'

DEFAULT_FILE_STORAGE = 'django_oss_storage.backends.OssMediaStorage' # django会自动把文件存储到oss中

# AliCloud access key ID
OSS_ACCESS_KEY_ID = os.environ.get('OSS_ACCESS_KEY_ID','')
# AliCloud access key secret
OSS_ACCESS_KEY_SECRET = os.environ.get('OSS_ACCESS_KEY_SECRET','')  # 访问oss需要一个AKSK
# The name of the bucket to store files in
OSS_BUCKET_NAME = 'djangorecruit' # 使用哪个BUCKET

# The URL of AliCloud OSS endpoint
# Refer https://www.alibabacloud.com/help/zh/doc-detail/31837.htm for OSS Region & Endpoint
OSS_ENDPOINT = 'oss-cn-beijing.aliyuncs.com'   # 使用的哪边的数据中心

DINGTALK_WEB_HOOK_TOKEN = os.environ.get('DINGTALK_WEB_HOOK_TOKEN','')
DINGTALK_WEB_HOOK = "https://oapi.dingtalk.com/robot/send?access_token=%s" % DINGTALK_WEB_HOOK_TOKEN

9、多数据库路由

  • 多数据库配置
  • 指定数据库表生成 model (inspectdb)
  • 注册到 Admin 中 (running/admin.py)
  • 添加 Router 类 & settings 中配置 Router

settings:

DATABASES = {
    'default': {
        #'ENGINE': 'django.db.backends.sqlite3',
        'ENGINE': 'django_prometheus.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    },
    # 'running': {
    #         'ENGINE': 'django.db.backends.mysql', 
    #         'NAME': 'running',                    
    #         'USER': 'recruitment',                      
    #         'PASSWORD': 'recruitment',                  
    #         'HOST': 'running',
    #         'PORT': '3306',
    # },
}

#DATABASE_ROUTERS = ['settings.router.DatabaseRouter']  #路由的配置

在settings文件夹里面创建的一个py文件:定义了数据库表的路由

class DatabaseRouter:
    route_app_labels = {'running'}  #应用的label

    def db_for_read(self, model, **hints):  #读操作时访问哪个数据库
        if model._meta.app_label in self.route_app_labels:
            return 'running'
        return 'default'

    def db_for_write(self, model, **hints): #写操作时返回哪个数据库
        if model._meta.app_label in self.route_app_labels:
            return 'running'
        return 'default'

    def allow_relation(self, obj1, obj2, **hints): # 是不是允许表之间有相互的关系
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints): # model跟数据库之间允不允许做同步做迁移
        """
        遗留数据库中的表不允许迁移
        """
        if app_label in self.route_app_labels:
            return False
        return True

terminal:

python manage.py inspectdb --database=running --settings=settings.local

10、3行代码支持大数据量的关联外键

Django笔记——05 日常开发中常见的复杂场景_第9张图片
admin:

@admin.register(Country) #相当于注册 admin.site.register
class CountryAdmin(admin.ModelAdmin):
    search_fields = ('chn_name', 'eng_name',)

@admin.register(Province)
class ProvinceAdmin(admin.ModelAdmin):
    search_fields = ('chn_name', 'eng_name',)
    
@admin.register(City)
class CityAdmin(admin.ModelAdmin):
    autocomplete_fields = ['provinceid','countryid',]
    list_display = ('cityid', 'countryid', 'areaid', 'provinceid', 'chn_name', 'eng_name')

多级关联
Django笔记——05 日常开发中常见的复杂场景_第10张图片

11、20行代码实现只读站点ReadOnlyAdmin

  • 成遗留的已有系统
  • 已有系统的数据涉及到核心数据
  • 为了确保数据安全,管理后台只提供数据的浏览功能
  • 设置列表页 list_display 展示所有字段

admin:

class ReadOnlyAdmin(admin.ModelAdmin):
    readonly_fields = []

    def get_list_display(self, request): # 默认所有字段都返回到list_display显示出来
        return [field.name for field in self.model._meta.concrete_fields]

    def get_readonly_fields(self, request, obj=None):
        return list(self.readonly_fields) + \
               [field.name for field in obj._meta.fields] + \
               [field.name for field in obj._meta.many_to_many]  #外键依赖关系

    def has_add_permission(self, request):   # 添加数据的权限
        return False

    def has_delete_permission(self, request, obj=None): # 删除数据的权限
        return False

    def has_change_permission(self, request, obj=None): # 更改数据的权限
        return False

# 继承只读的类
@admin.register(Country) #相当于注册 admin.site.register
class CountryAdmin(ReadOnlyAdmin):
    search_fields = ('chn_name', 'eng_name',)

@admin.register(Province)
class ProvinceAdmin(ReadOnlyAdmin):
    search_fields = ('chn_name', 'eng_name',)
    
@admin.register(City)
class CityAdmin(ReadOnlyAdmin):
    autocomplete_fields = ['provinceid','countryid',]
    list_display = ('cityid', 'countryid', 'areaid', 'provinceid', 'chn_name', 'eng_name')

12、10行代码自动注册所有model到管理后台

主应用,apps.py

class AdminClass(admin.ModelAdmin):
    def __init__(self, model, admin_site):
        # 列表页自动显示所有的字段:
        self.list_display = [field.name for field in model._meta.fields]
        super(AdminClass, self).__init__(model, admin_site)

# automatically register all models
class UniversalManagerApp(AppConfig):
    """
    应用配置在 所有应用的 Admin 都加载完之后执行
    """
    # the name of the AppConfig must be the same as current application
    name = 'recruitment'

    def ready(self):
        models = apps.get_app_config('running').get_models()  # 遍历running下面的所有model
        for model in models:
            try:
                admin.site.register(model, AdminClass)
            except admin.sites.AlreadyRegistered:
                pass

UniversalManagerApp添加到settings的应用中去(是路径)

Django笔记——05 日常开发中常见的复杂场景_第11张图片
可以用type函数动态的定义一个类,name是传入一个名称定义了一个model类名,中间是继承自哪些类

13、Signals信号

  • Signals是Django 的信号
  • Django 框架内置的信号发送器,这个信号发送器在框架里面
  • 有动作发生的时候,帮助解耦的应用接收到消息通知
  • 当动作发生时,允许特定的信号发送者发送消息到一系列的消息接收者
  • Signals 是同步调用

所有的 Signals 都是 django.dispatch.Signal 的实例/子类

  • django.db.models.signals.pre_init 模型实例初始化前
  • django.db.models.signals.post_init 模型实例初始化后
  • django.db.models.signals.pre_save 模型保存前
  • django.db.models.signals.post_save 模型保存后
  • django.db.models.signals.pre_delete 模型删除前
  • django.db.models.signals.post_delete 模型删除后
  • django.db.models.signals.m2m_changed 多对多字段被修改
  • django.core.signals.request_started 接收到 HTTP 请求
  • django.core.signals.request_finished HTTP 请求处理完毕

Django笔记——05 日常开发中常见的复杂场景_第12张图片


自定义信号

1)定义信号: 在项目根目录新建文件self_signal.py

import django.dispatch
my_signal = django.dispatch.Signals(providing_args=["argument1","argument2"])

2)触发信号:业务逻辑中触发信息

from self_signal import my_signal
my_signal.send(sender="Recruitment", argument1=111, argument2=2)

3)注册信号处理器/接收器

from self_signal import my_signal
my_signal.connect(callback_of_my_signal)

14、django常用插件

  • Django debug toolbar : 提供一个可以查看debug 信息的面板(包括SQL执行时间,页面耗时)
  • django-silk :性能瓶颈分析
  • Simple UI:基于Element UI 和 VUE 的 Django Admin 主题
  • Haystack Django :模块化搜索方案
  • Django notifications: 发送消息通知,你有 xx 条未处理简历
  • Django markdown editor :Markdown 编辑器
  • django-crispy-forms : Crispy 表单,以一种非常优雅、干净的方式来创建美观的表单
  • django-simple-captcha:Django表单验证码
  • Django debug toolbar : 提供一个可以查看debug 信息的面板(包括SQL执行时间,页面耗时)
  • https://django-debug-toolbar.readthedocs.io/

你可能感兴趣的:(Django笔记)