본문 바로가기

파이썬

공공데이터포털 API 연결 시 SSLV3_ALERT_ILLEGAL_PARAMETER 에러 [python, python 3.10.11, api, requests]

반응형

이상하다 분명 3주 전에 임시로 만들어뒀을 땐 잘 동작했는데..?

 

공공데이터포털 API를 연결해서 배치어플리케이션을 만들으려고 하는데 전에 잘만 됐던 requests.get(url)에서 에러가 발생했다.

url = 'http://api.data.go.kr/openapi/...'
params = {
    'serviceKey': os.getenv('OPEN_API_KEY'),
    'pageNo': '1',
    'numOfRows': '100',
    'type': 'json'
}
response = requests.get(url, params=params)

Caused by SSLError(SSLError(1, '[SSL: SSLV3_ALERT_ILLEGAL_PARAMETER] sslv3 alert illegal parameter (_ssl.c:1007)')

 

찾아보니 HTTP 요청 전에 TLS 핸드셰이크 단계에서 깨지는 경우라고 한다.
GPT한테 물어보니 이렇게 정리해줬다.

메시지 SSLV3_ALERT_ILLEGAL_PARAMETER 는 서버가 “네가 제시한 TLS 파라미터(버전/사이퍼/서명알고리즘 등)가 마음에 안 든다”라고 끊을 때 흔히 보입니다. 실제로 SSLv3를 쓰는 건 아니고(현대 스택은 TLS1.2/1.3), 호환성/환경 문제일 확률이 큽니다.

 

위와 같은 에러는 윈도우 운영체제 + Python 3.10/OpenSSL 조합에서 간헐적으로 발생한다고 하니 파이썬 버전을 3.11이상으로 업그레이드 하거나 아래와 같이 TLS1.2로 요청하도록 코드를 변경해주어야 한다.

 

tls.py

import ssl
from requests.adapters import HTTPAdapter

class TLS12Adapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        ctx = ssl.create_default_context()
        # TLS 1.2만 허용
        ctx.minimum_version = ssl.TLSVersion.TLSv1_2
        ctx.maximum_version = ssl.TLSVersion.TLSv1_2
        # 일부 서버 호환 (보안 완화; 필요할 때만)
        try:
            ctx.set_ciphers("DEFAULT:@SECLEVEL=1")
        except Exception:
            pass
        kwargs["ssl_context"] = ctx
        return super().init_poolmanager(*args, **kwargs)

 

batch_service.py

import requests, os, logging, json, xmltodict
from dotenv import load_dotenv
from appCommon.utils.tls import TLS12Adapter

load_dotenv()
logger = logging.getLogger(__name__)

def get_data():
    SERVICE_KEY = os.getenv('공공데이터포털 키')
    url = 'http://api.data.go.kr/openapi/...'
    num_of_rows = 1000
    response_type = 'xml'
    page_no = '1'
    
    parameter = {
        'serviceKey': SERVICE_KEY,
        'pageNo': page_no,
        'numOfRows': num_of_rows,
        'type': response_type
    }
    session = requests.Session()
    session.mount("https://", TLS12Adapter())
    response = session.get(url, params=parameter, timeout=7)
    logger.error(f'REQUEST URL:: {url}')
    if response.status_code == 200:
        # 요청 성공
        xml_data = response.content
        parsed_data = xmltodict.parse(xml_data)
        # 키가 잘못되었거나 하는 경우 Result Code 가 다른 게 아니라, OpenAPI_ServiceResponse 가 리턴되므로 해당 값이 None 이어야 정상 요청
        if parsed_data.get('OpenAPI_ServiceResponse') is not None:
            logger.error('RESULT:: ERROR')
            logger.error(f'params:: {json.dumps(params, ensure_ascii=False, indent=2)}')
            logger.error(f'response:{parsed_data}')
            return None

        logger.info('RESULT:: SUCCESS')
        return parsed_data
	else:
		# 실패
		status_code = response.status_code
		logger.error('RESULT:: ERROR')
		logger.error(f'status_code:{status_code}')
		logger.error(f'response:{response}')
		return None

 

parsed_data 부터는 api 미리보기에서 제공해주는 형식과 같으니 그 형식에 맞추어 파싱하여 사용하면 되겠다.