From 708a87c903384865adf966d37b02306474eada1b Mon Sep 17 00:00:00 2001
From: jiangweidong <80373698+F2C-Jiang@users.noreply.github.com>
Date: Tue, 9 Aug 2022 16:09:20 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81CMPPv2.0=E5=8D=8F?=
 =?UTF-8?q?=E8=AE=AE=E7=9F=AD=E4=BF=A1=E7=BD=91=E5=85=B3=20(#8591)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 支持CMPPv2.0协议短信网关
* 修改翻译
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
---
 apps/common/sdk/sms/base.py           |   4 +
 apps/common/sdk/sms/cmpp2.py          | 322 ++++++++++++++++++++++++++
 apps/common/sdk/sms/endpoint.py       |   3 +-
 apps/jumpserver/conf.py               |   9 +
 apps/locale/ja/LC_MESSAGES/django.po  | 153 +++++++-----
 apps/locale/zh/LC_MESSAGES/django.po  | 152 +++++++-----
 apps/settings/api/__init__.py         |   2 -
 apps/settings/api/alibaba_sms.py      |  58 -----
 apps/settings/api/settings.py         |   1 +
 apps/settings/api/sms.py              | 118 +++++++++-
 apps/settings/api/tencent_sms.py      |  63 -----
 apps/settings/serializers/auth/sms.py |  33 ++-
 apps/settings/serializers/settings.py |   3 +-
 apps/settings/urls/api_urls.py        |   3 +-
 14 files changed, 686 insertions(+), 238 deletions(-)
 create mode 100644 apps/common/sdk/sms/cmpp2.py
 delete mode 100644 apps/settings/api/alibaba_sms.py
 delete mode 100644 apps/settings/api/tencent_sms.py
diff --git a/apps/common/sdk/sms/base.py b/apps/common/sdk/sms/base.py
index 4d02370b1..77dcc669a 100644
--- a/apps/common/sdk/sms/base.py
+++ b/apps/common/sdk/sms/base.py
@@ -17,4 +17,8 @@ class BaseSMSClient:
     def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
         raise NotImplementedError
 
+    @staticmethod
+    def need_pre_check():
+        return True
+
 
diff --git a/apps/common/sdk/sms/cmpp2.py b/apps/common/sdk/sms/cmpp2.py
new file mode 100644
index 000000000..8a6a4fe32
--- /dev/null
+++ b/apps/common/sdk/sms/cmpp2.py
@@ -0,0 +1,322 @@
+import hashlib
+import socket
+import struct
+import time
+
+from django.conf import settings
+
+from common.utils import get_logger
+from common.exceptions import JMSException
+from .base import BaseSMSClient
+
+
+logger = get_logger(__file__)
+
+
+CMPP_CONNECT = 0x00000001  # 请求连接
+CMPP_CONNECT_RESP = 0x80000001  # 请求连接应答
+CMPP_TERMINATE = 0x00000002  # 终止连接
+CMPP_TERMINATE_RESP = 0x80000002  # 终止连接应答
+CMPP_SUBMIT = 0x00000004  # 提交短信
+CMPP_SUBMIT_RESP = 0x80000004  # 提交短信应答
+CMPP_DELIVER = 0x00000005  # 短信下发
+CMPP_DELIVER_RESP = 0x80000005  # 下发短信应答
+
+
+class CMPPBaseRequestInstance(object):
+    def __init__(self):
+        self.command_id = ''
+        self.body = b''
+        self.length = 0
+
+    def get_header(self, sequence_id):
+        length = struct.pack('!L', 12 + self.length)
+        command_id = struct.pack('!L', self.command_id)
+        sequence_id = struct.pack('!L', sequence_id)
+        return length + command_id + sequence_id
+
+    def get_message(self, sequence_id):
+        return self.get_header(sequence_id) + self.body
+
+
+class CMPPConnectRequestInstance(CMPPBaseRequestInstance):
+    def __init__(self, sp_id, sp_secret):
+        if len(sp_id) != 6:
+            raise ValueError("sp_id and sp_secret are both 6 bits")
+
+        super().__init__()
+
+        source_addr = sp_id.encode('utf-8')
+        sp_secret = sp_secret.encode('utf-8')
+        version = struct.pack('!B', 0x02)
+        timestamp = struct.pack('!L', int(self.get_now()))
+        authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8')
+        auth_source_md5 = hashlib.md5(authenticator_source).digest()
+        self.body = source_addr + auth_source_md5 + version + timestamp
+        self.length = len(self.body)
+        self.command_id = CMPP_CONNECT
+
+    @staticmethod
+    def get_now():
+        return time.strftime('%m%d%H%M%S', time.localtime(time.time()))
+
+
+class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
+    def __init__(self, msg_src, dest_terminal_id, msg_content, src_id,
+                 service_id='', dest_usr_tl=1):
+        if len(msg_content) >= 70:
+            raise JMSException('The message length should be within 70 characters')
+        if len(dest_terminal_id) > 100:
+            raise JMSException('The number of users receiving information should be less than 100')
+
+        super().__init__()
+
+        msg_id = 8 * b'\x00'
+        pk_total = struct.pack('!B', 1)
+        pk_number = struct.pack('!B', 1)
+        registered_delivery = struct.pack('!B', 0)
+        msg_level = struct.pack('!B', 0)
+        service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8')
+        fee_user_type = struct.pack('!B', 2)
+        fee_terminal_id = ('0' * 21).encode('utf-8')
+        tp_pid = struct.pack('!B', 0)
+        tp_udhi = struct.pack('!B', 0)
+        msg_fmt = struct.pack('!B', 8)
+        fee_type = '01'.encode('utf-8')
+        fee_code = '000000'.encode('utf-8')
+        valid_time = ('\x00' * 17).encode('utf-8')
+        at_time = ('\x00' * 17).encode('utf-8')
+        src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8')
+        reserve = b'\x00' * 8
+        _msg_length = struct.pack('!B', len(msg_content) * 2)
+        _msg_src = msg_src.encode('utf-8')
+        _dest_usr_tl = struct.pack('!B', dest_usr_tl)
+        _msg_content = msg_content.encode('utf-16-be')
+        _dest_terminal_id = b''.join([
+            (i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id
+        ])
+        self.length = 126 + 21 * dest_usr_tl + len(_msg_content)
+        self.command_id = CMPP_SUBMIT
+        self.body = msg_id + pk_total + pk_number + registered_delivery \
+            + msg_level + service_id + fee_user_type + fee_terminal_id \
+            + tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \
+            + valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \
+            + _msg_length + _msg_content + reserve
+
+
+class CMPPTerminateRequestInstance(CMPPBaseRequestInstance):
+    def __init__(self):
+        super().__init__()
+        self.body = b''
+        self.command_id = CMPP_TERMINATE
+
+
+class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance):
+    def __init__(self, msg_id, result=0):
+        super().__init__()
+        msg_id = struct.pack('!Q', msg_id)
+        result = struct.pack('!B', result)
+        self.length = len(self.body)
+        self.body = msg_id + result
+
+
+class CMPPResponseInstance(object):
+    def __init__(self):
+        self.command_id = None
+        self.length = None
+        self.response_handler_map = {
+            CMPP_CONNECT_RESP: self.connect_response_parse,
+            CMPP_SUBMIT_RESP: self.submit_response_parse,
+            CMPP_DELIVER: self.deliver_request_parse,
+        }
+
+    @staticmethod
+    def connect_response_parse(body):
+        status, = struct.unpack('!B', body[0:1])
+        authenticator_ISMG = body[1:17]
+        version, = struct.unpack('!B', body[17:18])
+        return {
+            'Status': status,
+            'AuthenticatorISMG': authenticator_ISMG,
+            'Version': version
+        }
+
+    @staticmethod
+    def submit_response_parse(body):
+        msg_id = body[:8]
+        result = struct.unpack('!B', body[8:9])
+        return {
+            'Msg_Id': msg_id, 'Result': result[0]
+        }
+
+    @staticmethod
+    def deliver_request_parse(body):
+        msg_id, = struct.unpack('!Q', body[0:8])
+        dest_id = body[8:29]
+        service_id = body[29:39]
+        tp_pid = struct.unpack('!B', body[39:40])
+        tp_udhi = struct.unpack('!B', body[40:41])
+        msg_fmt = struct.unpack('!B', body[41:42])
+        src_terminal_id = body[42:63]
+        registered_delivery = struct.unpack('!B', body[63:64])
+        msg_length = struct.unpack('!B', body[64:65])
+        msg_content = body[65:msg_length[0]+65]
+        return {
+            'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id,
+            'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt,
+            'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery,
+            'Msg_Length': msg_length, 'Msg_content': msg_content
+        }
+
+    def parse_header(self, data):
+        self.command_id, = struct.unpack('!L', data[4:8])
+        sequence_id, = struct.unpack('!L', data[8:12])
+        return {
+            'length': self.length,
+            'command_id': hex(self.command_id),
+            'sequence_id': sequence_id
+        }
+
+    def parse_body(self, body):
+        response_body_func = self.response_handler_map.get(self.command_id)
+        if response_body_func is None:
+            raise JMSException('Unable to parse the returned result: %s' % body)
+        return response_body_func(body)
+
+    def parse(self, data):
+        self.length, = struct.unpack('!L', data[0:4])
+        header = self.parse_header(data)
+        body = self.parse_body(data[12:self.length])
+        return header, body
+
+
+class CMPPClient(object):
+    def __init__(self, host, port, sp_id, sp_secret, src_id, service_id):
+        self.ip = host
+        self.port = port
+        self.sp_id = sp_id
+        self.sp_secret = sp_secret
+        self.src_id = src_id
+        self.service_id = service_id
+        self._sequence_id = 0
+        self._is_connect = False
+        self._times = 3
+        self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self._connect()
+
+    @property
+    def sequence_id(self):
+        s = self._sequence_id
+        self._sequence_id += 1
+        return s
+
+    def _connect(self):
+        self.__socket.settimeout(5)
+        for i in range(self._times):
+            try:
+                self.__socket.connect((self.ip, self.port))
+            except socket.timeout:
+                time.sleep(1)
+            else:
+                self._is_connect = True
+                break
+
+    def send(self, instance):
+        if isinstance(instance, CMPPBaseRequestInstance):
+            message = instance.get_message(sequence_id=self.sequence_id)
+        else:
+            message = instance
+        self.__socket.send(message)
+
+    def recv(self):
+        raw_length = self.__socket.recv(4)
+        length, = struct.unpack('!L', raw_length)
+        header, body = CMPPResponseInstance().parse(
+            raw_length + self.__socket.recv(length - 4)
+        )
+        return header, body
+
+    def close(self):
+        if self._is_connect:
+            terminate_request = CMPPTerminateRequestInstance()
+            self.send(terminate_request)
+            self.__socket.close()
+
+    def _cmpp_connect(self):
+        connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret)
+        self.send(connect_request)
+        header, body = self.recv()
+        if body['Status'] != 0:
+            raise JMSException('CMPPv2.0 authentication failed: %s' % body)
+
+    def _cmpp_send_sms(self, dest, sign_name, template_code, template_param):
+        """
+        优先发送template_param中message的信息
+        若该内容不存在,则根据template_code构建验证码发送
+        """
+        message = template_param.get('message')
+        if message is None:
+            code = template_param.get('code')
+            message = template_code.replace('{code}', code)
+        msg = '【%s】 %s' % (sign_name, message)
+        submit_request = CMPPSubmitRequestInstance(
+            msg_src=self.sp_id, src_id=self.src_id, msg_content=msg,
+            dest_usr_tl=len(dest), dest_terminal_id=dest,
+            service_id=self.service_id
+        )
+        self.send(submit_request)
+        header, body = self.recv()
+        command_id = header.get('command_id')
+        if command_id == CMPP_DELIVER:
+            deliver_request = CMPPDeliverRespRequestInstance(
+                msg_id=body['Msg_Id'], result=body['Result']
+            )
+            self.send(deliver_request)
+
+    def send_sms(self, dest, sign_name, template_code, template_param):
+        try:
+            self._cmpp_connect()
+            self._cmpp_send_sms(dest, sign_name, template_code, template_param)
+        except Exception as e:
+            logger.error('CMPPv2.0 Error: %s', e)
+            self.close()
+            raise JMSException(e)
+
+
+class CMPP2SMS(BaseSMSClient):
+    SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2'
+
+    @classmethod
+    def new_from_settings(cls):
+        return cls(
+            host=settings.CMPP2_HOST, port=settings.CMPP2_PORT,
+            sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET,
+            service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''),
+        )
+
+    def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''):
+        try:
+            self.client = CMPPClient(
+                host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id
+            )
+        except socket.timeout:
+            self.client = None
+            logger.warning('CMPPv2.0 connect remote time out.')
+
+    @staticmethod
+    def need_pre_check():
+        return False
+
+    def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
+        try:
+            logger.info(f'CMPPv2.0 sms send: '
+                        f'phone_numbers={phone_numbers} '
+                        f'sign_name={sign_name} '
+                        f'template_code={template_code} '
+                        f'template_param={template_param}')
+            self.client.send_sms(phone_numbers, sign_name, template_code, template_param)
+        except Exception as e:
+            raise JMSException(e)
+
+
+client = CMPP2SMS
diff --git a/apps/common/sdk/sms/endpoint.py b/apps/common/sdk/sms/endpoint.py
index 610bf2d99..3bcaa8559 100644
--- a/apps/common/sdk/sms/endpoint.py
+++ b/apps/common/sdk/sms/endpoint.py
@@ -15,6 +15,7 @@ logger = get_logger(__name__)
 class BACKENDS(TextChoices):
     ALIBABA = 'alibaba', _('Alibaba cloud')
     TENCENT = 'tencent', _('Tencent cloud')
