본문 바로가기
프로젝트/SRT&KTX 매진표 예매

SRT&KTX 기차표 매크로 예매 - (8) KTX 로그인

by 매크로메이커 2024. 1. 15.

SRT 개발이 완료됐으니, KTX 예매 기능을 구현하자.

 

SRT 로그인 기능 구현했던 글을 참조하여 빠르게 개발해보자.

 

Session 받기

KTX 세션은 https://www.letskorail.com/ 에서 받는다. - callback 함수들은 ui와 연동하면서 필요하므로, 일단 주석처리하자.

class KTX:
    def __init__(self): #, error_callback, try_callback):
        # self.error_callback = error_callback
        # self.try_callback = try_callback

        self.session = requests.Session()
        self.session.get("https://www.letskorail.com/")

 

로그인하기

KTX 로그인 페이지로 이동해서 로그인을 해보자.

e다소 당황스러운 KTX 로그인 Body Data

아이디랑 패스워드가 안보이고, encUserId와 encUserPwd가 보인다...

 

RSA 알고리즘으로 id/pw를 암호화를 한 번 하고 보내는 건데, 가능하면 암호화를 피하고 싶다...암호화가 들어가는 순간 좀 복잡해진다.

encUserId 위에 보면 UserId, UserPwd가 보이는데 암호화되지 않은 값을 전달해도 로그인이 될 수 있으니...희망을 갖고 암호화를 하는 코드를 좀 살펴보자.

 

로그인 폼이 제출되는 submit target을 찾아보면 Login() javascript 함수로 연결된다.

이 함수에서 암호화 부분을 찾아보자.

		// 이중보안 2015.05.07 ljy
		document.form2.encUserId.value = '';
		document.form2.encUserPwd.value = '';
		
			try {
				var rsa = new RSAKey();
				rsa.setPublic('8ac8f33129dceca9449e8ca2d6d8a1888e7c62bd95f234415a07f8c89b66fbe300a2d4314123cf049b797e9a2521886c00f618c97b0d4643ee180eca92c173ccd3a920bc5f45b6da83ef2fa5a8b38066a10d94486dfe24cf76f507c6d4ecbff5d7dc66a97378d497db31e1fe062a978e3f929d0b3356d4a8ed9947d0827e0315', '10001');
	
				// 사용자ID와 비밀번호를 RSA로 암호화한다.
				var e_txtMemberNo = rsa.encrypt(txtMemberNo);
	
				document.form2.encUserId.value = e_txtMemberNo;

				if (($('.keySec').is(":checked"))) {
					document.form2.useKeySecFlg.value = 'Y';
					var encData = nshc.encrypted();
			        encData = encodeURIComponent(encData);
			        document.form2.encUserPwd.value = encData;
				} else {
					document.form2.useKeySec.value = 'N';
					var e_txtPwd = rsa.encrypt(txtPwd);
					document.form2.encUserPwd.value = e_txtPwd;
				}
	
			} catch(err) {
				//alert(err);
				document.form2.UserId.value = txtMemberNo;
				document.form2.UserPwd.value = txtPwd;
			}

2중 보안을 이유로 RSA 암호화를 하고 있고...

try 안에서 에러가 발생하면 catch에서 암호화 되지 않은 계정을 보내고 있는 것을 볼 수 있다..!

 

개발자 도구에서 항상 암호화를 안하도록 javascript 코드를 바꾸고 테스트해보니 암호화를 안해도 로그인이 잘된다~~(싱글벙글)

 

로그인을 위해 요청하는 Form data를 정리하면 다음과 같다.

  • URL : https://www.letskorail.com/korail/com/loginAction.do
  • Method : POST
  • FormData
    • selInputFlg : 로그인 타입(2 - 멤버십 번호, 5 - 이메일, 4 - 휴대폰)
    • radIngrDvCd : 고정 - "2"
    • hidMemberFlg : 고정 - "1"
    • txtDv : 비밀번호 길이 관련 변수 (1 - 4자리 비밀번호, 2 - 비밀번호가 8자리 이상, 나머지 경우는 불가능)
    • UserId : 비밀번호
    • UserPwd : 아이디
    • acsURI : 고정 - "https://www.letskorail.com:443/ebizsso/sso/acs"
    • providerName : 고정 - "Ebiz Sso"
    • forwardingURI : 고정 - "/ebizsso/sso/sp/service_proc.jsp"
    • RelayState : 고정 - "/ebizsso/sso/sp/service_front.jsp"
    • IPType : 고정 - "Ebiz Sso Identity Provider"
    • 나머지 변수들 : 빈 값

