零基础入门python35:Django用户登录与权限边界

零基础入门python35:Django用户登录与权限边界

一、上一篇课后练习讲解

Admin 中创建时间应设置 readonly_fields,订单金额不能开放编辑,因为订单是财务事实,修改应经过支付或退款服务并留下审计记录。

上一篇课后练习完整答案

上一篇练习的要求已落实到下面完整文件;先运行项目测试,再用 curl 对照状态码和数据库持久化结果

答案要点:ProductAdmin 配置搜索、筛选、关联预取和只读时间;Admin action 只下架,不开放订单金额编辑。

文件:shop/admin.py

完整参考答案文件

完整文件:shop/admin.py

python 复制代码
from django.contrib import admin
from .models import Product
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ("name", "category", "price", "stock", "is_active")
    list_select_related = ("category",)
    search_fields = ("name", "slug")
    list_filter = ("is_active", "category")

完整参考答案文件

本篇对应的交付源码完整文件:django-shop/shop/admin.py

python 复制代码
from django.contrib import admin
from .models import Category, Order, OrderItem, Product

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug')
    prepopulated_fields = {'slug': ('name',)}

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ('name', 'category', 'price', 'stock', 'is_active')
    list_filter = ('is_active', 'category')
    search_fields = ('name',)
    prepopulated_fields = {'slug': ('name',)}

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    readonly_fields = ('product', 'product_name', 'price', 'quantity')

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ('id', 'user', 'status', 'total_amount', 'created_at')
    list_filter = ('status',)
    inlines = (OrderItemInline,)

验收命令:python -m pytest -q(Django 项目使用 python manage.py test)。预期测试通过;若失败先检查迁移、配置和事务回滚。

二、本篇完成什么

商品列表公开,购物车和结算需要登录;订单列表只返回当前用户的订单。认证解决"你是谁",权限解决"你能操作什么"。

三、DRF权限配置

python 复制代码
class CheckoutView(generics.GenericAPIView):
    permission_classes = (permissions.IsAuthenticated,)

权限类是第一道门;get_queryset() 仍然必须过滤用户,因为"已登录"不等于"能看所有订单"。不要用客户端传来的 user_id 做权限依据。

四、验收

未登录访问 /api/checkout/ 返回 403;登录后可以结算;用户 A 的订单不会出现在用户 B 的 /api/orders/。课后练习:为订单详情增加对象级权限测试。

项目增量:Django 认证与权限

注册使用 Django 密码哈希,登录由 AuthenticationForm 校验;商品后台权限和前台订单权限是两套规则,不要把"已登录"当成"可以修改所有数据"。

python 复制代码
def require_login(request):
    if not request.user.is_authenticated:
        return redirect('login')
    return None

订单查询必须 filter(user=request.user),详情页猜 id 也不能看到他人订单。验收未登录、普通用户和 staff 三种角色;课后练习增加装饰器保护结账页面。

五、上一篇练习讲解:Admin action 也要有权限

批量下架 action 应只修改 is_active,并使用 request.user 写审计日志;不能把 action 做成"批量改价格"。测试用 staff 用户调用 action,普通用户访问 Admin 应返回 302 到登录页或 403,取决于项目设置。

六、认证和授权是两件事

认证回答"你是谁",授权回答"你能做什么"。Django 的 request.user.is_authenticated 只能证明登录,不能让用户读取别人的订单。结账视图可以这样写:

python 复制代码
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse

@login_required
def checkout(request):
    if request.method != "POST":
        return JsonResponse({"error": "method_not_allowed"}, status=405)
    # 购物车内容随后会交给 service.place_order,视图不直接扣库存。
    order = place_order(request.user, request.session.get("cart", {}))
    request.session["cart"] = {}
    return JsonResponse({"id": order.id, "total": str(order.total)}, status=201)

login_required 默认返回 302;纯 API 可以配置 LoginRequiredMiddleware 或自定义装饰器返回 401/403。视图只负责 HTTP,库存和事务放进 service,避免网页和 API 两套下单规则。

七、对象级权限的查询写法

python 复制代码
def order_detail(request, order_id):
    order = get_object_or_404(Order, id=order_id, user=request.user)
    return JsonResponse(OrderSerializer(order).data)

user=request.user 放进 get_object_or_404,用户 A 猜到 B 的 id 只会得到 404。管理员查看全部订单应使用独立 URL 和独立权限检查,而不是在普通详情接口中偷偷放宽查询。

八、会话安全和排错