+    CMPP2 = 'cmpp2', _('CMPP v2.0')
 
 
 class SMS:
@@ -43,7 +44,7 @@ class SMS:
         sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
         template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
 
-        if not (sign_name and template_code):
+        if self.client.need_pre_check() and not (sign_name and template_code):
             raise JMSException(
                 code='verify_code_sign_tmpl_invalid',
                 detail=_('SMS verification code signature or template invalid')
diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py
index 78e73ba6c..7b26b4ee2 100644
--- a/apps/jumpserver/conf.py
+++ b/apps/jumpserver/conf.py
@@ -376,6 +376,15 @@ class Config(dict):
         'TENCENT_VERIFY_SIGN_NAME': '',
         'TENCENT_VERIFY_TEMPLATE_CODE': '',
 
+        'CMPP2_HOST': '',
+        'CMPP2_PORT': 7890,
+        'CMPP2_SP_ID': '',
+        'CMPP2_SP_SECRET': '',
+        'CMPP2_SRC_ID': '',
+        'CMPP2_SERVICE_ID': '',
+        'CMPP2_VERIFY_SIGN_NAME': '',
+        'CMPP2_VERIFY_TEMPLATE_CODE': '{code}',
+
         # Email
         'EMAIL_CUSTOM_USER_CREATED_SUBJECT': _('Create account successfully'),
         'EMAIL_CUSTOM_USER_CREATED_HONORIFIC': _('Hello'),
diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po
index a1ae87cba..fb5b39b2f 100644
--- a/apps/locale/ja/LC_MESSAGES/django.po
+++ b/apps/locale/ja/LC_MESSAGES/django.po
@@ -88,8 +88,8 @@ msgstr "ログイン確認"
 #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20
 #: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37
 #: audits/models.py:62 audits/models.py:87 audits/serializers.py:100
-#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220
-#: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41
+#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:214
+#: perms/models/base.py:84 rbac/builtin.py:117 rbac/models/rolebinding.py:41
 #: terminal/backends/command/models.py:20
 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:44
 #: terminal/models/sharing.py:33 terminal/notifications.py:91
@@ -393,8 +393,8 @@ msgstr "クラスター"
 
 #: applications/serializers/attrs/application_category/db.py:11
 #: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14
-#: terminal/models/endpoint.py:11
-#: xpack/plugins/cloud/serializers/account_attrs.py:72
+#: settings/serializers/auth/sms.py:52 terminal/models/endpoint.py:11
+#: xpack/plugins/cloud/serializers/account_attrs.py:70
 msgid "Host"
 msgstr "ホスト"
 
@@ -407,8 +407,8 @@ msgstr "ホスト"
 #: applications/serializers/attrs/application_type/redis.py:10
 #: applications/serializers/attrs/application_type/sqlserver.py:10
 #: assets/models/asset.py:214 assets/models/domain.py:62
-#: settings/serializers/auth/radius.py:15
-#: xpack/plugins/cloud/serializers/account_attrs.py:73
+#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:53
+#: xpack/plugins/cloud/serializers/account_attrs.py:71
 msgid "Port"
 msgstr "ポート"
 
@@ -591,7 +591,7 @@ msgstr "ホスト名生"
 
 #: assets/models/asset.py:215 assets/serializers/account.py:16
 #: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:41
-#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:43
+#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:42
 msgid "Protocols"
 msgstr "プロトコル"
 
@@ -990,7 +990,7 @@ msgid "Parent key"
 msgstr "親キー"
 
 #: assets/models/node.py:559 assets/serializers/system_user.py:267
-#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:70
+#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:69
 msgid "Node"
 msgstr "ノード"
 
@@ -2318,7 +2318,7 @@ msgstr "コードエラー"
 #: authentication/templates/authentication/_msg_reset_password.html:3
 #: authentication/templates/authentication/_msg_rest_password_success.html:2
 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:323 ops/tasks.py:145 ops/tasks.py:148
+#: jumpserver/conf.py:316 ops/tasks.py:145 ops/tasks.py:148
 #: perms/templates/perms/_msg_item_permissions_expire.html:3
 #: perms/templates/perms/_msg_permed_items_expire.html:3
 #: tickets/templates/tickets/approve_check_password.html:33
@@ -2723,11 +2723,15 @@ msgstr "アリ雲"
 msgid "Tencent cloud"
 msgstr "テンセント雲"
 
-#: common/sdk/sms/endpoint.py:28
+#: common/sdk/sms/endpoint.py:18
+msgid "CMPP v2.0"
+msgstr "CMPP v2.0"
+
+#: common/sdk/sms/endpoint.py:29
 msgid "SMS provider not support: {}"
 msgstr "SMSプロバイダーはサポートしていません: {}"
 
-#: common/sdk/sms/endpoint.py:49
+#: common/sdk/sms/endpoint.py:50
 msgid "SMS verification code signature or template invalid"
 msgstr "SMS検証コードの署名またはテンプレートが無効"
 
@@ -2763,11 +2767,11 @@ msgstr "特殊文字を含むべきではない"
 msgid "The mobile phone number format is incorrect"
 msgstr "携帯電話番号の形式が正しくありません"
 
-#: jumpserver/conf.py:322
+#: jumpserver/conf.py:315
 msgid "Create account successfully"
 msgstr "アカウントを正常に作成"
 
-#: jumpserver/conf.py:324
+#: jumpserver/conf.py:317
 msgid "Your account has been created successfully"
 msgstr "アカウントが正常に作成されました"
 
@@ -3026,8 +3030,8 @@ msgstr "組織のリソース ({}) は削除できません"
 msgid "App organizations"
 msgstr "アプリ組織"
 
-#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:85
-#: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48
+#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:80
+#: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48
 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62
 #: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71
 msgid "Organization"
@@ -3271,27 +3275,27 @@ msgstr "{} 少なくとも1つのシステムロール"
 msgid "RBAC"
 msgstr "RBAC"
 
-#: rbac/builtin.py:111
+#: rbac/builtin.py:108
 msgid "SystemAdmin"
 msgstr "システム管理者"
 
-#: rbac/builtin.py:114
+#: rbac/builtin.py:111
 msgid "SystemAuditor"
 msgstr "システム監査人"
 
-#: rbac/builtin.py:117
+#: rbac/builtin.py:114
 msgid "SystemComponent"
 msgstr "システムコンポーネント"
 
-#: rbac/builtin.py:123
+#: rbac/builtin.py:120
 msgid "OrgAdmin"
 msgstr "組織管理者"
 
-#: rbac/builtin.py:126
+#: rbac/builtin.py:123
 msgid "OrgAuditor"
 msgstr "監査員を組織する"
 
-#: rbac/builtin.py:129
+#: rbac/builtin.py:126
 msgid "OrgUser"
 msgstr "組織ユーザー"
 
@@ -3463,13 +3467,8 @@ msgstr "権限ツリーの表示"
 msgid "Execute batch command"
 msgstr "バッチ実行コマンド"
 
-#: settings/api/alibaba_sms.py:31 settings/api/tencent_sms.py:35
-msgid "test_phone is required"
-msgstr "携帯番号をテストこのフィールドは必須です"
-
-#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:31
-#: settings/api/feishu.py:36 settings/api/tencent_sms.py:57
-#: settings/api/wecom.py:37
+#: settings/api/dingtalk.py:31 settings/api/feishu.py:36
+#: settings/api/sms.py:131 settings/api/wecom.py:37
 msgid "Test success"
 msgstr "テストの成功"
 
@@ -3497,6 +3496,14 @@ msgstr "Ldapユーザーを取得するにはNone"
 msgid "Imported {} users successfully (Organization: {})"
 msgstr "{} 人のユーザーを正常にインポートしました (組織: {})"
 
+#: settings/api/sms.py:113
+msgid "Invalid SMS platform"
+msgstr "無効なショートメッセージプラットフォーム"
+
+#: settings/api/sms.py:119
+msgid "test_phone is required"
+msgstr "携帯番号をテストこのフィールドは必須です"
+
 #: settings/apps.py:7
 msgid "Settings"
 msgstr "設定"
@@ -3821,29 +3828,69 @@ msgstr "SP プライベートキー"
 msgid "SP cert"
 msgstr "SP 証明書"
 
-#: settings/serializers/auth/sms.py:11
+#: settings/serializers/auth/sms.py:14
 msgid "Enable SMS"
 msgstr "SMSの有効化"
 
-#: settings/serializers/auth/sms.py:13
-msgid "SMS provider"
-msgstr "SMSプロバイダ"
+#: settings/serializers/auth/sms.py:16
+msgid "SMS provider / Protocol"
+msgstr "SMSプロバイダ / プロトコル"
 
-#: settings/serializers/auth/sms.py:18 settings/serializers/auth/sms.py:36
-#: settings/serializers/auth/sms.py:44 settings/serializers/email.py:65
+#: settings/serializers/auth/sms.py:21 settings/serializers/auth/sms.py:39
+#: settings/serializers/auth/sms.py:47 settings/serializers/auth/sms.py:58
+#: settings/serializers/email.py:65
 msgid "Signature"
 msgstr "署名"
 
-#: settings/serializers/auth/sms.py:19 settings/serializers/auth/sms.py:37
-#: settings/serializers/auth/sms.py:45
+#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:40
+#: settings/serializers/auth/sms.py:48
 msgid "Template code"
 msgstr "テンプレートコード"
 
-#: settings/serializers/auth/sms.py:23
+#: settings/serializers/auth/sms.py:26
 msgid "Test phone"
 msgstr "テスト電話"
 
-#: settings/serializers/auth/sso.py:11
+#: settings/serializers/auth/sms.py:54
+msgid "Gateway account(SP id)"
+msgstr "ゲートウェイアカウント(SP id)"
+
+#: settings/serializers/auth/sms.py:55
+msgid "Gateway password(SP secret)"
+msgstr "ゲートウェイパスワード(SP secret)"
+
+#: settings/serializers/auth/sms.py:56
+msgid "Original number(Src id)"
+msgstr "元の番号(Src id)"
+
+#: settings/serializers/auth/sms.py:57
+msgid "Business type(Service id)"
+msgstr "ビジネス・タイプ(Service id)"
+
+#: settings/serializers/auth/sms.py:60
+msgid "Template"
+msgstr "テンプレート"
+
+#: settings/serializers/auth/sms.py:61
+msgid ""
+"Template need contain {code} and Signature + template length does not exceed "
+"67 words. For example, your verification code is {code}, which is valid for "
+"5 minutes. Please do not disclose it to others."
+msgstr ""
+"テンプレートには{code}を含める必要があり、署名+テンプレートの長さは67ワード未"
+"満です。たとえば、認証コードは{code}で、有効期間は5分です。他の人には言わない"
+"でください。"
+
+#: settings/serializers/auth/sms.py:70
+#, python-brace-format
+msgid "The template needs to contain {code}"
+msgstr "テンプレートには{code}を含める必要があります"
+
+#: settings/serializers/auth/sms.py:73
+msgid "Signature + Template must not exceed 65 words"
+msgstr "署名+テンプレートの長さは65文字以内"
+
+#: settings/serializers/auth/sso.py:12
 msgid "Enable SSO auth"
 msgstr "SSO Token認証の有効化"
 
@@ -6460,11 +6507,11 @@ msgstr "クラウドアカウント"
 msgid "Test cloud account"
 msgstr "クラウドアカウントのテスト"
 
-#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:67
+#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:66
 msgid "Account"
 msgstr "アカウント"
 
-#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:38
+#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:37
 msgid "Regions"
 msgstr "リージョン"
 
@@ -6472,19 +6519,19 @@ msgstr "リージョン"
 msgid "Hostname strategy"
 msgstr "ホスト名戦略"
 
-#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:68
+#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:67
 msgid "Unix admin user"
 msgstr "Unix adminユーザー"
 
-#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:69
+#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:68
 msgid "Windows admin user"
 msgstr "Windows管理者"
 
-#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:46
+#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:45
 msgid "IP network segment group"
 msgstr "IPネットワークセグメントグループ"
 
-#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:72
+#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:71
 msgid "Always update"
 msgstr "常に更新"
 
@@ -6790,11 +6837,9 @@ msgstr "テストポート"
 
 #: xpack/plugins/cloud/serializers/task.py:29
 msgid ""
-"Only instances matching the IP range will be synced. 
If the instance "
-"contains multiple IP addresses, the first IP address that matches will be "
-"used as the IP for the created asset. 
The default value of * means sync "
-"all instances and randomly match IP addresses. 
Format for comma-"
-"delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
+"The IP address that is first matched to will be used as the IP of the "
+"created asset. 
The default * indicates a random match. 
Format for "
+"comma-delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
 msgstr ""
 "IP範囲に一致するインスタンスのみが同期されます。
インスタンスに複数のIPア"
 "ドレスが含まれている場合、一致する最初のIPアドレスが作成されたアセットのIPと"
@@ -6802,24 +6847,24 @@ msgstr ""
 "ドレスをランダムに一致させることを意味します。 
形式はコンマ区切りの文字列"
 "です。例:192.168.1.0/24,10.1.1.1-10.1.1.20"
 
-#: xpack/plugins/cloud/serializers/task.py:36
+#: xpack/plugins/cloud/serializers/task.py:35
 msgid "History count"
 msgstr "実行回数"
 
-#: xpack/plugins/cloud/serializers/task.py:37
+#: xpack/plugins/cloud/serializers/task.py:36
 msgid "Instance count"
 msgstr "インスタンス数"
 
-#: xpack/plugins/cloud/serializers/task.py:66
+#: xpack/plugins/cloud/serializers/task.py:65
 msgid "Linux admin user"
 msgstr "Linux管理者"
 
-#: xpack/plugins/cloud/serializers/task.py:71
+#: xpack/plugins/cloud/serializers/task.py:70
 #: xpack/plugins/gathered_user/serializers.py:20
 msgid "Periodic display"
 msgstr "定期的な表示"
 
-#: xpack/plugins/cloud/utils.py:69
+#: xpack/plugins/cloud/utils.py:68
 msgid "Account unavailable"
 msgstr "利用できないアカウント"
 
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index cce71cba7..34da7efb9 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -87,8 +87,8 @@ msgstr "登录复核"
 #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20
 #: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37
 #: audits/models.py:62 audits/models.py:87 audits/serializers.py:100
-#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220
-#: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41
+#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:214
+#: perms/models/base.py:84 rbac/builtin.py:117 rbac/models/rolebinding.py:41
 #: terminal/backends/command/models.py:20
 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:44
 #: terminal/models/sharing.py:33 terminal/notifications.py:91
@@ -388,8 +388,8 @@ msgstr "集群"
 
 #: applications/serializers/attrs/application_category/db.py:11
 #: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14
-#: terminal/models/endpoint.py:11
-#: xpack/plugins/cloud/serializers/account_attrs.py:72
+#: settings/serializers/auth/sms.py:52 terminal/models/endpoint.py:11
+#: xpack/plugins/cloud/serializers/account_attrs.py:70
 msgid "Host"
 msgstr "主机"
 
@@ -402,8 +402,8 @@ msgstr "主机"
 #: applications/serializers/attrs/application_type/redis.py:10
 #: applications/serializers/attrs/application_type/sqlserver.py:10
 #: assets/models/asset.py:214 assets/models/domain.py:62
-#: settings/serializers/auth/radius.py:15
-#: xpack/plugins/cloud/serializers/account_attrs.py:73
+#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:53
+#: xpack/plugins/cloud/serializers/account_attrs.py:71
 msgid "Port"
 msgstr "端口"
 
@@ -586,7 +586,7 @@ msgstr "主机名原始"
 
 #: assets/models/asset.py:215 assets/serializers/account.py:16
 #: assets/serializers/asset.py:65 perms/serializers/asset/user_permission.py:41
-#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:43
+#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers/task.py:42
 msgid "Protocols"
 msgstr "协议组"
 
@@ -985,7 +985,7 @@ msgid "Parent key"
 msgstr "ssh私钥"
 
 #: assets/models/node.py:559 assets/serializers/system_user.py:267
-#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:70
+#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:69
 msgid "Node"
 msgstr "节点"
 
@@ -2293,7 +2293,7 @@ msgstr "代码错误"
 #: authentication/templates/authentication/_msg_reset_password.html:3
 #: authentication/templates/authentication/_msg_rest_password_success.html:2
 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:323 ops/tasks.py:145 ops/tasks.py:148
+#: jumpserver/conf.py:316 ops/tasks.py:145 ops/tasks.py:148
 #: perms/templates/perms/_msg_item_permissions_expire.html:3
 #: perms/templates/perms/_msg_permed_items_expire.html:3
 #: tickets/templates/tickets/approve_check_password.html:33
@@ -2689,11 +2689,15 @@ msgstr "阿里云"
 msgid "Tencent cloud"
 msgstr "腾讯云"
 
-#: common/sdk/sms/endpoint.py:28
+#: common/sdk/sms/endpoint.py:18
+msgid "CMPP v2.0"
+msgstr "CMPP v2.0"
+
+#: common/sdk/sms/endpoint.py:29
 msgid "SMS provider not support: {}"
 msgstr "短信服务商不支持:{}"
 
-#: common/sdk/sms/endpoint.py:49
+#: common/sdk/sms/endpoint.py:50
 msgid "SMS verification code signature or template invalid"
 msgstr "短信验证码签名或模版无效"
 
@@ -2729,11 +2733,11 @@ msgstr "不能包含特殊字符"
 msgid "The mobile phone number format is incorrect"
 msgstr "手机号格式不正确"
 
-#: jumpserver/conf.py:322
+#: jumpserver/conf.py:315
 msgid "Create account successfully"
 msgstr "创建账号成功"
 
-#: jumpserver/conf.py:324
+#: jumpserver/conf.py:317
 msgid "Your account has been created successfully"
 msgstr "你的账号已创建成功"
 
@@ -2986,8 +2990,8 @@ msgstr "组织存在资源 ({}) 不能被删除"
 msgid "App organizations"
 msgstr "组织管理"
 
-#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:85
-#: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48
+#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:80
+#: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48
 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62
 #: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71
 msgid "Organization"
@@ -3229,27 +3233,27 @@ msgstr "{} 至少有一个系统角色"
 msgid "RBAC"
 msgstr "RBAC"
 
-#: rbac/builtin.py:111
+#: rbac/builtin.py:108
 msgid "SystemAdmin"
 msgstr "系统管理员"
 
-#: rbac/builtin.py:114
+#: rbac/builtin.py:111
 msgid "SystemAuditor"
 msgstr "系统审计员"
 
-#: rbac/builtin.py:117
+#: rbac/builtin.py:114
 msgid "SystemComponent"
 msgstr "系统组件"
 
-#: rbac/builtin.py:123
+#: rbac/builtin.py:120
 msgid "OrgAdmin"
 msgstr "组织管理员"
 
-#: rbac/builtin.py:126
+#: rbac/builtin.py:123
 msgid "OrgAuditor"
 msgstr "组织审计员"
 
-#: rbac/builtin.py:129
+#: rbac/builtin.py:126
 msgid "OrgUser"
 msgstr "组织用户"
 
@@ -3420,13 +3424,8 @@ msgstr "查看授权树"
 msgid "Execute batch command"
 msgstr "执行批量命令"
 
-#: settings/api/alibaba_sms.py:31 settings/api/tencent_sms.py:35
-msgid "test_phone is required"
-msgstr "测试手机号 该字段是必填项。"
-
-#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:31
-#: settings/api/feishu.py:36 settings/api/tencent_sms.py:57
-#: settings/api/wecom.py:37
+#: settings/api/dingtalk.py:31 settings/api/feishu.py:36
+#: settings/api/sms.py:131 settings/api/wecom.py:37
 msgid "Test success"
 msgstr "测试成功"
 
@@ -3454,6 +3453,14 @@ msgstr "获取 LDAP 用户为 None"
 msgid "Imported {} users successfully (Organization: {})"
 msgstr "成功导入 {} 个用户 ( 组织: {} )"
 
+#: settings/api/sms.py:113
+msgid "Invalid SMS platform"
+msgstr "无效的短信平台"
+
+#: settings/api/sms.py:119
+msgid "test_phone is required"
+msgstr "测试手机号 该字段是必填项。"
+
 #: settings/apps.py:7
 msgid "Settings"
 msgstr "系统设置"
@@ -3778,29 +3785,68 @@ msgstr "SP 密钥"
 msgid "SP cert"
 msgstr "SP 证书"
 
-#: settings/serializers/auth/sms.py:11
+#: settings/serializers/auth/sms.py:14
 msgid "Enable SMS"
 msgstr "启用 SMS"
 
-#: settings/serializers/auth/sms.py:13
-msgid "SMS provider"
-msgstr "短信服务商"
+#: settings/serializers/auth/sms.py:16
+msgid "SMS provider / Protocol"
+msgstr "短信服务商 / 协议"
 
-#: settings/serializers/auth/sms.py:18 settings/serializers/auth/sms.py:36
-#: settings/serializers/auth/sms.py:44 settings/serializers/email.py:65
+#: settings/serializers/auth/sms.py:21 settings/serializers/auth/sms.py:39
+#: settings/serializers/auth/sms.py:47 settings/serializers/auth/sms.py:58
+#: settings/serializers/email.py:65
 msgid "Signature"
 msgstr "签名"
 
-#: settings/serializers/auth/sms.py:19 settings/serializers/auth/sms.py:37
-#: settings/serializers/auth/sms.py:45
+#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:40
+#: settings/serializers/auth/sms.py:48
 msgid "Template code"
 msgstr "模板"
 
-#: settings/serializers/auth/sms.py:23
+#: settings/serializers/auth/sms.py:26
 msgid "Test phone"
 msgstr "测试手机号"
 
-#: settings/serializers/auth/sso.py:11
+#: settings/serializers/auth/sms.py:54
+msgid "Gateway account(SP id)"
+msgstr "网关账号(SP id)"
+
+#: settings/serializers/auth/sms.py:55
+msgid "Gateway password(SP secret)"
+msgstr "网关密码(SP secret)"
+
+#: settings/serializers/auth/sms.py:56
+msgid "Original number(Src id)"
+msgstr "原始号码(Src id)"
+
+#: settings/serializers/auth/sms.py:57
+msgid "Business type(Service id)"
+msgstr "业务类型(Service id)"
+
+#: settings/serializers/auth/sms.py:60
+msgid "Template"
+msgstr "模板"
+
+#: settings/serializers/auth/sms.py:61
+msgid ""
+"Template need contain {code} and Signature + template length does not exceed "
+"67 words. For example, your verification code is {code}, which is valid for "
+"5 minutes. Please do not disclose it to others."
+msgstr ""
+"模板需要包含 {code},并且模板+签名长度不能超过67个字。例如, 您的验证码是 "
+"{code}, 有效期为5分钟。请不要泄露给其他人。"
+
+#: settings/serializers/auth/sms.py:70
+#, python-brace-format
+msgid "The template needs to contain {code}"
+msgstr "模板需要包含 {code}"
+
+#: settings/serializers/auth/sms.py:73
+msgid "Signature + Template must not exceed 65 words"
+msgstr "模板+签名不能超过65个字"
+
+#: settings/serializers/auth/sso.py:12
 msgid "Enable SSO auth"
 msgstr "启用 SSO Token 认证"
 
@@ -6365,11 +6411,11 @@ msgstr "云账号"
 msgid "Test cloud account"
 msgstr "测试云账号"
 
-#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:67
+#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers/task.py:66
 msgid "Account"
 msgstr "账号"
 
-#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:38
+#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:37
 msgid "Regions"
 msgstr "地域"
 
@@ -6377,19 +6423,19 @@ msgstr "地域"
 msgid "Hostname strategy"
 msgstr "主机名策略"
 
-#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:68
+#: xpack/plugins/cloud/models.py:100 xpack/plugins/cloud/serializers/task.py:67
 msgid "Unix admin user"
 msgstr "Unix 管理员"
 
-#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:69
+#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers/task.py:68
 msgid "Windows admin user"
 msgstr "Windows 管理员"
 
-#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:46
+#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers/task.py:45
 msgid "IP network segment group"
 msgstr "IP网段组"
 
-#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:72
+#: xpack/plugins/cloud/models.py:113 xpack/plugins/cloud/serializers/task.py:71
 msgid "Always update"
 msgstr "总是更新"
 
@@ -6694,34 +6740,32 @@ msgstr "测试端口"
 
 #: xpack/plugins/cloud/serializers/task.py:29
 msgid ""
-"Only instances matching the IP range will be synced. 
If the instance "
-"contains multiple IP addresses, the first IP address that matches will be "
-"used as the IP for the created asset. 
The default value of * means sync "
-"all instances and randomly match IP addresses. 
Format for comma-"
-"delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
+"The IP address that is first matched to will be used as the IP of the "
+"created asset. 
The default * indicates a random match. 
Format for "
+"comma-delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
 msgstr ""
 "只有匹配到 IP 段的实例会被同步。
如果实例包含多个 IP 地址,那么第一个匹配"
 "到的 IP 地址将被用作创建的资产的 IP。
默认值 * 表示同步所有实例和随机匹配 "
 "IP 地址。
格式为以逗号分隔的字符串,例如:192.168.1.0/24,10.1.1.1-10.1.1.20"
 
-#: xpack/plugins/cloud/serializers/task.py:36
+#: xpack/plugins/cloud/serializers/task.py:35
 msgid "History count"
 msgstr "执行次数"
 
-#: xpack/plugins/cloud/serializers/task.py:37
+#: xpack/plugins/cloud/serializers/task.py:36
 msgid "Instance count"
 msgstr "实例个数"
 
-#: xpack/plugins/cloud/serializers/task.py:66
+#: xpack/plugins/cloud/serializers/task.py:65
 msgid "Linux admin user"
 msgstr "Linux 管理员"
 
-#: xpack/plugins/cloud/serializers/task.py:71
+#: xpack/plugins/cloud/serializers/task.py:70
 #: xpack/plugins/gathered_user/serializers.py:20
 msgid "Periodic display"
 msgstr "定时执行"
 
-#: xpack/plugins/cloud/utils.py:69
+#: xpack/plugins/cloud/utils.py:68
 msgid "Account unavailable"
 msgstr "账号无效"
 
diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py
index 65438dda1..176a6c2c6 100644
--- a/apps/settings/api/__init__.py
+++ b/apps/settings/api/__init__.py
@@ -5,6 +5,4 @@ from .dingtalk import *
 from .feishu import *
 from .public import *
 from .email import *
-from .alibaba_sms import *
-from .tencent_sms import *
 from .sms import *
diff --git a/apps/settings/api/alibaba_sms.py b/apps/settings/api/alibaba_sms.py
deleted file mode 100644
index 8240ba0e0..000000000
--- a/apps/settings/api/alibaba_sms.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from rest_framework.views import Response
-from rest_framework.generics import GenericAPIView
-from rest_framework.exceptions import APIException
-from rest_framework import status
-from django.utils.translation import gettext_lazy as _
-
-from common.sdk.sms.alibaba import AlibabaSMS
-from settings.models import Setting
-from common.exceptions import JMSException
-
-from .. import serializers
-
-
-class AlibabaSMSTestingAPI(GenericAPIView):
-    serializer_class = serializers.AlibabaSMSSettingSerializer
-    rbac_perms = {
-        'POST': 'settings.change_sms'
-    }
-
-    def post(self, request):
-        serializer = self.serializer_class(data=request.data)
-        serializer.is_valid(raise_exception=True)
-
-        alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID']
-        alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET')
-        alibaba_verify_sign_name = serializer.validated_data['ALIBABA_VERIFY_SIGN_NAME']
-        alibaba_verify_template_code = serializer.validated_data['ALIBABA_VERIFY_TEMPLATE_CODE']
-        test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
-
-        if not test_phone:
-            raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
-
-        if not alibaba_access_key_secret:
-            secret = Setting.objects.filter(name='ALIBABA_ACCESS_KEY_SECRET').first()
-            if secret:
-                alibaba_access_key_secret = secret.cleaned_value
-
-        alibaba_access_key_secret = alibaba_access_key_secret or ''
-
-        try:
-            client = AlibabaSMS(
-                access_key_id=alibaba_access_key_id,
-                access_key_secret=alibaba_access_key_secret
-            )
-
-            client.send_sms(
-                phone_numbers=[test_phone],
-                sign_name=alibaba_verify_sign_name,
-                template_code=alibaba_verify_template_code,
-                template_param={'code': 'test'}
-            )
-            return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
-        except APIException as e:
-            try:
-                error = e.detail['errmsg']
-            except:
-                error = e.detail
-            return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py
index 4cce5e718..0f487c280 100644
--- a/apps/settings/api/settings.py
+++ b/apps/settings/api/settings.py
@@ -40,6 +40,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
         'sms': serializers.SMSSettingSerializer,
         'alibaba': serializers.AlibabaSMSSettingSerializer,
         'tencent': serializers.TencentSMSSettingSerializer,
+        'cmpp2': serializers.CMPP2SMSSettingSerializer,
     }
 
     rbac_category_permissions = {
diff --git a/apps/settings/api/sms.py b/apps/settings/api/sms.py
index bb30fa3aa..a886a3d8f 100644
--- a/apps/settings/api/sms.py
+++ b/apps/settings/api/sms.py
@@ -1,8 +1,19 @@
-from rest_framework.generics import ListAPIView
+import importlib
+
+from collections import OrderedDict
+
+from rest_framework.generics import ListAPIView, GenericAPIView
 from rest_framework.response import Response
+from rest_framework.exceptions import APIException
+from rest_framework import status
+from django.utils.translation import gettext_lazy as _
 
 from common.sdk.sms import BACKENDS
+from common.exceptions import JMSException
 from settings.serializers.sms import SMSBackendSerializer
+from settings.models import Setting
+
+from .. import serializers
 
 
 class SMSBackendAPI(ListAPIView):
@@ -21,3 +32,108 @@ class SMSBackendAPI(ListAPIView):
         ]
 
         return Response(data)