KTX는 SRT에 비해 분석이 좀 까다로웠는데, 로그인 페이지의 Login() 함수를 참고하여 분석했다.

 

동일한 body data를 만들고 post 요청을 보내도 정상 처리가 안되는데, 이건 Request Header 값을 안넣어줘서 그렇다.

SRT는 이 작업을 안해줘도 문제가 없었는데, 코레일에서는 헤더값을 보고 정상적인 브라우저 요청인지를 검토하는 것 같다.

POST 요청시 전달되는 header

앞으로 다른 요청을 보낼 때도 헤더가 필요할 것 같으니, 아예 헤더용 함수를 만들자.

    def get_req_headers(self):
        headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
            'Accept-Encoding': 'gzip, deflate, br',
            'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Content-Length': '460',
            'Content-Type': 'application/x-www-form-urlencoded',
            'Cookie': str(self.session.cookies),
            'Host': 'www.letskorail.com',
            'Origin': 'https://www.letskorail.com',
            'Pragma': 'no-cache',
            'Referer': 'https://www.letskorail.com/korail/com/login.do',
            'Sec-Fetch-Dest': 'document',
            'Sec-Fetch-Mode': 'navigate',
            'Sec-Fetch-Site': 'same-origin',
            'Sec-Fetch-User': '?1',
            'Upgrade-Insecure-Requests': '1',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"macOS"',
        }
        return headers

 

이 헤더를 포함한 POST 요청을 보내면 아래와 같은 응답이 돌아온다.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ko" xml:lang="ko">



<head>
	<script type="text/javascript" src="/js/jquery.ui/jquery-1.8.3.min.js"></script>
	<script type="text/javascript" src="/com/selectKorailCoMessage.do"></script>
	<script language="JavaScript">
	window.onload = function(){
		localStorage.removeItem("pc25100En");
		
		$.ajax({
			url		 : "/korail/com/mypage/preset/preset_list_json.do;jsessionid=mVZalxGB4P908cKJwTrRdaN8Kp1rfPrRxZzeNUgkcVoUzTRunJmD7FZl1lIya5Ng.kr014_servlet_engine4",
			type	 : "POST",
			data	 : {
				hidSetDv	: '02',
				hidRegSqno	: '0'
			},
			success: function(data) {
				var obj = JSON.parse(data);
				if (obj.strResult == "SUCC")
				{
					localStorage.setItem("pc25100En", data);
				}
				onSubmit();
			},
			error:function(jqXHR, textStatus, error) {
				onSubmit();
			}
		});
		
	};

	function onSubmit() {
		document.form1.action = pub3+"/korail/com/loginProc.do";
		document.form1.submit();
	}
	</script>
</head>
<body>
<form name="form1" method="post">
<input type="hidden" name="ret_url" 		 value="/"/>
<input type="hidden" name="strWebPwdCphdAt"  value="Y"/>

</form>
</body>
<html>

 

ajax 안에 있는 URL로 POST 요청을 보내면 로그인이 완료되는 것처럼 보인다.

 

ajax로 메세지를 보내면 아래와 같은 응답을 받을 수 있다.

{
  "strResult" : "SUCC",
  "pc25100En" : {
    "en_len" : 0,
    "h_result" : null,
    "h_msg_txt" : "정상적으로 처리되었습니다.",
    "entity" : [ ],
    "h_msg_cd" : "IRS100002",
    "h_grd_cnt" : "0000"
  }
}

 

*** 참고로, 테스트해본 결과 ajax 안의 request는 로그인 정보가 틀려도 SUCC라고 뜬다 ㅋㅋ

 

이렇게 완성된 login 메서드는 아래와 같다..!

