更多内容请点击 我的博客 查看,欢迎来访。
用户添加、删除收藏记录权限问题
如果用户指定id删除收藏记录,如果这条收藏记录是其他人的,而不是当前登录用户的,仍然会被成功删除掉,这时候就涉及到一个权限问题:用户只能查看到自己的收藏记录,且只能删除自己的收藏记录。
需要登录管理收藏记录IsAuthenticated
访问 https://www.django-rest-framework.org/api-guide/permissions/#isauthenticated 参考IsAuthenticated
IsAuthenticated
权限类将拒绝任何未经身份验证的用户的权限,否则将允许权限。
如果您希望您的API只允许注册用户访问,则此权限是合适的。
下面使用IsAuthenticated
权限来限制只有用户登录后才能访问。
# apps/user_operation/views.py
from rest_framework.permissions import IsAuthenticated
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
用户收藏商品
取消收藏商品
显示收藏商品
"""
queryset = UserFav.objects.all()
serializer_class = UserFavSerializer
permission_classes = (IsAuthenticated,)
当退出登录用户后,如果再访问 http://127.0.0.1:8000/userfavs/ 则会提示401错误。
但仅有IsAuthenticated
权限是不够的,删除收藏记录还要判断这条记录的创建人是否是当前登录的用户,如果是才允许删除。
DRF自定义BasePermission验证所有者
DRF也提供了自定义权限功能,要实现自定义权限,覆盖BasePermission
并实现以下方法之一或两者都实现:
.has_permission(self, request, view)
.has_object_permission(self, request, view, obj)
如果应该授予请求访问权,则方法应该返回True
,否则返回False
。
如果鉴权失败,自定义权限将引发一个PermissionDenied
异常。要更改与异常关联的错误消息,直接在自定义权限上实现message
属性。否则,将使用PermissionDenied
中的default_detail
属性。
可以查看 https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions 的示例
在util文件夹下创建 permissions.py 文件,用户放置权限相关的配置。
# utils/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
对象级权限,只允许对象的所有者编辑它。
模型实例有一个 user 属性,指向用户的外键。
"""
def has_object_permission(self, request, view, obj):
# 读取权限允许任何请求,所以我们总是允许GET、HEAD或OPTIONS请求。
if request.method in permissions.SAFE_METHODS:
return True
# 实例必须有一个名为user的属性。
return obj.user == request.user
以上的意思就是只有当对象的user
和当前登录user
一样,才返回True
,表明鉴权通过。
然后再用户收藏ViewSet中添加该权限类
# apps/user_operation/views.py
from utils.permissions import IsOwnerOrReadOnly
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
用户收藏商品
取消收藏商品
显示收藏商品
"""
queryset = UserFav.objects.all()
serializer_class = UserFavSerializer
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
只能查看自己的收藏记录
还有一个问题就是,获取用户收藏时,不能获取所有的收藏记录,只获取到当前用户的收藏记录。所以要重载get_queryset()
方法,过滤当前用户。
# apps/user_operation/views.py
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
用户收藏商品
取消收藏商品
显示收藏商品
"""
queryset = UserFav.objects.all()
serializer_class = UserFavSerializer
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
def get_queryset(self):
# 过滤当前用户的收藏记录
return self.queryset.filter(user=self.request.user)
现在访问 http://127.0.0.1:8000/userfavs/ 给当前用户增加几个收藏
在后台将任意一个收藏记录修改为其他用户,比如这儿id=5
的, http://127.0.0.1:8000/admin/user_operation/userfav/5/change/ 改为另一个用户
现在序列化时只会显示当前登录用户的收藏记录了。
测试取消全局登录认证
现在退出当前登录用户,最好重新打开浏览器,然后用工具请求删除功能,比如删除id=4
的收藏记录
点击send之后,因为权限类中配置了IsAuthenticated
,就会弹出登录框,取消就会出现401的错误。如果如果帐密后,那么该记录就会被删除
登陆之后,该记录就会被成功删除
刷新 http://127.0.0.1:8000/userfavs/ 指定id的记录就消失了。
这儿直接可以输入账号密码就完成登录,因为在 settings.py 中REST_FRAMEWORK
配置了'rest_framework.authentication.BasicAuthentication',
(即输入用户名密码认证模式)
注释掉全局token认证
# DjangoOnlineFreshSupermarket/settings.py
REST_FRAMEWORK = {
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
# 'PAGE_SIZE': 5,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication', # 上面两个用于DRF基本验证
# 'rest_framework.authentication.TokenAuthentication', # TokenAuthentication,取消全局token,放在视图中进行
# 'rest_framework_simplejwt.authentication.JWTAuthentication', # djangorestframework_simplejwt JWT认证
)
}
token
认证最好是放在view里面去,不要配置在全局变量中,如果再前端请求中,每一个request
都加入token
,这个token
恰好过期了,那么用户在访问category、goods这种公开数据时,就会抛异常,连商品的列表页都访问不了了。
配置局部JWTAuthentication认证
需要将JWTAuthentication
配置到需要的view中,这才是一种比较安全的做法,比如在用户收藏ViewSet中配置
# apps/user_operation/views.py
from rest_framework_simplejwt.authentication import JWTAuthentication
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
用户收藏商品
取消收藏商品
显示收藏商品
"""
queryset = UserFav.objects.all()
serializer_class = UserFavSerializer
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JWTAuthentication,) # 配置登录认证
def get_queryset(self):
# 过滤当前用户的收藏记录
return self.queryset.filter(user=self.request.user)
这样就不会在全局做token验证了。用户在发送JWT token时,如果是GoodsListViewSet
,那么商品列表视图就不会调用这个JWTAuthentication
,不会抛异常。只有当访问UserFavViewSet
这种配置了authentication_classes
,才会进行token验证。
现在来删除id=3
的收藏数据,会出现401错误,表示需要用户登录
熟练需要获取token,将用户名密码提交到 http://127.0.0.1:8000/login/ 获取
可以得到
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU1ODUxOTIyNSwianRpIjoiOGE1MWY4NmNlMTZlNDZmMjk4ODJmZmU0ZjExNjE0ODciLCJ1c2VyX2lkIjoxfQ.t31c-RiD3mjq3Fb3YvvDhZi_Yd5vcQl8f4OfTU0_oLE",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTU3ODI4MDI1LCJqdGkiOiJiYTEzMDYzNjMxNTU0NzFkYTE3ODNlN2FmYWMwODhiNCIsInVzZXJfaWQiOjF9.L_ZmXyv0wti9HImWDM5qcHVU7LTwBO--7JFGLb7l9rU"
}
然后通过这个token来请求,需要添加Header,键为Authorization
,值为Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTU3ODI4MDI1LCJqdGkiOiJiYTEzMDYzNjMxNTU0NzFkYTE3ODNlN2FmYWMwODhiNCIsInVzZXJfaWQiOjF9.L_ZmXyv0wti9HImWDM5qcHVU7LTwBO--7JFGLb7l9rU
就可以成功删除记录。
如果不是删除当前用户创建的记录,则会提示404错误
配置局部SessionAuthentication认证
访问 http://127.0.0.1:8000/userfavs/ 并点击右上角的登录
但仍然会显示"detail": "身份认证信息未提供。"
,因为在authentication_classes = (JWTAuthentication,)
中只使用了JWTAuthentication
,表明只支持JWT用户认证,如果想要DRF Api中可以通过认证,还需要添加SessionAuthentication
,表明支持session认证模式。
# apps/user_operation/views.py
from rest_framework.authentication import SessionAuthentication
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
用户收藏商品
取消收藏商品
显示收藏商品
"""
queryset = UserFav.objects.all()
serializer_class = UserFavSerializer
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JWTAuthentication, SessionAuthentication) # 配置登录认证:支持JWT认证和DRF基本认证
def get_queryset(self):
# 过滤当前用户的收藏记录
return self.queryset.filter(user=self.request.user)
显示具体收藏,根据商品id
首先将ViewSet继承mixins.RetrieveModelMixin
UserFavViewSet
继承的viewsets.GenericViewSet
又继承了generics.GenericAPIView
,其中有一个lookup_field = 'pk'
属性(如果想使用pk以外的对象查找,设置lookup_field
)
访问 https://www.django-rest-framework.org/api-guide/generic-views/#genericapiview 可以查看相关的属性
以下属性控制着基本视图的行为。
-
queryset
:用于从视图返回对象的查询结果集。通常,你必须设置此属性或者重写get_queryset()
方法。如果你重写了一个视图的方法,重要的是你应该调用get_queryset()
方法而不是直接访问该属性,因为queryset
将被计算一次,这些结果将为后续请求缓存起来。 -
serializer_class
:用于验证和反序列化输入以及用于序列化输出的Serializer类。 通常,你必须设置此属性或者重写get_serializer_class()
方法。 -
lookup_field
:用于执行各个model实例的对象查找的model字段。默认为'pk'
。 请注意,在使用超链接API时,如果需要使用自定义的值,你需要确保在API视图和序列化类都设置查找字段。 -
lookup_url_kwarg
:应用于对象查找的URL关键字参数。它的 URL conf 应该包括一个与这个值相对应的关键字参数。如果取消设置,默认情况下使用与lookup_field
相同的值。
将项目的lookup_field
修改为商品的id,因为对于当前登录用户来说,用户收藏商品是唯一的,它是根据get_queryset(self)
筛选后搜索的。
# apps/user_operation/views.py
class UserFavViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""
用户收藏商品
取消收藏商品
显示收藏商品列表
根据商品id显示收藏详情
"""
queryset = UserFav.objects.all()
serializer_class = UserFavSerializer
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JWTAuthentication, SessionAuthentication) # 配置登录认证:支持JWT认证和DRF基本认证
lookup_field = 'goods_id'
def get_queryset(self):
# 过滤当前用户的收藏记录
return self.queryset.filter(user=self.request.user)
测试下是否是根据goods_id
搜索
如果直接搜索id,则搜索不到结果
就可以按照商品的id显示详情。可用性高,用户进入商品详情页,用户要去查询这个商品有没有被收藏,这个url里面就可以直接填写goods的id
,不需要知道当时数据库里面保存的数据id是什么。直接根据商品的id去查询收藏,如果有记录,则页面就显示已收藏的信息,如果没被记录,表明商品没被收藏。
这里按照goods.id
进行查找,对于同一个商品,可能会被很多人收藏,但并不会造成查询出错,因为商品和用户一起构成了唯一性,而且lookup_field
字段是在get_queryset(self)
过滤之后的结果进行查找,也就是过滤当前登录用户,保证了唯一性,所以不会出错。
Vue联调
在商品详情页,如果用户没有登录的情况,那么收藏按钮的状态应该是未收藏。
这时候是无法获取用户的。Vue逻辑如下
获取商品收藏状态
用户进入详情页,也就是 src/views/productDetail/productDetail.vue 组件,获取商品的id,从cookie中找token
// src/views/productDetail/productDetail.vue
created() {
this.productId = this.$route.params.productId;
var productId = this.productId;
if (cookie.getCookie('token')) {
getFav(productId).then((response) => {
this.hasFav = true
}).catch(function (error) {
console.log(error);
});
}
this.getDetails();
},
如果找到这个token
,就调用getFav(productId)
查询该商品是否被收藏。这里面调用请求后端的接口。
// src/api/api.js
//判断是否收藏
export const getFav = goodsId => {
return axios.get(`${local_host}/userfavs/` + goodsId + '/')
};
如果获取的状态码为200,也就是已收藏状态(因为未获取到状态码为404),则将hasFav
设置为true
,看下面的判断逻辑
已收藏
收藏
如果为true
,则显示已收藏,否则显示收藏。
删除收藏点击
用户点击已收藏按钮时,则会执行@click="deleteCollect"
操作
// src/views/productDetail/productDetail.vue
deleteCollect() {
//删除收藏
delFav(this.productId).then((response) => {
//console.log(response.data);
this.hasFav = false
}).catch(function (error) {
console.log(error);
});
},
在这个方法中,调用删除收藏的接口,如果删除成功,状态码也是204,则将hasFav
置为false
// src/api/api.js
//取消收藏
export const delFav = goodsId => {
return axios.delete(`${local_host}/userfavs/` + goodsId + '/')
};
添加收藏点击
如果商品未收藏,用户点击该按钮时,执行@click="addCollect"
// src/views/productDetail/productDetail.vue
addCollect() { //加入收藏
addFav({
goods: this.productId
}).then((response) => {
//console.log(response.data);
this.hasFav = true;
alert('已成功加入收藏夹');
}).catch(function (error) {
console.log(error);
});
},
这里面会调用收藏接口
// src/api/api.js
//收藏
export const addFav = params => {
return axios.post(`${local_host}/userfavs/`, params)
};
向后台POST{goods: this.productId}
,如果状态码为2xx,则收藏成功。hasFav
置为true
登录之后打开调试,然后点击收藏
会收到弹框提示。按钮就变为已收藏了。
然后取消收藏,状态码为204
刷新 http://127.0.0.1:8000/admin/user_operation/userfav/ 之前的收藏已经消失了。