반응형

공공데이터포털 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 미리보기에서 제공해주는 형식과 같으니 그 형식에 맞추어 파싱하여 사용하면 되겠다.