这是一个实际工作中遇到的需求,集团有一个第三方的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...
280

被折叠的 条评论
为什么被折叠?



