mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-07 02:10:23 +00:00
perf: 优化用户 access key 的使用和创建 (#11776)
* perf: 优化用户 access key 的使用和创建 * perf: 优化 access key api --------- Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
@@ -1,20 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import UserConfirmation
|
||||
from rbac.permissions import RBACPermission
|
||||
from ..const import ConfirmType
|
||||
from ..serializers import AccessKeySerializer
|
||||
|
||||
|
||||
class AccessKeyViewSet(ModelViewSet):
|
||||
class AccessKeyViewSet(JMSModelViewSet):
|
||||
serializer_class = AccessKeySerializer
|
||||
search_fields = ['^id', '^secret']
|
||||
search_fields = ['^id']
|
||||
permission_classes = [RBACPermission]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.user.access_keys.all()
|
||||
|
||||
def get_permissions(self):
|
||||
if self.is_swagger_request():
|
||||
return super().get_permissions()
|
||||
|
||||
if self.action == 'create':
|
||||
self.permission_classes = [
|
||||
RBACPermission, UserConfirmation.require(ConfirmType.PASSWORD)
|
||||
]
|
||||
return super().get_permissions()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
user = self.request.user
|
||||
user.create_access_key()
|
||||
if user.access_keys.count() >= 10:
|
||||
raise serializers.ValidationError(_('Access keys can be created at most 10'))
|
||||
key = user.create_access_key()
|
||||
return key
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
key = self.perform_create(serializer)
|
||||
return Response({'secret': key.secret, 'id': key.id}, status=201)
|
||||
|
@@ -13,7 +13,7 @@ from ..serializers import ConfirmSerializer
|
||||
|
||||
|
||||
class ConfirmBindORUNBindOAuth(RetrieveAPIView):
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
return Response('ok')
|
||||
@@ -24,7 +24,7 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView):
|
||||
serializer_class = ConfirmSerializer
|
||||
|
||||
def get_confirm_backend(self, confirm_type):
|
||||
backend_classes = ConfirmType.get_can_confirm_backend_classes(confirm_type)
|
||||
backend_classes = ConfirmType.get_prop_backends(confirm_type)
|
||||
if not backend_classes:
|
||||
return
|
||||
for backend_cls in backend_classes:
|
||||
@@ -34,7 +34,7 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView):
|
||||
return backend
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
confirm_type = request.query_params.get('confirm_type')
|
||||
confirm_type = request.query_params.get('confirm_type', 'password')
|
||||
backend = self.get_confirm_backend(confirm_type)
|
||||
if backend is None:
|
||||
msg = _('This action require verify your MFA')
|
||||
@@ -51,7 +51,7 @@ class ConfirmApi(RetrieveAPIView, CreateAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
validated_data = serializer.validated_data
|
||||
|
||||
confirm_type = validated_data.get('confirm_type')
|
||||
confirm_type = validated_data.get('confirm_type', 'password')
|
||||
mfa_type = validated_data.get('mfa_type')
|
||||
secret_key = validated_data.get('secret_key')
|
||||
|
||||
|
@@ -27,7 +27,7 @@ class DingTalkQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
|
||||
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
|
@@ -27,7 +27,7 @@ class FeiShuQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
|
||||
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
|
@@ -27,7 +27,7 @@ class WeComQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),)
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
|
||||
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
|
@@ -1,119 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import authentication, exceptions
|
||||
from six import text_type
|
||||
|
||||
from common.auth import signature
|
||||
from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
||||
from .base import JMSBaseAuthBackend
|
||||
from common.utils import get_object_or_none
|
||||
from ..models import AccessKey, PrivateToken
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
def date_more_than(d, seconds):
|
||||
return d is None or (timezone.now() - d).seconds > seconds
|
||||
|
||||
|
||||
def get_request_date_header(request):
|
||||
date = request.META.get('HTTP_DATE', b'')
|
||||
if isinstance(date, text_type):
|
||||
# Work around django test client oddness
|
||||
date = date.encode(HTTP_HEADER_ENCODING)
|
||||
return date
|
||||
def after_authenticate_update_date(user, token=None):
|
||||
if date_more_than(user.date_api_key_last_used, 60):
|
||||
user.date_api_key_last_used = timezone.now()
|
||||
user.save(update_fields=['date_api_key_last_used'])
|
||||
|
||||
|
||||
class AccessKeyAuthentication(authentication.BaseAuthentication):
|
||||
"""App使用Access key进行签名认证, 目前签名算法比较简单,
|
||||
app注册或者手动建立后,会生成 access_key_id 和 access_key_secret,
|
||||
然后使用 如下算法生成签名:
|
||||
Signature = md5(access_key_secret + '\n' + Date)
|
||||
example: Signature = md5('d32d2b8b-9a10-4b8d-85bb-1a66976f6fdc' + '\n' +
|
||||
'Thu, 12 Jan 2017 08:19:41 GMT')
|
||||
请求时设置请求header
|
||||
header['Authorization'] = 'Sign access_key_id:Signature' 如:
|
||||
header['Authorization'] =
|
||||
'Sign d32d2b8b-9a10-4b8d-85bb-1a66976f6fdc:OKOlmdxgYPZ9+SddnUUDbQ=='
|
||||
|
||||
验证时根据相同算法进行验证, 取到access_key_id对应的access_key_id, 从request
|
||||
headers取到Date, 然后进行md5, 判断得到的结果是否相同, 如果是认证通过, 否则 认证
|
||||
失败
|
||||
"""
|
||||
keyword = 'Sign'
|
||||
|
||||
def authenticate(self, request):
|
||||
auth = authentication.get_authorization_header(request).split()
|
||||
if not auth or auth[0].lower() != self.keyword.lower().encode():
|
||||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
msg = _('Invalid signature header. No credentials provided.')
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
elif len(auth) > 2:
|
||||
msg = _('Invalid signature header. Signature '
|
||||
'string should not contain spaces.')
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
try:
|
||||
sign = auth[1].decode().split(':')
|
||||
if len(sign) != 2:
|
||||
msg = _('Invalid signature header. '
|
||||
'Format like AccessKeyId:Signature')
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
except UnicodeError:
|
||||
msg = _('Invalid signature header. '
|
||||
'Signature string should not contain invalid characters.')
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
access_key_id = sign[0]
|
||||
try:
|
||||
uuid.UUID(access_key_id)
|
||||
except ValueError:
|
||||
raise exceptions.AuthenticationFailed('Access key id invalid')
|
||||
request_signature = sign[1]
|
||||
|
||||
return self.authenticate_credentials(
|
||||
request, access_key_id, request_signature
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def authenticate_credentials(request, access_key_id, request_signature):
|
||||
access_key = get_object_or_none(AccessKey, id=access_key_id)
|
||||
request_date = get_request_date_header(request)
|
||||
if access_key is None or not access_key.user:
|
||||
raise exceptions.AuthenticationFailed(_('Invalid signature.'))
|
||||
access_key_secret = access_key.secret
|
||||
|
||||
try:
|
||||
request_unix_time = http_to_unixtime(request_date)
|
||||
except ValueError:
|
||||
raise exceptions.AuthenticationFailed(
|
||||
_('HTTP header: Date not provide '
|
||||
'or not %a, %d %b %Y %H:%M:%S GMT'))
|
||||
|
||||
if int(time.time()) - request_unix_time > 15 * 60:
|
||||
raise exceptions.AuthenticationFailed(
|
||||
_('Expired, more than 15 minutes'))
|
||||
|
||||
signature = make_signature(access_key_secret, request_date)
|
||||
if not signature == request_signature:
|
||||
raise exceptions.AuthenticationFailed(_('Invalid signature.'))
|
||||
|
||||
if not access_key.user.is_active:
|
||||
raise exceptions.AuthenticationFailed(_('User disabled.'))
|
||||
return access_key.user, None
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return 'Sign access_key_id:Signature'
|
||||
if token and hasattr(token, 'date_last_used') and date_more_than(token.date_last_used, 60):
|
||||
token.date_last_used = timezone.now()
|
||||
token.save(update_fields=['date_last_used'])
|
||||
|
||||
|
||||
class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||
keyword = 'Bearer'
|
||||
# expiration = settings.TOKEN_EXPIRATION or 3600
|
||||
model = get_user_model()
|
||||
|
||||
def authenticate(self, request):
|
||||
@@ -125,19 +39,20 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||
msg = _('Invalid token header. No credentials provided.')
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
elif len(auth) > 2:
|
||||
msg = _('Invalid token header. Sign string '
|
||||
'should not contain spaces.')
|
||||
msg = _('Invalid token header. Sign string should not contain spaces.')
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
try:
|
||||
token = auth[1].decode()
|
||||
except UnicodeError:
|
||||
msg = _('Invalid token header. Sign string '
|
||||
'should not contain invalid characters.')
|
||||
msg = _('Invalid token header. Sign string should not contain invalid characters.')
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
return self.authenticate_credentials(token)
|
||||
user, header = self.authenticate_credentials(token)
|
||||
after_authenticate_update_date(user)
|
||||
return user, header
|
||||
|
||||
def authenticate_credentials(self, token):
|
||||
@staticmethod
|
||||
def authenticate_credentials(token):
|
||||
model = get_user_model()
|
||||
user_id = cache.get(token)
|
||||
user = get_object_or_none(model, id=user_id)
|
||||
@@ -151,15 +66,23 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||
return self.keyword
|
||||
|
||||
|
||||
class PrivateTokenAuthentication(JMSBaseAuthBackend, authentication.TokenAuthentication):
|
||||
class PrivateTokenAuthentication(authentication.TokenAuthentication):
|
||||
model = PrivateToken
|
||||
|
||||
def authenticate(self, request):
|
||||
user_token = super().authenticate(request)
|
||||
if not user_token:
|
||||
return
|
||||
user, token = user_token
|
||||
after_authenticate_update_date(user, token)
|
||||
return user, token
|
||||
|
||||
|
||||
class SessionAuthentication(authentication.SessionAuthentication):
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Returns a `User` if the request session currently has a logged in user.
|
||||
Otherwise returns `None`.
|
||||
Otherwise, returns `None`.
|
||||
"""
|
||||
|
||||
# Get the session-based user from the underlying HttpRequest object
|
||||
@@ -195,6 +118,7 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
||||
if not key.is_active:
|
||||
return None, None
|
||||
user, secret = key.user, str(key.secret)
|
||||
after_authenticate_update_date(user, key)
|
||||
return user, secret
|
||||
except (AccessKey.DoesNotExist, exceptions.ValidationError):
|
||||
return None, None
|
||||
|
@@ -2,7 +2,6 @@ import abc
|
||||
|
||||
|
||||
class BaseConfirm(abc.ABC):
|
||||
|
||||
def __init__(self, user, request):
|
||||
self.user = user
|
||||
self.request = request
|
||||
@@ -23,7 +22,7 @@ class BaseConfirm(abc.ABC):
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return ''
|
||||
return []
|
||||
|
||||
@abc.abstractmethod
|
||||
def authenticate(self, secret_key, mfa_type) -> tuple:
|
||||
|
@@ -15,3 +15,14 @@ class ConfirmPassword(BaseConfirm):
|
||||
ok = authenticate(self.request, username=self.user.username, password=secret_key)
|
||||
msg = '' if ok else _('Authentication failed password incorrect')
|
||||
return ok, msg
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return [
|
||||
{
|
||||
'name': 'password',
|
||||
'display_name': _('Password'),
|
||||
'disabled': False,
|
||||
'placeholder': _('Password'),
|
||||
}
|
||||
]
|
||||
|
@@ -11,7 +11,7 @@ CONFIRM_BACKEND_MAP = {backend.name: backend for backend in CONFIRM_BACKENDS}
|
||||
|
||||
|
||||
class ConfirmType(TextChoices):
|
||||
ReLogin = ConfirmReLogin.name, ConfirmReLogin.display_name
|
||||
RELOGIN = ConfirmReLogin.name, ConfirmReLogin.display_name
|
||||
PASSWORD = ConfirmPassword.name, ConfirmPassword.display_name
|
||||
MFA = ConfirmMFA.name, ConfirmMFA.display_name
|
||||
|
||||
@@ -23,10 +23,11 @@ class ConfirmType(TextChoices):
|
||||
return types
|
||||
|
||||
@classmethod
|
||||
def get_can_confirm_backend_classes(cls, confirm_type):
|
||||
def get_prop_backends(cls, confirm_type):
|
||||
types = cls.get_can_confirm_types(confirm_type)
|
||||
backend_classes = [
|
||||
CONFIRM_BACKEND_MAP[tp] for tp in types if tp in CONFIRM_BACKEND_MAP
|
||||
CONFIRM_BACKEND_MAP[tp]
|
||||
for tp in types if tp in CONFIRM_BACKEND_MAP
|
||||
]
|
||||
return backend_classes
|
||||
|
||||
|
57
apps/authentication/migrations/0023_auto_20231010_1101.py
Normal file
57
apps/authentication/migrations/0023_auto_20231010_1101.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Generated by Django 4.1.10 on 2023-10-10 02:47
|
||||
|
||||
import uuid
|
||||
import authentication.models.access_key
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_access_key_secret(apps, schema_editor):
|
||||
access_key_model = apps.get_model('authentication', 'AccessKey')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
batch_size = 100
|
||||
count = 0
|
||||
|
||||
while True:
|
||||
access_keys = access_key_model.objects.using(db_alias).all()[count:count + batch_size]
|
||||
if not access_keys:
|
||||
break
|
||||
|
||||
count += len(access_keys)
|
||||
access_keys_updated = []
|
||||
for access_key in access_keys:
|
||||
s = access_key.secret
|
||||
if len(s) != 32 or not s.islower():
|
||||
continue
|
||||
try:
|
||||
access_key.secret = '%s-%s-%s-%s-%s' % (s[:8], s[8:12], s[12:16], s[16:20], s[20:])
|
||||
access_keys_updated.append(access_key)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
access_key_model.objects.bulk_update(access_keys_updated, fields=['secret'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0022_passkey'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='accesskey',
|
||||
name='date_last_used',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Date last used'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='privatetoken',
|
||||
name='date_last_used',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Date last used'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='accesskey',
|
||||
name='secret',
|
||||
field=models.CharField(default=authentication.models.access_key.default_secret, max_length=36, verbose_name='AccessKeySecret'),
|
||||
),
|
||||
migrations.RunPython(migrate_access_key_secret),
|
||||
]
|
@@ -5,16 +5,20 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import common.db.models
|
||||
from common.utils.random import random_string
|
||||
|
||||
|
||||
def default_secret():
|
||||
return random_string(36)
|
||||
|
||||
|
||||
class AccessKey(models.Model):
|
||||
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True,
|
||||
default=uuid.uuid4, editable=False)
|
||||
secret = models.UUIDField(verbose_name='AccessKeySecret',
|
||||
default=uuid.uuid4, editable=False)
|
||||
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False)
|
||||
secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
|
||||
on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys')
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
||||
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def get_id(self):
|
||||
|
@@ -1,9 +1,11 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
|
||||
class PrivateToken(Token):
|
||||
"""Inherit from auth token, otherwise migration is boring"""
|
||||
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Private Token')
|
||||
|
@@ -10,7 +10,7 @@ from users.serializers import UserProfileSerializer
|
||||
from ..models import AccessKey, TempToken
|
||||
|
||||
__all__ = [
|
||||
'AccessKeySerializer', 'BearerTokenSerializer',
|
||||
'AccessKeySerializer', 'BearerTokenSerializer',
|
||||
'SSOTokenSerializer', 'TempTokenSerializer',
|
||||
]
|
||||
|
||||
@@ -18,8 +18,8 @@ __all__ = [
|
||||
class AccessKeySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AccessKey
|
||||
fields = ['id', 'secret', 'is_active', 'date_created']
|
||||
read_only_fields = ['id', 'secret', 'date_created']
|
||||
fields = ['id', 'is_active', 'date_created', 'date_last_used']
|
||||
read_only_fields = ['id', 'date_created', 'date_last_used']
|
||||
|
||||
|
||||
class BearerTokenSerializer(serializers.Serializer):
|
||||
@@ -37,7 +37,8 @@ class BearerTokenSerializer(serializers.Serializer):
|
||||
def get_keyword(obj):
|
||||
return 'Bearer'
|
||||
|
||||
def update_last_login(self, user):
|
||||
@staticmethod
|
||||
def update_last_login(user):
|
||||
user.last_login = timezone.now()
|
||||
user.save(update_fields=['last_login'])
|
||||
|
||||
@@ -96,7 +97,7 @@ class TempTokenSerializer(serializers.ModelSerializer):
|
||||
username = request.user.username
|
||||
kwargs = {
|
||||
'username': username, 'secret': secret,
|
||||
'date_expired': timezone.now() + timezone.timedelta(seconds=5*60),
|
||||
'date_expired': timezone.now() + timezone.timedelta(seconds=5 * 60),
|
||||
}
|
||||
token = TempToken(**kwargs)
|
||||
token.save()
|
||||
|
0
apps/authentication/tests/__init__.py
Normal file
0
apps/authentication/tests/__init__.py
Normal file
34
apps/authentication/tests/access_key.py
Normal file
34
apps/authentication/tests/access_key.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Python 示例
|
||||
# pip install requests drf-httpsig
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import requests
|
||||
from httpsig.requests_auth import HTTPSignatureAuth
|
||||
|
||||
|
||||
def get_auth(KeyID, SecretID):
|
||||
signature_headers = ['(request-target)', 'accept', 'date']
|
||||
auth = HTTPSignatureAuth(key_id=KeyID, secret=SecretID, algorithm='hmac-sha256', headers=signature_headers)
|
||||
return auth
|
||||
|
||||
|
||||
def get_user_info(jms_url, auth):
|
||||
url = jms_url + '/api/v1/users/users/?limit=1'
|
||||
gmt_form = '%a, %d %b %Y %H:%M:%S GMT'
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'X-JMS-ORG': '00000000-0000-0000-0000-000000000002',
|
||||
'Date': datetime.datetime.utcnow().strftime(gmt_form)
|
||||
}
|
||||
|
||||
response = requests.get(url, auth=auth, headers=headers)
|
||||
print(json.loads(response.text))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
jms_url = 'http://localhost:8080'
|
||||
KeyID = '0753098d-810c-45fb-b42c-b27077147933'
|
||||
SecretID = 'a58d2530-d7ee-4390-a204-3492e44dde84'
|
||||
auth = get_auth(KeyID, SecretID)
|
||||
get_user_info(jms_url, auth)
|
@@ -99,7 +99,7 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View):
|
||||
|
||||
|
||||
class DingTalkQRBindView(DingTalkQRMixin, View):
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN))
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
|
@@ -69,7 +69,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe
|
||||
|
||||
|
||||
class FeiShuQRBindView(FeiShuQRMixin, View):
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN))
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
@@ -100,7 +100,7 @@ class WeComOAuthMixin(WeComBaseMixin, View):
|
||||
|
||||
|
||||
class WeComQRBindView(WeComQRMixin, View):
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
|
||||
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN))
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
|
Reference in New Issue
Block a user