def login(self, login_type='', login_id='', login_pwd=''):
    login_url = "https://www.letskorail.com/korail/com/loginAction.do"

    # 아이디 검증
    if login_type == '2':           # 회원번호 로그인
        if len(login_id) != 10:
            self.error_callback('KTX 로그인 실패', f"회원번호는 10자리 숫자입니다")
            return False
    elif login_type == '4':         # 휴대전화번호 로그인
        login_id = login_id.replace('-', '')
        if len(login_id) == 10:
            login_id = login_id[0:3] + '-' + login_id[3:6] + '-' + login_id[6:]
        elif len(login_id) == 11:
            login_id = login_id[0:3] + '-' + login_id[3:7] + '-' + login_id[7:]
        else:
            self.error_callback('KTX 로그인 실패', f"휴대전화번호 입력 오류")
            return False
    elif login_type == '5':         # 이메일 로그인
        if '@' not in login_id:
            self.error_callback('KTX 로그인 실패', f"이메일 입력 오류")
            return False
    else:
        self.error_callback('KTX 로그인 실패', f"로그인 타입 코드 오류 - {login_type}")
        return False

    # 비밀번호 길이 검증
    if len(login_pwd) == 4:
        txtDv = "1"
    elif len(login_pwd) >= 4:
        txtDv = "2"
    else:
        self.error_callback('KTX 로그인 실패', f"비밀번호는 4자리 또는 8자리 이상 필수 [비밀번호 길이 : {len(login_pwd)}]")
        return False

    body = {
        "txtBookCnt": "",
        "txtIvntDt": "",
        "txtTotCnt": "",
        "selValues": "",
        "selInputFlg": login_type,
        "radIngrDvCd": "2",
        "ret_url": "",
        "hidMemberFlg": "1",
        "txtHaeRang": "",
        "hidEmailAdr": "",
        "txtDv": txtDv,
        "useKeySec": "",
        "UserId": login_id,
        "UserPwd": login_pwd,
        "encUserId": "",
        "encUserPwd": "",
        "keyname": "",
        "useKeySecFlg": "",
        "acsURI": "https://www.letskorail.com:443/ebizsso/sso/acs",
        "providerName": "Ebiz Sso",
        "forwardingURI": "/ebizsso/sso/sp/service_proc.jsp",
        "RelayState": "/ebizsso/sso/sp/service_front.jsp",
        "IPType": "Ebiz Sso Identity Provider"
    }
    try:
        res = self.session.post(login_url, data=body, headers=self.get_req_headers())
    except Exception as e:
        self.error_callback('KTX 로그인 요청 실패', f"HTTP 로그인 요청에 실패했습니다 - \n{e}")
        return False

    if 'preset_list_json.do' not in res.text:
        self.error_callback('KTX 로그인 요청 실패', f"아이디 혹은 패스워드가 틀렸습니다")
        return False
    body = {
        'hidSetDv': '02',
        'hidRegSqno': '0'
    }
    try:
        res = self.session.post(
            f"https://www.letskorail.com/korail/com/mypage/preset/preset_list_json.do;jsessionid={self.session.cookies.get('JSESSIONID')}",
            data=body, headers=self.get_req_headers())
    except Exception as e:
        self.error_callback('KTX 로그인 요청 실패', f"HTTP 로그인 확인 요청에 실패했습니다 - \n{e}")
        return False

    if 'SUCC' not in res.text:
        self.error_callback('KTX 로그인 요청 실패', f"로그인 결과 확인에 실패했습니다")
        return False

    return self.is_logged_in()

 

로그인 여부 확인은 SRT와 비슷하게 만들면 된다.

def is_logged_in(self):
    try:
        res = self.session.get(f"https://www.letskorail.com/index.jsp")
    except Exception as e:
        self.error_callback('KTX 로그인 여부 확인 실패', f"HTTP 요청에 실패했습니다 - \n{e}")
        return False

    try:
        if '로그아웃' in res.text:
            return True
    except Exception as e:
        self.error_callback('KTX 로그인 여부 확인 실패', f"로그인 여부 확인에 실패했습니다 - \n{e}")
    return False

 

 

풀 코드는 아래 깃허브를 참고하세용

 

https://www.github.com/dhgwag/train_reservation 

 

GitHub - dhgwag/train_reservation

Contribute to dhgwag/train_reservation development by creating an account on GitHub.

github.com