Django+DRF为第三方系统开发扩展模块:用户验证篇

这是一个实际工作中遇到的需求,集团有一个第三方的OA系统,需要扩展一个订餐功能,但是OA开发商的报价太贵,于是就找内部人员来开发这个扩展。

首先这个OA是ASP.NET架构的,技术栈我不熟,只提供了接口文档,翻来翻去没什么有用的接口,只有一个SSO鉴权用的接口能用得上(吐槽一下国内做这些OA、票务、PMS系统的公司,用的都是很老旧的API规范),那就开整吧

需求比较简单,做一个能让OA用户访问的订餐系统,页面适配PC和移动端做成响应式,食堂负责人能看到订餐数据,那后端还是用我熟悉的Django+DRF+PostgreSQL做吧。具体的业务实现这里不谈,因为比较简单,这个需求的难点在于需要让OA用户在访问订餐系统时做到无需二次注册登录,因为在用户眼里这个功能是和OA一体的呀,所以这篇文章重点讲我是如何实现用Django+DRF接入这个OA系统的用户体系的。

在建模时,为了让我们的数据库保存OA员工的用户数据,以达到进行订餐和统计数据的目的,通常你可能会想到继承AbstractUser类或者写一个OneToOneField的model来扩展django现成的user model,但这个方法会带来潜在的安全风险,尤其是在OA没有可靠的加密手段交换信息的情况下(这个OA还是用IP地址直接从外网访问的,我们集团就穷到这个地步)。虽然我们做的就是一个订餐系统,就算被攻破了也没什么损失,但是从职业角度触发,我觉得还是应该做到尽可能安全。所以在这条路走不通的情况下,我们就改为定义一个OAUser模型,继承普通Model保存员工信息,再改写DRF的JWTAuthentication函数让它检查OAUser,从而实现直接给OA系统的用户下发我们django系统的JWT密钥,缺点是我们系统的开放API授权完全依赖第三方系统了,不过就算这样假如被攻破,攻击者能做的破坏也有限,总比直接继承AbstractUser来得安全。

详细的代码如下(仅供参考)

"""
models.py
定义模型
"""
class OAUser(models.Model):
    uid = models.CharField("用户UID", null=True, blank=True)
    name = models.CharField("姓名", null=True, blank=True)
    phone = models.CharField("手机号", null=True, blank=True)

    def __str__(self) -> str:
        return f"OA员工 {self.phone}"

    class Meta:
        verbose_name = "OA员工"
        verbose_name_plural = verbose_name
        constraints = [
            UniqueConstraint(fields=['uid'], name='unique_oa_uid'),
            UniqueConstraint(fields=['phone'], name='unique_phone_number')
        ]
"""
authentication.py
创建自定义授权函数
"""
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken, AuthenticationFailed
from dingcan.models import OAUser


class CustomJWTAuthentication(JWTAuthentication):
    def get_user(self, validated_token):
        try:
            user_id = validated_token['user_id']
        except KeyError:
            raise InvalidToken('Token contained no recognizable user identification')

        try:
            user = OAUser.objects.get(id=user_id)
        except OAUser.DoesNotExist:
            raise AuthenticationFailed('User not found', code='user_not_found')

        return user
"""
调用OA接口验证员工信息的代码
"""
class CustomTokenObtainView(TokenViewBase):
    serializer_class = CustomTokenObtainSerializer

    def post(self, request, *args, **kwargs):
        loginid = request.data.get('loginid')

        try:
            user = OrderUser.objects.get(phone=loginid)
        except OrderUser.DoesNotExist:
            url = ''
            headers = {}
            data = {}
            response = requests.post(url, headers=headers, params=data)

            if response.status_code == 200 and response.text != "未找到对应的在职人员信息":
                user = OrderUser.objects.create(uid=response.uid, name=response.name, phone=loginid)
            else:
                return Response({"detail": "Invalid loginid or not found."}, status=400)
            
        refresh = RefreshToken.for_user(user)
        return Response({
            'refresh': str(refresh),
            'access': str(refresh.access_token),
            'user': {
                'name': user.name,
                'phone': user.phone,
            }
        }, status=200)

这样就无需大改django底层代码,通过一种相对优雅的方式实现了接入第三方用户体系的需求,同时隔离了第三方用户访问业务代码以外部分的能力,增强了安全性。

这里再分享一个踩到的坑

在业务代码中,如果你的class继承的是DRF的ViewSet并且要验证登录态,那么默认的IsAuthenticated是通不过的,因为我们自定义的JWT解出来的信息里包含的是OAUser的ID而不是django User的ID值,所以还需要继承DRF的BasePermission让系统判断JWT里是否包含正确的OAUserID

"""
permissions.py
"""
from rest_framework.permissions import BasePermission
from dingcan.models import OAUser

class IsOAUserAuthenticated(BasePermission):
    def has_permission(self, request, view):
        return bool(request.user and OAUser.objects.filter(pk=request.user.id).exists())

这样在业务代码中,继承viewset的class应该这样配置授权和鉴权类

"""
views.py
"""

from api.authentication import CustomJWTAuthentication
from api.permissions import CustomPermissions

class OrderRecordViewSets(viewsets.ModelViewSet):
    authentication_classes = [CustomJWTAuthentication]
    permission_classes = [CustomPermissions]

    #do something...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值