+
+
+class SMSTestingAPI(GenericAPIView):
+    backends_serializer = {
+        'alibaba': serializers.AlibabaSMSSettingSerializer,
+        'tencent': serializers.TencentSMSSettingSerializer,
+        'cmpp2': serializers.CMPP2SMSSettingSerializer
+    }
+    rbac_perms = {
+        'POST': 'settings.change_sms'
+    }
+
+    @staticmethod
+    def get_or_from_setting(key, value=''):
+        if not value:
+            secret = Setting.objects.filter(name=key).first()
+            if secret:
+                value = secret.cleaned_value
+
+        return value or ''
+
+    def get_alibaba_params(self, data):
+        init_params = {
+            'access_key_id': data['ALIBABA_ACCESS_KEY_ID'],
+            'access_key_secret': self.get_or_from_setting(
+                'ALIBABA_ACCESS_KEY_SECRET', data.get('ALIBABA_ACCESS_KEY_SECRET')
+            )
+        }
+        send_sms_params = {
+            'sign_name': data['ALIBABA_VERIFY_SIGN_NAME'],
+            'template_code': data['ALIBABA_VERIFY_TEMPLATE_CODE'],
+            'template_param': {'code': '666666'}
+        }
+        return init_params, send_sms_params
+
+    def get_tencent_params(self, data):
+        init_params = {
+            'secret_id': data['TENCENT_SECRET_ID'],
+            'secret_key': self.get_or_from_setting(
+                'TENCENT_SECRET_KEY', data.get('TENCENT_SECRET_KEY')
+            ),
+            'sdkappid': data['TENCENT_SDKAPPID']
+        }
+        send_sms_params = {
+            'sign_name': data['TENCENT_VERIFY_SIGN_NAME'],
+            'template_code': data['TENCENT_VERIFY_TEMPLATE_CODE'],
+            'template_param': OrderedDict(code='666666')
+        }
+        return init_params, send_sms_params
+
+    def get_cmpp2_params(self, data):
+        init_params = {
+            'host': data['CMPP2_HOST'], 'port': data['CMPP2_PORT'],
+            'sp_id': data['CMPP2_SP_ID'], 'src_id': data['CMPP2_SRC_ID'],
+            'sp_secret': self.get_or_from_setting(
+                'CMPP2_SP_SECRET', data.get('CMPP2_SP_SECRET')
+            ),
+            'service_id': data['CMPP2_SERVICE_ID'],
+        }
+        send_sms_params = {
+            'sign_name': data['CMPP2_VERIFY_SIGN_NAME'],
+            'template_code': data['CMPP2_VERIFY_TEMPLATE_CODE'],
+            'template_param': OrderedDict(code='666666')
+        }
+        return init_params, send_sms_params
+
+    def get_params_by_backend(self, backend, data):
+        """
+        返回两部分参数
+            1、实例化参数
+            2、发送测试短信参数
+        """
+        get_params_func = getattr(self, 'get_%s_params' % backend)
+        return get_params_func(data)
+
+    def post(self, request, backend):
+        serializer_class = self.backends_serializer.get(backend)
+        if serializer_class is None:
+            raise JMSException(_('Invalid SMS platform'))
+        serializer = serializer_class(data=request.data)
+        serializer.is_valid(raise_exception=True)
+
+        test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
+        if not test_phone:
+            raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
+
+        init_params, send_sms_params = self.get_params_by_backend(backend, serializer.validated_data)
+
+        m = importlib.import_module(f'common.sdk.sms.{backend}', __package__)
+        try:
+            client = m.client(**init_params)
+            client.send_sms(
+                phone_numbers=[test_phone],
+                **send_sms_params
+            )
+            status_code = status.HTTP_200_OK
+            data = {'msg': _('Test success')}
+        except APIException as e:
+            try:
+                error = e.detail['errmsg']
+            except:
+                error = e.detail
+            status_code = status.HTTP_400_BAD_REQUEST
+            data = {'error': error}
+        return Response(status=status_code, data=data)
diff --git a/apps/settings/api/tencent_sms.py b/apps/settings/api/tencent_sms.py
deleted file mode 100644
index 83a87a474..000000000
--- a/apps/settings/api/tencent_sms.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from collections import OrderedDict
-
-from rest_framework.views import Response
-from rest_framework.generics import GenericAPIView
-from rest_framework.exceptions import APIException
-from rest_framework import status
-from django.utils.translation import gettext_lazy as _
-
-from common.sdk.sms.tencent import TencentSMS
-from settings.models import Setting
-from common.exceptions import JMSException
-
-from .. import serializers
-
-
-class TencentSMSTestingAPI(GenericAPIView):
-    serializer_class = serializers.TencentSMSSettingSerializer
-    rbac_perms = {
-        'POST': 'settings.change_sms'
-    }
-
-    def post(self, request):
-        serializer = self.serializer_class(data=request.data)
-        serializer.is_valid(raise_exception=True)
-
-        tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID']
-        tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY')
-        tencent_verify_sign_name = serializer.validated_data['TENCENT_VERIFY_SIGN_NAME']
-        tencent_verify_template_code = serializer.validated_data['TENCENT_VERIFY_TEMPLATE_CODE']
-        tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID')
-
-        test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
-
-        if not test_phone:
-            raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
-
-        if not tencent_secret_key:
-            secret = Setting.objects.filter(name='TENCENT_SECRET_KEY').first()
-            if secret:
-                tencent_secret_key = secret.cleaned_value
-
-        tencent_secret_key = tencent_secret_key or ''
-
-        try:
-            client = TencentSMS(
-                secret_id=tencent_secret_id,
-                secret_key=tencent_secret_key,
-                sdkappid=tencent_sdkappid
-            )
-
-            client.send_sms(
-                phone_numbers=[test_phone],
-                sign_name=tencent_verify_sign_name,
-                template_code=tencent_verify_template_code,
-                template_param=OrderedDict(code='666666')
-            )
-            return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
-        except APIException as e:
-            try:
-                error = e.detail['errmsg']
-            except:
-                error = e.detail
-            return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py
index cd3bef74c..8875f6437 100644
--- a/apps/settings/serializers/auth/sms.py
+++ b/apps/settings/serializers/auth/sms.py
@@ -4,13 +4,16 @@ from rest_framework import serializers
 from common.drf.fields import EncryptedField
 from common.sdk.sms import BACKENDS
 