登录后检查响应是否包含 HttpOnlySameSite=Lax cookie;生产环境开启 HTTPS 后再设置 Secure=True。出现"登录后跳回登录页",检查 django.contrib.sessionsSessionMiddlewareAuthenticationMiddleware 的顺序。AnonymousUser 访问 request.user.id 会得到 None,不要把它直接用于数据库写入。

九、本篇练习

写三个测试:未登录访问结账、普通用户访问他人订单、staff 访问管理员订单列表。下一篇会用 Session 保存购物车,并说明为什么价格必须重新从数据库读取。

十、认证故障排查清单

遇到 403 时按顺序检查:用户是否真的登录(request.user.is_authenticated)、SessionMiddleware 是否加载、CSRF token 是否随 POST 提交、接口是否要求 staff。不要为了让测试通过而全局关闭 CSRF;API 可以使用 token 认证,后台表单仍保留 CSRF。密码验证失败时确认使用 set_password 而非直接给 password 字段赋值,后者会把明文写入数据库。

可在测试中打印 response.wsgi_request.userclient.cookies,但生产日志不应输出 cookie。认证和授权边界固定后,购物车、订单、管理员 action 都只依赖 request.user,不会再从请求体猜用户。

十一、角色矩阵练习

把角色写成矩阵更直观:游客可读在售商品;普通用户可管理自己的购物车和订单;staff 可维护商品、查看全部订单;超级用户可管理权限。为矩阵每一格至少写一个测试,避免以后新增接口时把"已登录"误当成"管理员"。

本篇结束:完整模块文件

下面是交付项目中真实存在的完整文件 django-shop/shop/views.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码;复制单个函数会丢失上下文,因此这里提供整份文件。

python 复制代码
from rest_framework import generics, permissions, status, viewsets
from rest_framework.response import Response
from .models import Category, Order, Product
from .serializers import CategorySerializer, CheckoutSerializer, OrderSerializer, ProductSerializer
from .services import create_order

class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Category.objects.order_by('name')
    serializer_class = CategorySerializer

class ProductViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = ProductSerializer
    def get_queryset(self):
        query = Product.objects.filter(is_active=True).select_related('category')
        if category := self.request.query_params.get('category'): query = query.filter(category_id=category)
        if keyword := self.request.query_params.get('q'): query = query.filter(name__icontains=keyword)
        return query.order_by('-created_at')

class CheckoutView(generics.GenericAPIView):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = CheckoutSerializer
    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        order = create_order(request.user, serializer.validated_data['items'])
        return Response(OrderSerializer(order).data, status=201)

class MyOrderViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = OrderSerializer
    def get_queryset(self):
        return Order.objects.filter(user=self.request.user).prefetch_related('items').order_by('-id')

class CartView(generics.GenericAPIView):
    def get(self, request):
        return Response(request.session.get('cart', {}))

    def post(self, request):
        product = generics.get_object_or_404(Product, id=request.data.get('product_id'), is_active=True)
        quantity = int(request.data.get('quantity', 1))
        if quantity < 1 or quantity > product.stock:
            return Response({'message': '数量必须大于0且不能超过库存'}, status=status.HTTP_400_BAD_REQUEST)
        cart = request.session.setdefault('cart', {})
        cart[str(product.id)] = quantity
        request.session.modified = True
        return Response(cart, status=status.HTTP_201_CREATED)

    def delete(self, request):
        cart = request.session.get('cart', {})
        cart.pop(str(request.data.get('product_id')), None)
        request.session.modified = True
        return Response(cart)
相关推荐
淼澄研学17 分钟前
LangChain构建Prompt模板解决Python长尾报错实操
python·langchain·prompt
枫叶v.21 分钟前
DeepSeek Harness(DSH)插件开发保姆级教程:从 0 到 1 快速写出第一个插件
python
Htr_29 分钟前
Python面向对象编程:类与对象基础
开发语言·python
muddjsv32 分钟前
Python 神经网络入门:用 scikit-learn 完成 MLP 分类与回归
python·神经网络·scikit-learn
Java后端的Ai之路35 分钟前
09、Python组合模式
开发语言·人工智能·python·docker·组合模式
Tenifs41 分钟前
Python Loguru 使用指南
python·loguru
青 春 记 忆44 分钟前
零基础入门python36:用 Django Session 实现购物车
python·django·后端开发
l1258651 小时前
# LangGraph Tool Calling Agent 深度实战:从零构建 ReAct 循环与工具调用链
人工智能·python·自然语言处理·langchain·agent