python面试题:web框架(五)

五、场景与综合题

  1. 设计一个博客系统的 API:需要哪些资源、接口、权限控制?

    1. 设计一个博客系统的 API 时,我们需要从功能需求出发,抽象出核心资源(Resources),定义对这些资源的操作接口(Endpoints / HTTP Methods),并考虑不同用户角色及其权限控制(Authorization & Authentication)。以下是一个较为完整的设计方案:

      一、核心资源(Resources)

      1. 用户(User)
        • 属性:id, username, email, password_hash, avatar_url, bio, created_at, updated_at
      2. 文章(Post)
        • 属性:id, title, content, excerpt, cover_image_url, author_id (FK), status (draft/published), tags[], created_at, updated_at
      3. 分类/标签(Category / Tag)
        • Category: id, name, description
        • Tag: id, name
      4. 评论(Comment)
        • 属性:id, post_id (FK), user_id (nullable for guests), parent_comment_id (for nested comments), content, created_at
      5. 点赞/收藏(Like / Favorite)
        • Like: id, user_id, post_id, created_at
        • Favorite: id, user_id, post_id, created_at
      6. 媒体文件(Media)【可选】
        • 属性:id, url, uploaded_by (user_id), type (image/video), created_at

      二、API 接口设计(RESTful 风格示例)

      1. 用户相关接口
      方法 路径 描述 权限
      POST /api/auth/register 注册 公开
      POST /api/auth/login 登录 公开
      GET /api/users/ 获取用户信息 公开或需授权
      PUT /api/users/ 更新用户信息 本人或管理员
      DELETE /api/users/ 删除用户 管理员
      1. 文章相关接口
      方法 路径 描述 权限
      GET /api/posts 获取文章列表(支持分页、过滤) 公开
      GET /api/posts/ 获取单篇文章详情 公开
      POST /api/posts 创建新文章 已登录用户
      PUT /api/posts/ 更新文章 作者或管理员
      DELETE /api/posts/ 删除文章 作者或管理员
      GET /api/users/{userId}/posts 获取某用户的文章列表 公开
      1. 分类/标签接口
      方法 路径 描述 权限
      GET /api/categories 获取所有分类 公开
      POST /api/categories 创建分类 管理员
      GET /api/tags 获取所有标签 公开
      POST /api/tags 创建标签 管理员
      1. 评论接口
      方法 路径 描述 权限
      GET /api/posts/{postId}/comments 获取文章评论列表 公开
      POST /api/comments 添加评论 已登录用户或游客(可选)
      PUT /api/comments/ 修改评论 评论作者或管理员
      DELETE /api/comments/ 删除评论 作者、文章作者、管理员
      1. 点赞与收藏接口
      方法 路径 描述 权限
      POST /api/posts/{postId}/like 点赞 已登录用户
      DELETE /api/posts/{postId}/like 取消点赞 已登录用户
      POST /api/posts/{postId}/favorite 收藏 已登录用户
      DELETE /api/posts/{postId}/favorite 取消收藏 已登录用户
      GET /api/users/{userId}/favorites 查看用户收藏列表 本人或公开设置允许
      1. 媒体上传接口(可选)
      方法 路径 描述 权限
      POST /api/media/upload 上传图片或文件 已登录用户

      三、权限控制设计(Authentication & Authorization)

      1. 身份认证方式(Authentication)
      • 使用 JWT(JSON Web Token)或 OAuth 2.0
      • 登录后返回 access_token,用于后续请求的身份验证(放在 HTTP Header: Authorization: Bearer
      1. 常见用户角色及权限(Authorization)
      角色 权限说明
      游客(Guest) 浏览文章、评论(若开放)、搜索
      注册用户(User) 发布文章、编辑自己的文章、评论、点赞、收藏、管理个人资料
      作者(Author) User 权限 + 管理自己所有内容(包括评论审核)
      管理员(Admin) 所有权限,包括管理用户、分类、标签、审核内容、删除任意文章或评论
      1. 示例权限逻辑
      • 创建文章:需认证(USER 或以上)
      • 修改文章:仅限作者或管理员
      • 删除评论:评论者本人、文章作者或管理员可删
      • 访问用户私有信息:仅本人或管理员可见

      四、其他建议

      1. 支持过滤、排序、分页
        • 如:GET /api/posts?status=published&tag=tech&page=1&limit=10&sort=-created_at
      2. 支持草稿状态
        • 只有作者和管理员能查看草稿状态的文章
      3. 安全性
        • 密码加密存储(如使用 bcrypt)
        • 防止 XSS/CSRF(前端渲染转义、后端校验输入)
        • 对上传文件做类型和大小限制
      4. 限流和防刷
        • 对频繁请求如登录、评论等接口加入限流机制(Rate Limiting)

      五、扩展功能(进阶)

      • 支持多语言内容
      • 富文本编辑器集成(如 Markdown、Quill)
      • 实时评论(WebSocket)
      • 第三方登录(Google, GitHub)
      • 后台管理系统 API(供 CMS 使用)

      总结

      一个典型的博客系统 API 至少应包括用户、文章、评论、点赞收藏等模块,通过 RESTful 设计清晰的资源接口,同时引入基于角色的权限控制确保数据安全。设计时兼顾易用性、安全性和扩展性,可满足大多数中小型博客或个人创作平台的需求。

  2. 如果前端需要同时获取文章列表和每篇文章的作者信息,如何避免 N+1 查询?

    1. 在 Django 学习中,N+1 查询是常见性能问题——尤其在序列化关联数据时,如果不提前预加载关联对象,会导致每遍历一篇文章就额外查询一次数据库来获取作者信息,最终形成 1 次查文章 + N 次查作者 = N+1 次查询。下面我们从理论到实践讲如何避免。


      一、理论:N+1 查询的成因与解决方案

      1. 成因

      假设你的模型关系是:

      class Post(models.Model):
          title = models.CharField(max_length=200)
          content = models.TextField()
          author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
          # 其它字段...
      

      在前端需要一次性拿到文章列表以及每篇文章的作者信息时,如果你直接这样写:

      posts = Post.objects.all()
      serializer = PostSerializer(posts, many=True)
      

      PostSerializer里引用了 author(比如输出 author.username),Django ORM 默认不会一次性把作者数据查出来,而是在序列化每条记录时触发一次额外的 SQL 查询 → N+1 问题


      Django 提供两种主要方法来减少查询次数:

      • select_related:用于 ForeignKey / OneToOne单次 JOIN 查询,适合“一”的那方数据量小且一定会用到的情况。
      • prefetch_related:用于 ManyToMany / ForeignKey 反向(多)两次查询 + Python 合并,适合“多”的那方可能很大,但希望批量预加载的情况。

      对于 文章 → 作者(ForeignKey,一对一关系),应使用 select_related


      二、例子:避免 N+1 的正确写法

      1. View 层优化查询

      from django.shortcuts import render
      from rest_framework.generics import ListAPIView
      from .models import Post
      from .serializers import PostSerializer
      
      class PostListView(ListAPIView):
          serializer_class = PostSerializer
          
          def get_queryset(self):
              # 关键:使用 select_related 把 author 表 JOIN 进来
              return Post.objects.select_related('author').filter(status='published')
      

      原理select_related('author')会在同一条 SQL 中 JOIN User 表,把作者的所有字段一起取出,这样序列化时不会再触发额外查询。


      2. Serializer 正常写即可

      from rest_framework import serializers
      from .models import Post
      
      class PostSerializer(serializers.ModelSerializer):
          author_username = serializers.ReadOnlyField(source='author.username')
          author_avatar = serializers.ImageField(source='author.avatar_url', allow_null=True)
      
          class Meta:
              model = Post
              fields = [
                  'id', 'title', 'excerpt', 'cover_image_url',
                  'author_username', 'author_avatar',
                  'created_at'
              ]
      

      这里 source='author.username'不会引发新查询,因为 author对象已经在 select_related时预加载到内存。


      3. 如果是嵌套更复杂的场景(例如文章 → 多个标签)

      标签是 ManyToMany 关系:

      class Post(models.Model):
          ...
          tags = models.ManyToManyField('Tag', related_name='posts')
      

      此时要同时避免文章和标签的 N+1:

      from django.db.models import Prefetch
      
      class PostListView(ListAPIView):
          serializer_class = PostSerializer
          
          def get_queryset(self):
              return Post.objects.select_related('author').prefetch_related(
                  Prefetch('tags', queryset=Tag.objects.only('id', 'name'))
              ).filter(status='published')
      
      • select_related('author')解决作者 N+1
      • prefetch_related(Prefetch(...))解决标签 N+1

      三、验证是否消除 N+1

      在 Django shell 或调试工具中开启 SQL 日志:

      python manage.py shell
      >>> from django.db import connection
      >>> from app.models import Post
      >>> posts = Post.objects.select_related('author').all()
      >>> print(len(connection.queries))  # 应该只有 1 条 SQL
      

      或在 DRF 的 Browsable API 页面查看网络请求的 DB 查询数。


      四、小结(适用于 90% 场景的要点)

      • ForeignKey / OneToOne 场景 → 用 select_related
      • ManyToMany / 反向 ForeignKey 场景 → 用 prefetch_related
      • View 的 get_queryset 中完成预加载,而不是在 Serializer 里临时查数据库
      • 预加载还能配合 only()/ defer()只取需要的字段,进一步减少数据传输和内存占用

      这样即可在 Django 中高效解决前端一次性获取文章列表及作者信息的 N+1 查询问题。

  3. 高并发场景下,Django + DRF 的性能瓶颈可能出现在哪里?如何优化?

    1. 在 Django + DRF 的学习中,高并发场景的性能瓶颈往往集中在几个典型位置。我们按“理论分析 + 例子”来梳理 90% 场景下会遇到的瓶颈及对应优化思路,帮助你抓住重点。


      一、理论:高并发下常见的性能瓶颈点

      1. WSGI 服务器与同步阻塞模型

      • Django 默认是同步框架,运行在 WSGI(如 gunicorn + sync worker)时,每个请求占用一个线程/进程,IO 等待会阻塞整个 worker。
      • 高并发下,worker 很快被占满 → 响应变慢、超时甚至拒绝服务。

      2. 数据库查询与 ORM 开销

      • 未优化的 ORM 调用(尤其是 N+1 查询、无索引查询、大结果集)会让 DB 成为瓶颈。
      • 数据库连接池不足或长事务导致连接数耗尽。

      3. 序列化过程的 CPU/内存消耗

      • DRF 的 Serializer 在大数据量或深层嵌套时,序列化过程会占用大量 CPU 与内存。
      • 重复计算、未缓存的结果会加重负担。

      4. 静态文件与媒体文件的低效处理

      • 开发环境用 runserver 直接 serve 静态资源,生产环境若未交给 CDN/Nginx,会浪费应用服务器资源。

      5. 缓存缺失

      • 热点数据(如文章列表、用户信息)每次都查库,没有利用缓存,导致 DB 压力骤增。

      6. 外部服务调用的同步阻塞

      • 请求第三方 API(支付、短信、搜索)若采用同步调用,会拖慢整体响应时间。

      二、例子与优化方案

      1. 瓶颈①:同步阻塞模型 → 优化为异步或增加并发能力

      理论:Gunicorn 默认 sync worker 在高并发 IO 场景容易塞满。

      优化

      • 使用 ASGI + Uvicorn(Django 3.1+ 支持异步视图)处理高并发 IO 请求。

      • 或者保持 WSGI,但调整 Gunicorn 的 worker 数量与类型(如 gevent、uvicorn workers):

        gunicorn myproject.wsgi:application --workers 4 --worker-class gevent --worker-connections 1000
        
      • 使用 Nginx 做反向代理,缓冲慢客户端,提升并发吞吐。

      例子

      博客 API 有大量读请求时,将 Gunicorn 换成 gevent异步 worker,可让单进程同时处理更多请求,减少线程切换开销。


      2. 瓶颈②:数据库查询 → 优化查询与连接池

      理论:高并发读/写时,未优化的 ORM 会压垮 DB。

      优化

      • 避免 N+1(见上题 select_relatedprefetch_related)。
      • 给常用过滤字段加索引(db_index=Trueindex_together)。
      • 使用 数据库连接池(如 django-db-geventpoolpgbouncer)减少连接建立开销。
      • 对统计类、复杂报表类请求,使用 只读副本(Read Replica)分流。

      例子

      文章列表接口 GET /api/posts在并发下慢,发现缺少 (status, created_at)复合索引 → 加上索引后查询耗时下降 80%。


      3. 瓶颈③:序列化开销 → 精简结构与缓存结果

      理论:DRF 序列化大列表或深层嵌套会吃 CPU。

      优化

      • 扁平化返回结构,避免不必要的嵌套(只返回前端需要的字段)。

      • 使用 Serializer 的 only/defer思想(即自定义字段而非全量)。

      • 对热点接口启用 结果缓存(如 Redis):

        from django.core.cache import cache
        class PostListView(ListAPIView):
            def list(self, request, *args, **kwargs):
                key = "post_list_published"
                data = cache.get(key)
                if not data:
                    qs = self.get_queryset()
                    serializer = self.get_serializer(qs, many=True)
                    data = serializer.data
                    cache.set(key, data, timeout=60)  # 缓存 60 秒
                return Response(data)
        

      例子

      首页文章列表每天访问量巨大,缓存 30 秒后 DB 读请求下降 70%。


      4. 瓶颈④:静态/媒体资源 → 使用 CDN 与 Nginx

      理论:Django 直接 serve 静态文件会占用应用服务器资源。

      优化

      • 生产环境关闭 DEBUG,用 Nginx 直接托管静态文件和媒体文件
      • 将图片、视频放到 CDN,减轻源站带宽压力。

      例子

      博客文章封面图访问慢,迁移到阿里云 OSS + CDN,加载时间从 500ms 降到 50ms。


      5. 瓶颈⑤:缓存缺失 → 多层缓存策略

      理论:热点数据不命中缓存会放大 DB 压力。

      优化

      • 使用 Redis / Memcached 做视图缓存、对象缓存。
      • 对频繁计算的查询结果做 模板/片段缓存(DRF 可用 @method_decorator(cache_page(60)))。

      例子

      文章详情页访问量高,使用 @cache_page(60)缓存整页响应,DB 读降低 90%。


      6. 瓶颈⑥:外部服务同步调用 → 异步化或队列化

      理论:同步调用第三方 API 会拉长请求链路。

      优化

      • 改为 异步任务队列(Celery + Redis/RabbitMQ)处理非实时任务(如发邮件、生成缩略图)。
      • 使用 异步 HTTP 客户端(如 httpx + asyncio)并行请求多个外部服务。

      例子

      用户发布文章后发送 Slack 通知,改用 Celery 异步执行,文章发布接口响应时间从 300ms 降到 50ms。


      三、高并发优化速查表(90% 场景适用)

      瓶颈类别 优化手段 关键词
      请求处理模型 ASGI / 异步 worker / Nginx 代理 gevent, uvicorn, nginx
      数据库 索引、select_related、只读副本、连接池 prefetch_related, db_index, read replica
      序列化 精简字段、缓存序列化结果 serializer field limit, cache_page
      静态资源 CDN、Nginx 托管 static_root, media_root, CDN
      缓存 Redis/Memcached、页面缓存 cache_page, redis
      外部调用 异步任务队列、并行请求 Celery, httpx async

      四、小结

      在 Django + DRF 的高并发场景中,瓶颈通常出现在同步阻塞、数据库查询、序列化开销、缓存缺失与外部服务调用。优化思路围绕:

      1. 提升并发处理能力(异步/多 worker)
      2. 减少慢查询(ORM 优化 + 索引)
      3. 降低序列化与传输成本(精简结构 + 缓存)
      4. 分流静态资源与热点数据(CDN + 缓存层)
      5. 解耦耗时任务(异步队列)

      掌握这些 90% 场景下会碰到的优化点,就能显著提升博客或其他业务 API 的并发性能。

  4. 如何实现一个支持多语言的 API(国际化 i18n)?

    1. 实现支持多语言的 API(国际化,常称 i18n)是现代 Web 应用的常见需求,尤其是在面向全球用户的博客系统或 SaaS 产品中。下面我将从「设计思路」「技术实现」「Django 中的做法」三个层面,详细讲解如何实现一个支持多语言的 API。


      一、设计思路:多语言支持的两种常见方式

      在设计 API 时,有两种主流方式来实现国际化(i18n):

      1. 基于请求头或参数的语言识别(推荐用于 API)

      客户端在每次请求中指定语言(通过 Accept-Languageheader 或 query/path 参数)

      服务端根据该标识返回对应语言的内容

      优点:灵活、符合 REST API 规范,适合前后端分离项目

      ✅ 示例:

      GET /api/posts
      Accept-Language: zh-CN
      

      GET /api/posts?lang=en
      

      2. 基于 URL 路径或子域名

      /en/api/posts, /zh/api/postsen.example.com/api/posts

      更适合网站(Web 多语言站点),API 场景下稍显繁琐


      二、技术实现方案(通用)

      核心要素:

      语言标识:使用标准 locale 格式,如 en, en-US, zh-Hans-CN, fr

      翻译内容存储

       方案 A:**字段级多语言**(每个语言存一份字段)→ 简单直观
      
       方案 B:**独立翻译表**(主表 + 翻译表)→ 更规范,适合复杂系统
      

      语言检测:从 Header / Query / Cookie 中获取

      API 响应:根据语言返回对应文案和数据


      三、Django 中的多语言 API 实现(DRF)

      Django 原生支持 i18n(django.utils.translation+ gettext_lazy),但主要用于模板。对于 API 场景,我们更关注结构化数据的多语言字段,推荐以下两种做法:


      ✅ 方案一:字段级多语言(适合中小项目)

      1. 模型设计:每个字段存不同语言版本

      # models.py
      class Post(models.Model):
          title_en = models.CharField(max_length=200)
          title_zh = models.CharField(max_length=200)
          content_en = models.TextField()
          content_zh = models.TextField()
          status = models.CharField(max_length=10, default='published')
          created_at = models.DateTimeField(auto_now_add=True)
      
          def get_title(self, lang='en'):
              return getattr(self, f'title_{lang}', self.title_en)
      
          def get_content(self, lang='en'):
              return getattr(self, f'content_{lang}', self.content_en)
      

      2. 序列化器根据语言动态返回字段

      # serializers.py
      class PostSerializer(serializers.ModelSerializer):
          title = serializers.SerializerMethodField()
          content = serializers.SerializerMethodField()
      
          class Meta:
              model = Post
              fields = ['id', 'title', 'content', 'created_at']
      
          def get_title(self, obj):
              lang = self.context['request'].query_params.get('lang', 'en')
              return obj.get_title(lang)
      
          def get_content(self, obj):
              lang = self.context['request'].query_params.get('lang', 'en')
              return obj.get_content(lang)
      

      3. 视图中传递语言上下文

      # views.py
      class PostListView(generics.ListAPIView):
          queryset = Post.objects.filter(status='published')
          serializer_class = PostSerializer
      
          def get_serializer_context(self):
              context = super().get_serializer_context()
              context['lang'] = self.request.query_params.get('lang', 'en')
              return context
      

      💡 注意:上面例子中,Serializer是通过 self.context['request']拿语言参数。也可统一用中间件提取语言并设置到 request.lang


      ✅ 方案二:使用 django-modeltranslation(自动生成多语言字段)

      这是 Django 社区最流行的多语言插件之一。

      1. 安装

      pip install django-modeltranslation
      

      2. 配置 settings.py

      INSTALLED_APPS += ['modeltranslation']
      

      3. 创建翻译配置(admin.py 同级新建 translation.py

      # translation.py
      from modeltranslation.translator import register, TranslationOptions
      from .models import Post
      
      @register(Post)
      class PostTranslationOptions(TranslationOptions):
          fields = ('title', 'content')  # 这些字段会自动生成 title_en, title_zh 等
      

      4. 生成迁移文件并迁移

      python manage.py makemigrations
      python manage.py migrate
      

      → Django 会自动为每个语言添加字段(如 title_en, title_zh

      5. 序列化器使用 TranslatedFieldsField(或手动构造)

      Modeltranslation 不直接支持 DRF,但我们可以自定义序列化逻辑:

      # serializers.py
      class PostSerializer(serializers.ModelSerializer):
          title = serializers.SerializerMethodField()
          content = serializers.SerializerMethodField()
      
          class Meta:
              model = Post
              fields = ['id', 'title', 'content', 'created_at']
      
          def get_title(self, obj):
              lang = self.context['request'].query_params.get('lang', 'en')
              return getattr(obj, f'title_{lang}', obj.title)
      
          def get_content(self, obj):
              lang = self.context['request'].query_params.get('lang', 'en')
              return getattr(obj, f'content_{lang}', obj.content)
      

      ✅ 优点:自动管理多语言字段,无需手写 getter;缺点:需迁移数据库结构。


      ✅ 方案三:独立翻译表(适合大型系统)

      模型设计:

      class Post(models.Model):
          status = models.CharField(max_length=10)
          created_at = models.DateTimeField(auto_now_add=True)
      
      class PostTranslation(models.Model):
          post = models.ForeignKey(Post, related_name='translations', on_delete=models.CASCADE)
          language = models.CharField(max_length=10)  # e.g., 'en', 'zh'
          title = models.CharField(max_length=200)
          content = models.TextField()
      

      查询时预加载翻译:

      Post.objects.prefetch_related('translations')
      

      序列化器根据语言筛选:

      def get_title(self, obj):
          lang = self.context['lang']
          translation = obj.translations.filter(language=lang).first()
          return translation.title if translation else None
      

      ✅ 优点:结构清晰、支持无限语言、易于维护;❌ 缺点:查询更复杂,需手动 join 或 prefetch。


      四、语言检测与默认策略

      在中间件或视图中统一提取语言:

      # middleware.py
      class LanguageMiddleware:
          def __init__(self, get_response):
              self.get_response = get_response
      
          def __call__(self, request):
              lang = (
                  request.GET.get('lang') or
                  request.META.get('HTTP_ACCEPT_LANGUAGE', '').split(',')[0].lower() or
                  'en'
              )
              request.lang = lang.split('-')[0]  # 简化为 en/zh/fr
              response = self.get_response(request)
              return response
      

      注册到 MIDDLEWARE

      MIDDLEWARE = [
          # ...
          'yourapp.middleware.LanguageMiddleware',
      ]
      

      然后在视图中通过 request.lang获取语言。


      五、最佳实践建议

      方面 建议
      语言标识 使用 ISO 639-1 代码(en, zh, fr),避免自定义
      默认语言 设置 fallback(如 en
      API 文档 在 Swagger/OpenAPI 中注明 lang参数
      缓存 对多语言接口做缓存时注意区分语言键:cache_key = f"posts_{lang}"
      内容安全 避免用户生成内容直接翻译,需审核或使用专业服务

      六、总结

      实现支持多语言的 API,关键在于:

      1. 选择适合的多语言数据存储方式(字段级 / modeltranslation / 独立表)
      2. 通过请求参数或 Header 识别语言
      3. 在序列化层动态返回对应语言内容
      4. 使用中间件统一管理语言上下文
      5. 注意缓存、默认语言和错误处理

      对于 Django + DRF 项目:

      • 小项目 → 字段级多语言(简单直接)
      • 中型项目 → django-modeltranslation(自动化)
      • 大型系统 → 独立翻译表(灵活可扩展)

      掌握这些方法,你就能轻松构建出面向全球用户的国际化 API。

  5. 用户登录后 token 被盗,如何增强安全性?

    1. 在 Django + DRF 的学习过程中,用户登录后 token 被盗是一个必须考虑的安全风险。虽然我们无法做到 100% 杜绝泄露,但在 90% 的实际场景下,可以通过一系列分层防御来增强安全性,降低被盗后的危害。下面按照理论 + 例子的逻辑展开。


      一、理论:Token 被盗的风险来源与防御思路

      1. Token 被盗的常见途径

      • 前端存储不安全(如 localStorage 被 XSS 读取)
      • 网络被窃听(HTTP 明文传输)
      • 客户端设备中毒或被恶意软件截取
      • 被钓鱼或中间人攻击截获
      • 过期时间过长导致窗口期长

      2. 防御的核心原则

      • 缩短 token 有效期 → 降低被盗后的可用时间
      • 绑定 token 与设备或 IP → 被盗也难直接用
      • 安全存储与传输 → 防止被脚本或网络窃取
      • 可主动撤销 token → 被盗后可立即作废
      • 监控异常使用 → 快速发现并响应

      二、增强安全性的具体做法(90% 场景适用)

      1. 使用 HTTPS(强制加密传输)

      理论:Token 在网络中以明文传输会被中间人拦截。

      实践

      • 部署 SSL/TLS 证书,强制所有 API 走 https://
      • Django 设置 SECURE_SSL_REDIRECT = True强制跳转 HTTPS
      • Nginx/负载均衡层终止 TLS 也可,但要保证内部通信安全

      例子

      # settings.py
      CSRF_COOKIE_SECURE = True
      SESSION_COOKIE_SECURE = True
      SECURE_SSL_REDIRECT = True
      

      2. 缩短 Access Token 有效期 + Refresh Token 机制

      理论:JWT Access Token 一旦签发无法单独作废,有效期越长风险越大。

      实践

      • Access Token 设为短期有效(如 5~15 分钟)
      • 使用 Refresh Token(长期有效)换取新的 Access Token
      • Refresh Token 存储在 HttpOnly + Secure 的 Cookie 中,防止 JS 读取

      例子(DRF + Simple JWT):

      # settings.py
      from datetime import timedelta
      SIMPLE_JWT = {
          "ACCESS_TOKEN_LIFETIME": timedelta(minutes=10),
          "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
          "ROTATE_REFRESH_TOKENS": True,  # 刷新时旧 token 作废
          "BLACKLIST_AFTER_ROTATION": True,
      }
      

      前端用 Access Token 调业务 API,快过期时用 Refresh Token 换新的 Access Token;Refresh Token 被偷的难度更高(HttpOnly Cookie)。


      3. 安全存储 Token(防 XSS)

      理论:XSS 可读取 localStoragesessionStorage里的 token。

      实践

      • Access Token 可放内存(前端变量),避免持久化到易被读取的位置
      • Refresh TokenHttpOnly; Secure; SameSite=Strict的 Cookie,JS 无法访问
      • 避免把 JWT 直接放到 URL 参数中(会记录在日志、浏览器历史)

      例子(Set-Cookie 响应):

      Set-Cookie: refresh_token=xxxx; HttpOnly; Secure; SameSite=Strict; Path=/api/token/refresh/
      

      4. Token 绑定设备/IP(增加盗用难度)

      理论:即使 token 被盗,在不同设备/IP 上使用可被检测并拒绝。

      实践

      • 登录时记录 device_id(前端生成 UUID 并随登录请求发)或 IP
      • 在 token payload 中加入 device_id/ ipclaim
      • 每次请求校验 token 里的信息与当前请求一致,否则拒绝

      例子(Simple JWT 自定义 claims):

      from rest_framework_simplejwt.tokens import RefreshToken
      
      def generate_tokens_for_user(user, device_id):
          refresh = RefreshToken.for_user(user)
          refresh['device_id'] = device_id
          return {
              'access': str(refresh.access_token),
              'refresh': str(refresh),
          }
      

      在中间件或 DRF authentication 类中校验 device_id一致性。


      5. 黑名单机制(主动吊销)

      理论:JWT 本身无法单独失效,但可在服务端维护一个黑名单(如 Redis)实现吊销。

      实践

      • 用户退出登录或检测到异常 → 把当前 token 加入黑名单
      • 每次请求校验是否在黑名单内
      • 结合 Refresh Token 轮换可自动清理旧 token

      例子(Simple JWT + blacklist app):

      pip install djangorestframework-simplejwt[blacklist]
      

      settings.py:

      INSTALLED_APPS += ['rest_framework_simplejwt.token_blacklist']
      SIMPLE_JWT = {
          ...
          "BLACKLIST_AFTER_ROTATION": True,
      }
      

      退出登录时:

      from rest_framework_simplejwt.token_blacklist.models import OutstandingToken, BlacklistedToken
      
      def logout_user(user):
          for token in OutstandingToken.objects.filter(user=user):
              try:
                  BlacklistedToken.objects.get_or_create(token=token)
              except:
                  pass
      

      6. 异常检测与风控

      理论:及时发现异常使用并阻断可降低损失。

      实践

      • 检测同一账号短时间多地登录 → 强制下线旧会话
      • 检测短时间内大量请求 → 限流或验证码挑战
      • 登录成功后向用户发送通知(邮件/短信)

      例子

      登录接口记录 last_login_ipdevice_id,若与上一次差异过大,则要求二次验证。


      三、安全加固速查表(90% 场景)

      安全措施 作用 关键词
      HTTPS 强制 防网络窃听 SECURE_SSL_REDIRECT
      Access Token 短期 + Refresh Token 缩小被盗窗口 SIMPLE_JWT lifetime
      Refresh Token HttpOnly Cookie 防 XSS 窃取 Set-Cookie HttpOnly
      Token 绑定 device_id / IP 增加盗用难度 custom claims
      黑名单机制 主动吊销 blacklist token
      异常检测 & 通知 快速响应 login IP 检查

      四、小结

      当用户的登录 token 被盗时,仅靠单一手段难以完全防范,但我们可以通过 HTTPS、短期 Access Token、HttpOnly Refresh Token、绑定设备信息、黑名单机制、异常监控 等多层防护,在 90% 场景下显著降低风险与损失。

      在 Django + DRF 项目中,结合 djangorestframework-simplejwt与自定义校验逻辑,可以快速落地这些安全增强措施,让 API 更加可靠。

  6. 描述一次你解决过的生产环境 Django/DRF Bug 的过程(考察排查思路)。

    1. 好的,我来模拟一次我在学习与实际项目中遇到并解决的 生产环境 Django/DRF Bug 的完整过程,重点突出排查思路、工具使用和解决逻辑,这也是面试官常考察的能力。


      🐞 背景与问题描述

      我在一个基于 Django + DRF 的博客系统中负责后端开发与运维。某天晚上收到运维报警:

      现象:用户反馈「文章列表接口 /api/posts在高峰期偶尔返回 500 错误」,监控系统显示该接口在晚 8 点左右出现间歇性 5xx 错误率飙升,持续约 20 分钟后恢复正常。

      我的任务是定位根因并修复,避免再次发生。


      🔍 第一步:复现与初步信息收集

      1. 查看错误日志

      • 登录服务器,查看 Django 错误日志(error.log):

        Internal Server Error: /api/posts
        Traceback (most recent call last):
          ...
          File "apps/blog/models.py", line 45, in get_posts
            return Post.objects.select_related('author').prefetch_related('tags')[start:end]
          File ".../django/db/models/query.py", line 935, in __getitem__
            return self._fetch_all()[offset:offset + size]
          File ".../django/db/models/query.py", line 1249, in _fetch_all
            self._result_cache = list(self._iterable_class(self))
        MemoryError
        
      • 关键信息MemoryError→ 内存耗尽导致进程崩溃。

      2. 确认发生时间与负载情况

      • 查看监控(Prometheus + Grafana):
        • 晚 8 点流量突增(用户集中阅读晚间推送文章)
        • 数据库 CPU 正常,但 Django 工作进程内存使用飙升到上限(容器 OOM 或进程被杀)

      🧠 第二步:定位可能原因(排查思路)

      可能方向:

      1. N+1 查询? → 但日志显示用了 select_relatedprefetch_related,SQL 次数不多。
      2. 查询返回数据量过大? → 检查分页逻辑。
      3. 序列化时构造了大对象? → 检查 Serializer 是否做了 heavy 操作。
      4. 缓存失效导致全量查库? → 检查缓存层。

      🔬 第三步:深入分析代码与数据

      1. 检查视图与分页

      # views.py
      class PostListView(generics.ListAPIView):
          serializer_class = PostSerializer
      
          def get_queryset(self):
              return Post.objects.select_related('author')\
                                .prefetch_related('tags')\
                                .filter(status='published')
      
      • 使用了分页:PageNumberPagination,默认 page_size=20→ 看起来没问题?

      2. 检查前端请求参数

      • 发现部分请求带 ?page=1&page_size=1000(测试人员或爬虫?)
      • 问题根源浮现page_size未限制最大值,导致一次性查询 1000 篇文章并序列化 → 内存爆炸。

      3. 检查 Serializer 是否有额外开销

      class PostSerializer(serializers.ModelSerializer):
          author_name = serializers.CharField(source='author.username')
          tags_list = serializers.StringRelatedField(source='tags', many=True)
          excerpt = serializers.SerializerMethodField()
      
          def get_excerpt(self, obj):
              return obj.content[:100]  # 看似轻量,但 obj.content 是大文本字段
      
      • obj.contentTextField,长度可能几千字,[:100]虽切片,但仍需先加载整个字段到内存。
      • page_size=1000时,1000 个大文本字段同时加载 → 内存激增。

      🛠️ 第四步:制定解决方案

      1. 限制分页大小(紧急修复)

      在 DRF 分页类中设置最大 page_size:

      # pagination.py
      class StandardResultsSetPagination(PageNumberPagination):
          page_size = 20
          page_size_query_param = 'page_size'
          max_page_size = 100  # 防止恶意或误操作请求过大
      

      2. 优化查询:避免加载大字段

      • 使用 .only()排除 content等大字段:
      def get_queryset(self):
          return Post.objects.select_related('author')\
                            .prefetch_related('tags')\
                            .only('id', 'title', 'excerpt', 'author__username', 'created_at')\
                            .filter(status='published')
      
      • excerpt单独存储(冗余字段),避免实时截取 content

      3. 加强缓存

      • 文章列表加入缓存(Redis),减少数据库压力:
      def list(self, request, *args, **kwargs):
          cache_key = f"posts_list:{request.query_params}"
          cached = cache.get(cache_key)
          if cached:
              return Response(cached)
          response = super().list(request, *args, **kwargs)
          cache.set(cache_key, response.data, timeout=60)
          return response
      

      4. 增加监控与告警

      • /api/posts接口的 page_size参数值做日志记录与异常告警。
      • 监控单个请求的内存使用(如 Sentry memory profiling)。

      ✅ 第五步:验证与预防

      1. 灰度发布修复代码

      • 先在测试环境模拟 page_size=1000请求,确认不再出现 MemoryError
      • 生产环境分批部署,观察内存曲线恢复正常。

      2. 补充单元测试与文档

      • 添加测试用例:请求 page_size=2000应返回 400 错误。
      • 更新 API 文档,注明 page_size最大 100。

      📌 总结:排查思路复盘

      步骤 关键动作 工具/方法
      1. 现象收集 看日志、监控、错误码 ELK、Grafana、Sentry
      2. 定位方向 从日志找异常(MemoryError) 回溯堆栈
      3. 代码审查 检查分页、查询、序列化 IDE、Debug Toolbar
      4. 根因确认 发现 page_size 无限制 + 大字段加载 请求分析、DB 监控
      5. 修复验证 限制参数、优化查询、加缓存 单元测试、压测
      6. 预防 监控告警、文档完善 Prometheus AlertManager

      🎯 面试回答技巧

      你可以这样说:

      “我曾遇到生产环境文章列表接口间歇性 500 错误的问题。首先通过日志发现是 MemoryError,结合监控看到内存飙升。排查发现是因为分页参数 page_size没有上限,导致一次性查询大量文章并加载大文本字段,引发内存溢出。我通过限制最大分页大小、优化查询(用 only()排除大字段)、增加缓存来解决,并补充了监控和测试。这次经历让我意识到边界参数校验和资源加载优化在高并发系统中的重要性。”


      这样回答既体现了系统性排查思维(日志 → 监控 → 代码 → 数据),又展示了实际解决能力与预防意识,是面试官想听到的内容。

  7. 如何设计 API 限流策略,既能防爬虫又能保证正常用户体验?

    1. 在 Django + DRF 的学习与实践中,API 限流(Rate Limiting)是保障服务稳定、防止恶意爬虫和滥用的重要手段。

      我们的目标是在 90% 的场景下,既能阻止高频爬取/攻击,又不影响正常用户的体验。下面从理论 → 策略 → 实现案例逐步讲解。


      一、理论:限流的核心概念与目标

      1. 限流的作用

      • 防爬虫:限制单位时间内的请求次数,使大规模抓取成本过高。
      • 防 DoS / 暴力破解:阻断异常高频请求。
      • 保护下游资源:减少数据库、第三方服务的压力。
      • 保证正常用户体验:让真实用户在合理范围内不受干扰。

      2. 常见限流算法

      算法 原理 适用场景
      固定窗口 以固定时间段(如 1 分钟)为单位计数请求 简单易实现,但存在临界突发问题
      滑动窗口 按更小粒度(如秒)维护计数,滑动统计 平滑限流,较公平
      令牌桶 以固定速率向桶中放令牌,请求需取令牌 允许短时突发,适合混合场景
      漏桶 请求进入固定速率输出的桶,超出丢弃 严格控制流出速率,适合保护下游

      二、设计思路:兼顾防爬虫与用户体验的策略

      1. 分层限流(针对不同目标)

      • 全局限流:防止整个 API 被压垮(粗粒度,如 Nginx 层)。
      • 接口级限流:针对热门或敏感接口(如登录、文章列表)单独设阈值。
      • 用户级限流
        • 匿名用户:严格限制(防爬虫)
        • 登录用户:放宽(保证体验)
        • VIP/付费用户:更高配额
      • IP 级限流:针对单 IP 的异常行为(可结合地理位置白名单)。

      2. 差异化阈值(关键防爬虫 & 保体验的平衡点)

      • 匿名用户:如 /api/posts每 IP 每分钟 ≤ 30 次(足够正常浏览,但爬虫难大规模抓取)。
      • 登录用户:每用户每分钟 ≤ 120 次(正常使用 + 翻页无阻碍)。
      • 突发容忍:短时间突发(如 5 秒内 10 次)可允许,防止页面初始化请求被误拦。

      3. 渐进式惩罚(避免一刀切封死)

      • 第一次超限 → 返回 429 + Retry-After 提示
      • 多次超限 → 延长封禁时间(如 1min → 10min)
      • 配合 CAPTCHA 或登录验证解除封锁(防自动化脚本)

      4. 白名单 & 特殊放行

      • 内部服务、监控、搜索引擎爬虫(User-Agent 白名单)不限流
      • 特定 API Key(合作方)走独立配额

      三、Django + DRF 的实现方法(90% 场景够用)

      1. 使用 djangorestframework-simple-rate-throttle

      DRF 内置了灵活的限流类,可以基于 用户IP接口 进行配置。

      配置示例(settings.py):

      REST_FRAMEWORK = {
          'DEFAULT_THROTTLE_CLASSES': [
              'rest_framework.throttling.AnonRateThrottle',     # 匿名用户
              'rest_framework.throttling.UserRateThrottle',      # 登录用户
          ],
          'DEFAULT_THROTTLE_RATES': {
              'anon': '30/minute',      # 匿名每 IP 每分钟最多 30 次
              'user': '120/minute',     # 登录用户每用户每分钟最多 120 次
              'special_api': '10/minute' # 针对某些接口可单独设
          }
      }
      

      针对特定接口加严:

      from rest_framework.throttling import AnonRateThrottle
      
      class StrictAnonThrottle(AnonRateThrottle):
          rate = '10/minute'   # 比全局匿名更严格
      
      class PostListView(generics.ListAPIView):
          throttle_classes = [StrictAnonThrottle]  # 文章列表防爬虫
      

      2. 自定义限流逻辑(更灵活)

      可实现 滑动窗口用户等级差异化

      from rest_framework.throttling import BaseThrottle
      import time
      from collections import defaultdict
      
      class SlidingWindowThrottle(BaseThrottle):
          """
          简单的滑动窗口限流(示例)
          """
          scope = "sliding"
          rates = {'anon': (30, 60), 'user': (120, 60)}  # (次数, 秒)
      
          def __init__(self):
              self.history = defaultdict(list)
      
          def allow_request(self, request, view):
              # 判断用户类型
              user_type = 'user' if request.user.is_authenticated else 'anon'
              limit, window = self.rates[user_type]
      
              now = time.time()
              key = self.get_ident(request)  # IP 或 user.id
              self.history[key] = [t for t in self.history[key] if now - t < window]
      
              if len(self.history[key]) >= limit:
                  return False  # 超限
              self.history[key].append(now)
              return True
      

      然后在视图中:

      throttle_classes = [SlidingWindowThrottle]
      

      3. 网关/反向代理层限流(更高效)

      • Nginx 限流(防爬虫第一道墙):

        limit_req_zone $binary_remote_addr zone=api_limit:10m rate=60r/m;
        
        location /api/ {
            limit_req zone=api_limit burst=10 nodelay;
            proxy_pass http://backend;
        }
        
      • API Gateway(如 Kong、AWS API Gateway)可直接配置限流策略,减少后端压力。


      四、结合防爬虫的额外措施

      措施 目的 实现
      User-Agent 检测 拦掉明显非浏览器请求 中间件检查 UA,拒绝空或异常 UA
      验证码挑战 阻止自动化脚本 超限后返回 CAPTCHA 页面或接口
      动态令牌(HMAC nonce) 防止简单重放请求 前端请求带一次性 token,后端校验
      行为分析 识别异常访问模式 同 IP 短时间大量不同文章请求视为爬虫

      五、策略速查表(90% 场景)

      维度 策略 说明
      匿名用户 IP 限流 + 低阈值 防爬虫主力
      登录用户 用户 ID 限流 + 高阈值 保证体验
      接口分级 热门接口单独严控 如文章列表、搜索
      时间窗口 滑动窗口或令牌桶 平滑突发
      惩罚机制 渐进封禁 + 验证解锁 防暴力绕过
      部署位置 Nginx → DRF → 业务逻辑 多层次防护

      六、小结

      设计 API 限流策略的核心是分层、差异化、可恢复

      1. 匿名用户严控 → 防爬虫与滥用
      2. 登录用户宽松 → 保证正常体验
      3. 滑动窗口/令牌桶 → 平滑突发流量
      4. 结合 Nginx/DRF/Gateway → 多层次防护
      5. 配合验证码与行为检测 → 提高爬虫成本

      在 Django + DRF 项目中,通过 DEFAULT_THROTTLE_CLASSES与自定义 Throttle 类即可快速落地,再辅以 Nginx 网关限流,就能在 90% 场景下有效平衡 防爬虫用户体验

  8. 当接口返回大量数据时,如何设计增量更新或数据同步机制?

    1. 在 Django + DRF 的学习与实践中,当接口返回大量数据时(例如文章列表、商品目录、日志等),一次性全量拉取会带来性能瓶颈(带宽、内存、响应时间)和用户体验问题。

      因此,我们需要设计增量更新 / 数据同步机制,让客户端只获取“发生变化的部分”。下面按照理论 → 常见方案 → Django/DRF 实现案例来讲解,覆盖 90% 场景下的可行做法。


      一、理论:增量更新的核心思想

      1. 为什么需要增量更新

      • 全量拉取成本高:数据量大时,网络传输慢、解析耗时、移动端耗电。
      • 实时性要求:客户端需要及时感知新增、修改、删除的数据。
      • 降低服务端压力:减少数据库的查询与序列化开销。

      2. 增量同步的关键要素

      要素 说明
      变更标识 用时间戳、版本号、Hash 标记数据变化
      变更范围 明确“自上次同步以来”哪些记录有变动
      变更类型 新增 / 修改 / 删除
      幂等性 客户端多次拉取相同区间数据结果一致
      冲突处理 明确当两端同时改动时的策略(一般服务端为权威)

      二、常见增量更新方案(适用 90% 场景)

      1. 基于时间戳(Last-Modified / Since)

      • 服务端每条记录维护 updated_at字段(精确到秒或毫秒)。
      • 客户端保存上次同步的时间戳 since_time
      • 请求时传参 ?since=2024-06-01T12:00:00Z,服务端返回此时间之后有变更的记录。

      优点:实现简单、直观。

      缺点:依赖服务器时间一致性;高并发下 updated_at相同的记录可能漏掉。


      2. 基于版本号(Version / Revision)

      • 每条记录或数据集有一个递增的 versionrevision号。
      • 客户端保存上次同步的版本号 since_version
      • 服务端返回 version > since_version的所有变更。

      优点:不依赖时间,顺序明确,适合分布式系统。

      缺点:需要维护版本号(可用数据库自增字段或全局计数器)。


      3. 基于 Hash / ETag(内容指纹)

      • 对数据集合生成唯一哈希(如 MD5、SHA256)或 ETag。
      • 客户端请求时带上上次拿到的 etag,服务端对比:
        • 若相同 → 返回 304 Not Modified(空包体)
        • 若不同 → 返回完整或差分数据

      优点:节省流量、天然幂等。

      缺点:集合较大时计算 Hash 成本高;不适合单条记录频繁改动的场景。


      4. 基于变更日志(Change Log / Sync Token)

      • 服务端维护一张 变更记录表SyncLog):object_id, action(create/update/delete), timestamp, version
      • 客户端保存上次同步的 sync_token(最后一条 log 的 ID 或时间戳)。
      • 拉取时只返回 log_id > sync_token的变更事件,客户端依此在本地增删改。

      优点:可精确追踪删除操作(软删/硬删),适合复杂业务。

      缺点:需要额外存储与维护日志表。


      三、Django/DRF 实现案例(基于时间戳方案)

      假设我们有一个 Post模型,需要让客户端增量同步文章列表。

      1. 模型设计

      class Post(models.Model):
          title = models.CharField(max_length=200)
          content = models.TextField()
          status = models.CharField(max_length=10, default='published')
          created_at = models.DateTimeField(auto_now_add=True)
          updated_at = models.DateTimeField(auto_now=True)  # 用作增量依据
      

      2. 增量接口设计

      请求参数

      • since:ISO8601 格式的时间戳,表示“只返回此时间之后更新的记录”
      • 可选 deleted=true表示需要返回删除标记(如果用软删)

      URL 示例

      GET /api/posts/sync?since=2024-06-01T10:00:00Z
      

      3. 视图实现

      from django.utils.dateparse import parse_datetime
      from rest_framework.views import APIView
      from rest_framework.response import Response
      from .models import Post
      from .serializers import PostSerializer
      
      class PostSyncView(APIView):
          def get(self, request):
              since_str = request.query_params.get('since')
              if since_str:
                  since_dt = parse_datetime(since_str)
                  if not since_dt:
                      return Response({'error': 'Invalid since parameter'}, status=400)
              else:
                  since_dt = None
      
              qs = Post.objects.filter(status='published')
              if since_dt:
                  qs = qs.filter(updated_at__gt=since_dt)
      
              serializer = PostSerializer(qs, many=True)
              latest_dt = qs.order_by('-updated_at').first().updated_at if qs.exists() else since_dt
              return Response({
                  'data': serializer.data,
                  'latest_updated_at': latest_dt.isoformat() if latest_dt else None
              })
      
      • 客户端保存返回的 latest_updated_at作为下次请求的 since参数。

      4. 支持删除的扩展(软删)

      如果文章可能被删除,可引入 is_deleted字段或单独变更日志:

      # 查询时包含删除标记
      if since_dt:
          qs = qs.filter(Q(updated_at__gt=since_dt) | Q(is_deleted=True, deleted_at__gt=since_dt))
      

      或直接返回变更日志(推荐用 Change Log 方案)。


      四、进阶优化(应对 90% 以上场景)

      场景 优化方案
      数据量极大 分页同步(?since=...&page=2&page_size=50
      多端同时同步 用版本号或 sync_token 防止漏变更
      网络不稳定 支持断点续传(记录上次收到的最后一条 ID)
      频繁变动 合并短时间批量变更,减少请求次数(服务端聚合窗口)
      冲突解决 服务端为权威,客户端以服务端数据为准覆盖本地

      五、小结

      当接口返回大量数据时,增量更新/同步机制的设计核心是:

      1. 选好变更标识:时间戳(简单)、版本号(精准)、Hash(高效比对)、变更日志(可追踪删除)。
      2. 接口支持 since/ version/ sync_token参数,返回变更数据 + 最新标识。
      3. 客户端保存上次同步标识,下次请求只拉取差异。
      4. 在 Django + DRF 中可通过普通视图或 DRF GenericAPIView + FilterBackend 实现,必要时加缓存提升性能。
      5. 结合分页、软删标记、变更日志,可覆盖绝大多数业务场景。

      这种机制能显著减少流量与计算消耗,提升大规模数据同步的效率与稳定性。

  9. DRF 中如何优雅地处理跨域(CORS)问题?

    1. 在 Django + DRF 的开发与部署过程中,跨域(CORS, Cross-Origin Resource Sharing) 是一个常见问题,尤其当前后端分离架构下,前端(如 React、Vue)运行在不同域名或端口时,浏览器会因同源策略阻止 API 请求。

      下面我会从原理 → 优雅解决方案 → Django/DRF 实战配置三个方面,帮你彻底理解并正确处理 CORS,覆盖 90% 以上的应用场景。


      一、理论:什么是 CORS?为什么需要它?

      1. 同源策略(Same-Origin Policy)

      浏览器默认只允许协议、域名、端口三者完全相同的网页发起 AJAX 请求。

      例如:

      • 前端运行在 http://localhost:3000

      • 后端 API 在 http://localhost:8000

        → 不同端口 → 属于跨域 → 浏览器会拦截响应(除非服务端明确允许)。

      2. CORS 的作用

      服务端通过在响应头中添加特定字段(如 Access-Control-Allow-Origin),告诉浏览器:“我允许来自某个源的请求访问我的资源”。

      这样浏览器才会放行前端 JavaScript 的跨域请求。


      二、常见跨域场景与预检请求(Preflight)

      1. 简单请求 vs 预检请求

      • 简单请求(Simple Request):

        方法为 GET/HEAD/POST,且 Content-Type 为 application/x-www-form-urlencodedmultipart/form-datatext/plain→ 浏览器直接发请求。

      • 非简单请求(如带自定义 Header、PUT/DELETE、Content-Type: application/json):

        浏览器会先发一个 OPTIONS 预检请求,询问服务器是否允许实际请求。

      ⚠️ 如果服务器没正确处理 OPTIONS 请求并返回 CORS 头,预检失败 → 浏览器拦截实际请求。


      三、Django/DRF 中优雅处理 CORS 的方案

      ✅ 推荐方案:使用 django-cors-headers(最主流、最省心)

      1. 安装

      pip install django-cors-headers
      

      2. 配置 settings.py

      INSTALLED_APPS = [
          ...
          'corsheaders',           # 必须在中间件靠前位置
          'rest_framework',
          ...
      ]
      
      MIDDLEWARE = [
          'corsheaders.middleware.CorsMiddleware',  # 👈 尽量放在最顶部
          'django.middleware.common.CommonMiddleware',
          ...
      ]
      
      # ===== CORS 配置 =====
      # 允许所有源(开发阶段方便,生产慎用!)
      CORS_ALLOW_ALL_ORIGINS = True  
      
      # 或 生产环境推荐:指定允许的源
      # CORS_ALLOWED_ORIGINS = [
      #     "https://example.com",
      #     "https://frontend.yoursite.com",
      # ]
      
      # 允许携带凭证(如 cookies、Authorization header)
      CORS_ALLOW_CREDENTIALS = True
      
      # 允许的 HTTP 方法
      CORS_ALLOW_METHODS = [
          "DELETE",
          "GET",
          "OPTIONS",
          "PATCH",
          "POST",
          "PUT",
      ]
      
      # 允许的请求头(重要!DRF 常用 Authorization、Content-Type)
      CORS_ALLOW_HEADERS = [
          "accept",
          "accept-encoding",
          "authorization",
          "content-type",
          "dnt",
          "origin",
          "user-agent",
          "x-csrftoken",
          "x-requested-with",
      ]
      

      3. 处理预检请求(OPTIONS)

      corsheaders会自动为 OPTIONS 请求添加正确的 CORS 响应头,无需你手写视图。

      只要中间件顺序正确(CorsMiddleware 在最前),它会拦截 OPTIONS 并返回 200 + 必要头。


      四、生产环境安全建议(避免开放过度)

      ❌ 不要直接使用 CORS_ALLOW_ALL_ORIGINS = True

      这在生产环境等于允许任意网站调用你的 API,可能导致 CSRF 或数据泄露。

      ✅ 正确做法:

      # 只允许信任的前端域名
      CORS_ALLOWED_ORIGINS = [
          "https://app.yourblog.com",
          "https://admin.yourblog.com",
      ]
      
      # 如果前端带 credentials(如 cookies),则不能使用通配符 *
      CORS_ALLOW_CREDENTIALS = True
      CORS_ALLOWED_ORIGINS  # 必须是具体域名列表
      

      五、结合 DRF 的特殊注意事项

      1. 认证请求(如 JWT in Authorization header)

      • 必须在 CORS_ALLOW_HEADERS中包含 "authorization"(如上配置已含)。
      • 如果使用 SessionAuthentication,还需确保 CORS_ALLOW_CREDENTIALS = True

      2. 自定义 Header 场景

      如果前端加了自定义 Header(如 X-Requested-With, X-API-Key),也要加到 CORS_ALLOW_HEADERS

      3. 与 CSRF 共存(Session 登录时)

      • 前端需从 csrftokencookie 读取 token,并在请求头 X-CSRFToken中发送。

      • corsheaders会允许该 Header,配合 CSRF_TRUSTED_ORIGINS使用更安全:

        CSRF_TRUSTED_ORIGINS = ["https://app.yourblog.com"]
        

      六、替代方案(不推荐,仅了解)

      方案 说明 缺点
      Nginx 配置 CORS 头 在反向代理层加 add_header 需手写规则,难维护,OPTIONS 仍需后端处理
      手动在 DRF 视图加装饰器 自定义中间件或 APIView中设置 header 重复劳动、易遗漏、难统一管理

      👉 结论django-cors-headers是最优雅、最省力的方案,几乎成为 Django 项目的标配。


      七、调试技巧

      1. 打开浏览器 DevTools → Network → 查看请求的 Response Headers 是否有:

        Access-Control-Allow-Origin: https://yourfrontend.com
        Access-Control-Allow-Credentials: true
        Access-Control-Allow-Methods: GET, POST, OPTIONS
        Access-Control-Allow-Headers: authorization, content-type
        
      2. 如果 OPTIONS 请求返回 403/404 → 检查 CorsMiddleware是否在 CommonMiddleware之前。

      3. 使用 curl测试:

        curl -X OPTIONS -H "Origin: https://example.com" \
             -H "Access-Control-Request-Method: POST" \
             http://localhost:8000/api/posts/ -I
        

      八、小结

      在 DRF 中优雅地处理 CORS 的最佳实践是:

      1. 使用 django-cors-headers中间件(官方推荐)
      2. 合理配置允许的源、方法、Header、Credentials
      3. 生产环境禁用 CORS_ALLOW_ALL_ORIGINS,使用白名单
      4. 确保中间件顺序正确CorsMiddleware最靠前)
      5. 注意预检请求(OPTIONS)与认证头的兼容性

      这样既能解决跨域问题,又能保障安全性,适用于 90% 以上的 Django + DRF 前后端分离项目。

  10. 如何监控 Django/DRF 项目的运行状态与性能指标?

    1. 在 Django + DRF 的学习与实践中,监控项目的运行状态与性能指标是保证线上稳定性的关键环节。

      在 90% 的场景下,我们不需要大而全的企业级 APM,而是围绕可用性、性能瓶颈、错误追踪、资源使用四大维度,搭建一套低成本、易落地的监控体系。下面按照理论 → 关键指标 → 工具选型 → Django/DRF 实战配置的逻辑展开。


      一、理论:监控的核心目标与分层

      1. 监控的目的

      • 提前发现问题:如接口变慢、错误率升高、数据库压力大。
      • 快速定位根因:定位是代码、数据库、缓存还是外部依赖引起。
      • 保障 SLA:确保接口可用率、响应时间在可接受范围。

      2. 常见监控分层

      层级 监控内容 示例
      基础设施层 CPU、内存、磁盘、网络 容器/服务器资源使用率
      应用层 请求量、响应时间、错误率、慢查询 DRF 接口延迟分布
      业务层 活跃用户数、订单量、关键流程成功率 博客发布成功率
      日志/异常层 错误堆栈、异常频率 500 错误的 traceback

      二、关键性能指标(适用于 90% 场景)

      1. 应用性能指标

      • 请求量(QPS/TPS):每秒处理的 API 请求数
      • 响应时间(Latency):平均响应时间、P95/P99 分位值
      • 错误率:HTTP 5xx、4xx 占比
      • 并发数:同时处理的请求数

      2. 数据库与缓存指标

      • DB 查询耗时:特别是慢查询(>200ms)
      • 连接数:是否接近上限
      • 缓存命中率:Redis/Memcached 命中率

      3. 外部依赖指标

      • 第三方 API 响应时间:支付、短信、搜索等服务
      • 调用失败率

      三、工具选型与组合(推荐实用方案)

      类别 工具 作用 特点
      Metrics 收集 Prometheus + Grafana 指标采集与可视化 开源、强大、生态丰富
      日志收集 ELK(Elasticsearch + Logstash/Fluentd + Kibana)或 Loki 集中查看与分析日志 适合排查错误
      异常追踪 Sentry 捕获并聚合异常栈 实时报警、易集成
      健康检查 Django Health Check 检查 DB、Cache、外部服务状态 简单实用
      APM(可选) New Relic、Datadog 全链路性能分析 商业方案,功能强

      在 90% 场景下,Prometheus + Grafana + Sentry + Django Health Check 足以覆盖监控需求。


      四、Django/DRF 实战配置

      1. 基础健康检查(django-health-check)

      pip install django-health-check
      # settings.py
      INSTALLED_APPS += ['health_check', 'health_check.db', 'health_check.cache']
      

      URL 配置:

      # urls.py
      from django.urls import path
      from health_check import urls as health_urls
      urlpatterns += [path('health/', include(health_urls))]
      

      访问 GET /health/可检查数据库、缓存是否可用。


      2. 暴露 Metrics 给 Prometheus(django-prometheus)

      pip install django-prometheus
      # settings.py
      INSTALLED_APPS += ['django_prometheus']
      MIDDLEWARE = ['django_prometheus.middleware.PrometheusBeforeMiddleware'] + \
                    MIDDLEWARE + \
                   ['django_prometheus.middleware.PrometheusAfterMiddleware']
      

      URL 配置:

      # urls.py
      from django_prometheus.exports import ExportToDjangoView
      urlpatterns += [path('metrics/', ExportToDjangoView)]
      

      启动后访问 /metrics可看到请求数、响应时间、DB 查询数等指标,Prometheus 可定期抓取。


      3. 接入 Sentry 捕获异常

      pip install sentry-sdk[django]
      # settings.py
      import sentry_sdk
      from sentry_sdk.integrations.django import DjangoIntegration
      
      sentry_sdk.init(
          dsn="https://<key>@sentry.io/<project>",
          integrations=[DjangoIntegration()],
          traces_sample_rate=1.0,  # 开启性能监控(可选)
          send_default_pii=True
      )
      

      效果:

      • 所有未捕获异常自动上报到 Sentry
      • 能看到错误发生的 URL、参数、用户、堆栈
      • 支持 Release 版本追踪与报警

      4. Grafana 可视化

      • 配置 Prometheus 数据源
      • 导入官方或社区的 Django 仪表盘(ID: 1860 等)
      • 可监控:
        • API 请求速率(按 endpoint 分组)
        • 平均响应时间 & P95
        • 错误率(5xx 比例)
        • DB 查询耗时

      5. 日志集中化(可选 Loki / ELK)

      Django 配置 JSON 格式日志:

      LOGGING = {
          'version': 1,
          'handlers': {
              'console': {'class': 'logging.StreamHandler'}
          },
          'root': {'level': 'INFO', 'handlers': ['console']},
          'formatters': {
              'json': {
                  '()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
                  'format': '%(asctime)s %(levelname)s %(message)s'
              }
          },
          'handlers': {
              'json_console': {'class': 'logging.StreamHandler', 'formatter': 'json'},
          },
          'root': {'level': 'INFO', 'handlers': ['json_console']},
      }
      

      配合 Fluentd/Loki 收集并可视化日志。


      五、报警与响应机制

      • Prometheus Alertmanager:当 QPS 突降、错误率 > 5%、响应时间 P95 > 1s 时发邮件/Slack。
      • Sentry Alerts:新错误出现或频率激增时即时通知开发者。
      • 健康检查报警/health/返回非 200 时触发 PagerDuty/钉钉报警。

      六、监控体系速查表(90% 场景)

      维度 工具 配置要点
      应用指标 Prometheus + django-prometheus 暴露 /metrics,抓 DB/请求数
      可视化 Grafana 建 Dashboard 看 QPS、延迟、错误率
      异常追踪 Sentry 自动捕获 500 错误与 traceback
      健康检查 django-health-check 检查 DB/Cache/外部依赖
      日志 Loki / ELK JSON 格式 + 集中收集
      报警 Alertmanager / Sentry 阈值触发通知

      七、小结

      在 Django/DRF 项目中,搭建监控的思路是:

      1. 确定核心指标:请求量、延迟、错误率、资源使用。
      2. 组合实用工具:Prometheus 抓指标 + Grafana 看趋势 + Sentry 捕异常 + Health Check 保可用。
      3. 在代码中埋点:用 django-prometheus暴露 metrics,用 Sentry SDK 捕获异常。
      4. 建立报警:及时感知异常并响应。
      5. 迭代优化:根据监控数据持续优化慢接口、缓存策略和查询。

      这套方案在 90% 场景下能让团队快速发现并定位问题,显著提升线上稳定性与运维效率。


posted @ 2025-12-09 18:52  GDms  阅读(24)  评论(0)    收藏  举报