Python3 환경에서 YunTongXun SMS SDK 활용한 인증 문자 발송

Python 3.x 프로젝트에서 YunTongXun(容联云通讯) 플랫폼을 활용해 SMS 인증 코드를 발송하는 방법을 살펴본다. 해당 플랫폼은 가입 후 실명 인증 없이도 테스트용 문자 발송 기능을 제공하며, 다만 무료 테스트 계정은 미리 등록한 3개의 휴대폰 번호로만 발송 가능하고 템플릿은 1개만 사용할 수 있다.

SDK 구성 및 프로젝트 설정

공식 문서에서 Python SDK를 내려받으면 CCPRestSDK.pyxmltojson.py 두 개의 핵심 파일이 포함되어 있다. 프로젝트 내 yuntongxun 폴더를 생성하고 이 파일들을 배치한 후, 기존 SendTemplateSMS.py 대신 싱글톤 패턴으로 재구성한 모듈을 사용한다.

메시지 발송 클래스 구현

아래 sms_dispatcher.py는 공식 예제를 재구성한 싱글톤 패턴 기반의 발송 클래스다. Python 3 문법에 맞게 수정했으며, 계정 정보는 실제 콘솔 값으로 대체해야 한다.

# coding=utf-8
from .CCPRestSDK import REST

# 콘솔에서 확인한 계정 정보
ACCOUNT_SID = '실제_ACCOUNT_SID'
AUTH_TOKEN = '실제_AUTH_TOKEN'
APP_ID = '실제_AppID'

# 서버 접속 정보
HOST = 'app.cloopen.com'
PORT = '8883'
API_VERSION = '2013-12-26'


class MessageSender(object):
    _singleton = None

    def __new__(cls):
        if cls._singleton is None:
            instance = super(MessageSender, cls).__new__(cls)
            instance.client = REST(HOST, PORT, API_VERSION)
            instance.client.setAccount(ACCOUNT_SID, AUTH_TOKEN)
            instance.client.setAppId(APP_ID)
            cls._singleton = instance
        return cls._singleton

    def deliver_verification_code(self, phone_number, content_values, template_id):
        """
        인증 문자 발송 메서드
        
        Args:
            phone_number: 수신자 번호
            content_values: 템플릿 변수 값 목록 (예: ['1234', '5'])
            template_id: 템플릿 식별자
        """
        response = self.client.sendTemplateSMS(
            phone_number, 
            content_values, 
            template_id
        )
        print('response:', response)
        
        status = response.get("statusCode")
        return 0 if status == "000000" else -1


if __name__ == '__main__':
    sender = MessageSender()
    outcome = sender.deliver_verification_code(
        "176xxxxxxxx", 
        ["5678", "3"], 
        1
    )
    print(outcome)

Python 3 마이그레이션 핵심 수정 사항

원본 SDK가 Python 2 기반으로 작성되어 있어 Python 3 환경에서 여러 문법 오류가 발생한다. 핵심 수정 내역은 다음과 같다.

모듈 임포트 변경

# Python 2
# import md5
# import urllib2

# Python 3
from hashlib import md5
import urllib.request as urllib2

해시 및 인코딩 처리

# 문자열을 바이트로 변환 후 해싱
signature = self.AccountSid + self.AccountToken + self.Batch
signature = signature.encode('utf-8')
sig = md5(signature).hexdigest().upper()

# Base64 인코딩 시 바이트 변환 필수
src = self.AccountSid + ":" + self.Batch
auth = base64.encodestring(src.encode()).strip()

요청 본문 전송 방식

# Python 2 방식 (미지원)
# req.add_data(body)

# Python 3 방식 - 바이트 타입으로 변환
req.data = body.encode()

네트워크 오류 {'172001':'网络错误'} 해결

위 문법 수정 후에도 네트워크 오류가 지속될 수 있다. 세 가지 추가 조치가 필요하다.

1. 상대 경로 임포트 적용

가상 환경의 xmltojson.py 대신 프로젝트 내 SDK 파일을 사용하도록 수정한다.

# 잘못된 방식
# from xmltojson import xmltojson

# 올바른 방식
from .xmltojson import xmltojson

2. SSL 인증서 검증 비활성화