-__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
+__all__ = [
+    'SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer',
+    'CMPP2SMSSettingSerializer'
+]
 
 
 class SMSSettingSerializer(serializers.Serializer):
     SMS_ENABLED = serializers.BooleanField(default=False, label=_('Enable SMS'))
     SMS_BACKEND = serializers.ChoiceField(
-        choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider')
+        choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider / Protocol')
     )
 
 
@@ -43,3 +46,29 @@ class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
     TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id')
     TENCENT_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
     TENCENT_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
+
+
+class CMPP2SMSSettingSerializer(BaseSMSSettingSerializer):
+    CMPP2_HOST = serializers.CharField(max_length=256, required=True, label=_('Host'))
+    CMPP2_PORT = serializers.IntegerField(default=7890, label=_('Port'))
+    CMPP2_SP_ID = serializers.CharField(max_length=128, required=True, label=_('Gateway account(SP id)'))
+    CMPP2_SP_SECRET = EncryptedField(max_length=256, required=False, label=_('Gateway password(SP secret)'))
+    CMPP2_SRC_ID = serializers.CharField(max_length=256, required=False, label=_('Original number(Src id)'))
+    CMPP2_SERVICE_ID = serializers.CharField(max_length=256, required=True, label=_('Business type(Service id)'))
+    CMPP2_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
+    CMPP2_VERIFY_TEMPLATE_CODE = serializers.CharField(
+        max_length=69, required=True, label=_('Template'),
+        help_text=_('Template need contain {code} and Signature + template length does not exceed 67 words. '
+                    'For example, your verification code is {code}, which is valid for 5 minutes. '
+                    'Please do not disclose it to others.')
+    )
+
+    def validate(self, attrs):
+        sign_name = attrs.get('CMPP2_VERIFY_SIGN_NAME', '')
+        template_code = attrs.get('CMPP2_VERIFY_TEMPLATE_CODE', '')
+        if template_code.find('{code}') == -1:
+            raise serializers.ValidationError(_('The template needs to contain {code}'))
+        if len(sign_name + template_code) > 65:
+            # 保证验证码内容在一条短信中(长度小于70字), 签名两边的括号和空格占3个字,再减去2个即可(验证码占用4个但占位符6个
+            raise serializers.ValidationError(_('Signature + Template must not exceed 65 words'))
+        return attrs
diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py
index 7baa19196..3152a5ef5 100644
--- a/apps/settings/serializers/settings.py
+++ b/apps/settings/serializers/settings.py
@@ -7,7 +7,7 @@ from .auth import (
     LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
     CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
     WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer,
-    TencentSMSSettingSerializer,
+    TencentSMSSettingSerializer, CMPP2SMSSettingSerializer
 )
 from .terminal import TerminalSettingSerializer
 from .security import SecuritySettingSerializer
@@ -37,6 +37,7 @@ class SettingsSerializer(
     CleaningSerializer,
     AlibabaSMSSettingSerializer,
     TencentSMSSettingSerializer,
+    CMPP2SMSSettingSerializer,
 ):
     # encrypt_fields 现在使用 write_only 来判断了
     pass
diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py
index 728baf0ae..ba04f2a4b 100644
--- a/apps/settings/urls/api_urls.py
+++ b/apps/settings/urls/api_urls.py
@@ -16,8 +16,7 @@ urlpatterns = [
     path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
     path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
     path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
-    path('alibaba/testing/', api.AlibabaSMSTestingAPI.as_view(), name='alibaba-sms-testing'),
-    path('tencent/testing/', api.TencentSMSTestingAPI.as_view(), name='tencent-sms-testing'),
+    path('sms//testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
     path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
 
     path('setting/', api.SettingsApi.as_view(), name='settings-setting'),