从零搭建 RESTful API 服务:DRF 开发指南与 JWT 认证实战
【免费下载链接】Python-100-DaysPython - 100天从新手到大师项目地址: https://gitcode.com/GitHub_Trending/py/Python-100-Days
在 Python-100-Days 项目里,DRF 开发指南相关的章节覆盖了 RESTful API 从零搭建的完整路径:资源怎么规划、DRF 序列化器配置怎么写、JWT 认证流程如何跑通,直到接口文档的规范交付。你跟着本文走一遍,就能亲手做出一个带认证、分页、过滤和文档的博客 API,并且知道每一步"为什么这么做"。
一条博客 API 的完整旅程
假设你要给前端提供"查看某篇文章及其评论"的能力。用户点击页面的瞬间,发生的事是这样的:
- 浏览器发出
GET /api/blogs/1/请求; - 请求穿过 Django 的中间件,被 URL 分发器转给对应的视图;
- DRF 在这里接手:先做认证(你是谁)、再做权限判断(你能不能干这事);
- 视图取出模型数据,交给序列化器转成 JSON;
- 响应原路返回,前端拿到结果渲染页面。
理解这条链路非常重要:以后接口出任何问题——401、403、404、字段缺失——你都能快速定位到是"认证、权限、路由、视图还是序列化"哪一环出的事,而不是两眼一抹黑地猜。
安装 DRF 并配置全局默认值
在终端执行安装命令,把rest_framework注册进INSTALLED_APPS:
pip install djangorestframeworkINSTALLED_APPS = [ # ...其他应用 'rest_framework', ]然后在配置文件的REST_FRAMEWORK字典里设默认值。💡 这些默认值是全项目的"底线":单个视图随时可以用permission_classes、pagination_class等属性覆盖,但覆盖是少数情况,绝大多数接口应该沿用统一策略。
| 配置项 | 作用 |
|---|---|
DEFAULT_AUTHENTICATION_CLASSES | 默认认证方式,含SessionAuthentication(基于 session)、TokenAuthentication、BasicAuthentication |
DEFAULT_PERMISSION_CLASSES | 默认权限,如AllowAny、IsAuthenticated、IsAdminUser |
DEFAULT_PAGINATION_CLASS与PAGE_SIZE | 列表接口的默认分页器与每页条数 |
EXCEPTION_HANDLER | 统一异常出口,定制错误响应格式在这里改 |
一个适合博客场景的起步配置:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.AllowAny', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, }序列化器:给前端"翻译"模型字段
前端、App、其他服务消费的都是 JSON,而你的数据躺在 Django 模型里。两者之间需要一个"翻译官"——这就是 DRF 序列化器(serializer):它负责把模型实例转成可 JSON 化的字典,同时反过来做输入校验。
绝大多数情况下,继承ModelSerializer并声明Meta就够了。注意fields要显式列出:'__all__'会把模型里每个字段都暴露出去,密码、内部计数这类字段一旦漏出去就是事故。
class BlogSerializer(serializers.ModelSerializer): class Meta: model = Blog fields = ('id', 'title', 'summary', 'content', 'pub_date')序列化时机是在视图里:单个对象直接传,列表则加many=True。返回结构也固定:.data拿到字典,交给Response就变成 JSON 响应。
函数视图:博客详情接口
DRF 支持两种写法:函数视图(FBV)和类视图(CBV)。函数视图最直观,@api_view装饰器声明允许的 HTTP 方法,函数体想怎么写就怎么写,适合逻辑个性化的接口。
下面是一个博客详情接口,同时演示了 404 的正确处理方式:
@api_view(['GET']) def blog_detail(request, blog_id): try: blog = Blog.objects.get(pk=blog_id) except Blog.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) return Response(BlogSerializer(blog).data)⚠️ 注意一个常见误区:查不到对象就继续往下走、返回一串None,前端会很难受。资源不存在就该返回 404,这是 HTTP 语义的基本约定。
类视图与嵌套路由:评论接口一次配齐
当接口是标准的"列表 / 详情 / 创建"套路时,用类视图更省。ModelViewSet一次性给你list、retrieve、create、update、destroy全套动作,你只需声明queryset和serializer_class;如果只要读接口,用ReadOnlyModelViewSet更克制。
评论是博客的子资源,放在blog/{blog_pk}/comments/下面。用DefaultRouter注册嵌套路由,评论的"挂在哪个博客下"由路径参数直接给出:
class BlogViewSet(ReadOnlyModelViewSet): queryset = Blog.objects.all() serializer_class = BlogSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] class CommentSerializer(serializers.ModelSerializer): class Meta: model = Comment fields = ('id', 'content', 'pub_date') def get_queryset(self): return Comment.objects.filter(blog_id=self.kwargs['blog_pk']) @action(detail=False, methods=['get', 'post']) def comments(self, request, blog_pk=None): # GET 返回该博客的评论,POST 创建新评论(此处省略分支细节) ... class CommentViewSet(ModelViewSet): serializer_class = BlogViewSet.CommentSerializer queryset = Comment.objects.none() def get_queryset(self): return Comment.objects.filter(blog_id=self.kwargs['blog_pk']) def perform_create(self, serializer): serializer.save(blog_id=self.kwargs['blog_pk'])router = DefaultRouter() router.register('blogs', BlogViewSet) router.register(r'blogs/(?P<blog_pk>\d+)/comments', CommentViewSet, basename='blog-comment') urlpatterns += router.urls几个要点值得记住:
- 权限写在视集上:
IsAuthenticatedOrReadOnly的语义是——GET等安全方法人人可读,POST/PUT/DELETE必须登录。这正符合"内容公开、评论需登录"的业务诉求。 - 评论挂在博客路径下,创建时无需用户再传一次
blog_id,从路径取,杜绝了"评论写错博客"这种 bug。 ModelViewSet必须配合路由器注册,basename在存在歧义时指定一下更稳。
注册完成后,用浏览器访问http://127.0.0.1:8000/api/,DRF 自带一套可交互的接口页面,能直接看到每个端点并手动发请求。📌 开发早期用它联调非常方便,前端同学也能在上面试数据:
JWT 令牌怎么签发和校验
session 认证有个扩展难题:服务端必须保存会话对象,多机部署时还要靠 Redis 之类共享存储。而 token 方案把状态挪到了客户端——用户登录后拿到令牌,之后每次请求都带着它,服务端只验签、不存状态,加机器就能扩容。
JWT(JSON Web Token,开放标准 RFC 7519)由三段组成,段与段之间用.连接:
| 部分 | 内容 | 用途 |
|---|---|---|
| 头部 | {"alg": "HS256", "typ": "JWT"} | 声明签名算法与类型 |
| 载荷 | 用户标识、exp过期时间,可加自定义字段 | 承载实际数据 |
| 签名 | 用服务器端密钥对前两段做 HMAC 摘要 | 防伪造、防篡改 |
完整 JWT 认证流程在 Python-100-Days 项目的 54.RESTful架构和DRF入门 中有系统讲解,落地时用pip install pyjwt装上 PyJWT 即可:
- 签发:登录成功后把用户 ID、过期时间等放进载荷,用
settings.SECRET_KEY编码成令牌返回给前端; - 存储:前端存入
localStorage或sessionStorage,之后每个请求都通过自定义请求头(约定为Authorization)携带; - 校验:服务端解码验签,令牌过期或无效统一返回 401,前端收到 401 就跳回登录页。
两个提醒:密钥只能留在服务端,绝不能下发给客户端;令牌一旦签发、在过期前无法作废,所以有效期要设得短一些,敏感操作另加二次验证。
接口文档规范:联调之前先把契约写清楚
接口写好了,前端怎么知道怎么调?靠一份清晰的接口文档。项目里 94.网络API接口设计 推荐用 RAP2、YAPI 这类工具管理文档,核心是一份"契约"。以博客详情接口为例,文档应包含:
| 项目 | 约定 |
|---|---|
| URL 与方法 | GET /api/blogs/{id}/ |
| 路径参数 | id:整数,必填 |
| 权限 | 无需登录 |
| 成功响应 | 200,正文为博客对象(字段与序列化器一致) |
| 失败响应 | 404:博客不存在;401:token 过期(仅需认证接口) |
分页接口的响应结构也值得写进文档,DRF 的PageNumberPagination默认返回count(总数)、next/previous(翻页地址)和results(当前页数据)四个键。💡 实际项目里建议再约定一个统一信封:成功时业务字段外附code: 0,失败时code非零并配message,让前端用一套逻辑处理所有异常。契约一旦双方确认,后端改字段就必须同步改文档——这是避免"联调扯皮"最便宜的办法。
新手最容易踩的 5 个坑
fields = '__all__'顺手一写:敏感字段直接裸露。永远显式列字段。- 查不到就返回 None:应该返回 404,让客户端明确知道资源不存在。
- 部分更新用了 PUT:PUT 语义是全量替换,只改几个字段请用 PATCH。
- 默认权限没想清楚:全局
AllowAny意味着所有写接口也是公开的,创建类接口记得收紧。 - 跨域忘了配:前后端分离必然跨域,接入
django-cors-headers并按来源白名单配置,别图省事写*。
下一步往哪走
- API 版本控制:用 URL 前缀或请求头版本策略,让老客户端不被破坏
- 限流与节流:给匿名/认证用户配置不同的频率上限,防刷接口
- 异步任务:导出、通知这类慢操作交给 Celery 处理
- 性能监控与日志:记录慢查询,给热点接口加缓存并盯紧命中率
RESTful 架构与 DRF 的更多细节,可以回看项目中的 55.RESTful架构和DRF进阶,里面还有数据筛选(django-filter)和游标分页的完整写法。
【免费下载链接】Python-100-DaysPython - 100天从新手到大师项目地址: https://gitcode.com/GitHub_Trending/py/Python-100-Days
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考