Python 2.7.9 이후부터 HTTPS 연결 시 SSL 인증서 검증이 기본 적용된다. YunTongXun 서버가 자체 서명 인증서를 사용하므로 검증을 우회해야 한다.

import ssl

# 전역적으로 SSL 인증서 검증 비활성화
ssl._create_default_https_context = ssl._create_unverified_context

3. 요청 데이터 타입 최종 확인

최종적으로 req.data에 할당하는 값이 bytes 타입인지 다시 한번 확인한다. 문자열 그대로 전달하면 서버에서 빈 본문으로 해석해 오류가 발생한다.

완성된 SDK 핵심 모듈 (CCPRestSDK.py)

아래는 Python 3 호환성을 완전히 적용한 핵심 SDK 코드의 발송 메서드 부분이다.

import datetime
import json
import base64
from hashlib import md5
import urllib.request as urllib2
from .xmltojson import xmltojson


class REST:
    
    def __init__(self, server_ip, server_port, soft_version):
        self.ServerIP = server_ip
        self.ServerPort = server_port
        self.SoftVersion = soft_version
        self.AccountSid = ''
        self.AccountToken = ''
        self.AppId = ''
        self.Batch = ''
        self.BodyType = 'xml'
        self.Iflog = True

    def setAccount(self, account_sid, account_token):
        self.AccountSid = account_sid
        self.AccountToken = account_token

    def setAppId(self, app_id):
        self.AppId = app_id

    def sendTemplateSMS(self, to, datas, temp_id):
        now = datetime.datetime.now()
        self.Batch = now.strftime("%Y%m%d%H%M%S")
        
        # 서명 생성
        sign_content = (self.AccountSid + self.AccountToken + self.Batch).encode('utf-8')
        signature = md5(sign_content).hexdigest().upper()
        
        # 요청 URL 구성
        endpoint = (
            f"https://{self.ServerIP}:{self.ServerPort}/"
            f"{self.SoftVersion}/Accounts/{self.AccountSid}/"
            f"SMS/TemplateSMS?sig={signature}"
        )
        
        # 인증 헤더 생성
        auth_raw = f"{self.AccountSid}:{self.Batch}"
        auth_header = base64.encodestring(auth_raw.encode()).strip()
        
        # HTTP 요청 준비
        request = urllib2.Request(endpoint)
        self._apply_headers(request)
        request.add_header("Authorization", auth_header)
        
        # 요청 본문 구성
        if self.BodyType == 'json':
            data_str = '","'.join(datas)
            payload = (
                f'{{"to": "{to}", "datas": ["{data_str}"], '
                f'"templateId": "{temp_id}", "appId": "{self.AppId}"}}'
            )
            request.add_header("Accept", "application/json")
            request.add_header("Content-Type", "application/json;charset=utf-8")
        else:
            items = ''.join(f'<data>{item}</data>' for item in datas)
            payload = (
                f'<?xml version="1.0" encoding="utf-8"?>'
                f'<SubAccount><datas>{items}</datas>'
                f'<to>{to}</to><templateId>{temp_id}</templateId>'
                f'<appId>{self.AppId}</appId></SubAccount>'
            )
            request.add_header("Accept", "application/xml")
            request.add_header("Content-Type", "application/xml;charset=utf-8")
        
        # 중요: bytes 타입으로 변환하여 전송
        request.data = payload.encode('utf-8')
        
        # 요청 실행
        try:
            response = urllib2.urlopen(request)
            raw_data = response.read()
            response.close()
            
            if self.BodyType == 'json':
                result = json.loads(raw_data)
            else:
                converter = xmltojson()
                result = converter.main(raw_data)
            
            return result
            
        except Exception as exc:
            print(f"Request failed: {exc}")
            return {'172001': '网络错误'}

    def _apply_headers(self, req):
        content_type = "application/json" if self.BodyType == 'json' else "application/xml"
        req.add_header("Accept", content_type)
        req.add_header("Content-Type", f"{content_type};charset=utf-8")

위 코드는 Python 3.6 이상 환경에서 정상 동작하며, SSL 인증서 문제와 바이트/문자열 변환 이슈를 모두 해결한 상태다. 실제 운영 환경에서는 SSL 검증 우회 대신 공식 인증서를 적용하는 것이 보안상 권장된다.

태그: Python3 SMS YunTongXun SSL urllib

6월 13일 19:30에 게시됨