이번 게시글에서는 스마트스토어센터 페이지에서 데이터를 수집하는 자동화 프로그램을 제작하기 위한
첫 번째 과정으로 네이버 로그인을 구현할 것입니다.

앞선 게시글에서 데이터를 수집하는 방식에 대해 알아보면서
로그인이 필요한 페이지에 접근하기 다음과 같은 쿠키 값이 필요함을 확인했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cookies = {
    "NNB": "...",
    "nid_inf": "...",
    "NID_AUT": "...",
    "NID_SES": "...",
    "NID_JKL": "...",
    "CBI_SES": "...",
    "CBI_CHK": "...",
    "NSI": "...",
}

위 키값들은 앞으로 로그인 프로세스를 파악하는 과정에서 중요하게 활용됩니다.


네이버 로그인 이해

네이버 스마트스토어센터 로그인 과정에서 진행되는 네이버 로그인은
일반적인 네이버 로그인과는 다른 과정으로 진행됩니다.

따라서 우선 일반적인 네이버 로그인 과정을 알아보겠습니다.

해당 파트는 아래 게시글을 참고해 작성되었습니다.

파이썬#76 - 파이썬 크롤링 requests 로 네이버 로그인 하기

네이버 로그인 요청 분석

네이버 로그인 과정을 분석하기 위해서는 우선 네이버 로그인을 요청을 시도하여
전달되는 값을 확인해야 합니다.

네이버 로그인 페이지에서 로그인을 수행하는 과정에서
발견할 수 있는 POST 요청을 살펴보면 다음과 같은 데이터가 전달됨을 발견할 수 있습니다.

nid-login

암호화된 값을 생략하고 키로 전달되는 내용을 확인하면 다음과 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
    "localechange": "",
    "dynamicKey": "...",
    "encpw": "...",
    "enctp": 1,
    "svctype": 1,
    "smart_LEVEL": 1,
    "bvsd": {
        "uuid": "...",
        "encData": "..."
    },
    "encnm": "...",
    "locale": "ko_KR",
    "url": "https://www.naver.com",
    "id": "",
    "pw": ""
}

공백이나 고정된 값을 가진 키를 제외하면 결과적으로
dynamicKey, encpw, bvsd, encnm를 밝혀내는 것이 중요할 것이라 판단됩니다.

네이버 로그인 폼 분석

키의 명칭만으로는 무엇을 의미하는지 알 수 없기 때문에
로그인 페이지 소스에서 키명칭을 검색하였고 네이버 로그인 폼에서 하나의 단서를 찾을 수 있었습니다.

nid-form

dynamicKey의 경우 로그인 폼에 동적으로 부여되는 값임을 알 수 있습니다.
하지만 나머지 encpw, bvsd, encnm의 값은 비어있기 때문에
다른 자바스크립트 응답을 분석해야 합니다.

네이버 로그인 RSA 암호화

encpw 값에 대한 단서를 찾기 위해 전체 검색을 수행했을 때
common_202201.js 내부에서 RSA 암호화 처리를 통해 값을 생성함을 알 수 있습니다.
그 중에서 가장 처음 단계로 실행될 것이라 추측되는 것이 아래 confirmSubmit() 함수입니다.

encpw

해당 함수는 아이디와 비밀번호의 여부를 체크하고 encryptIdPw() 함수의 결과를 반환합니다.
바로 밑에서 확인할 수 있는 encryptIdPw() 함수의 내용은 다음과 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function encryptIdPw() {
	var id = $("id");
	var pw = $("pw");
	var encpw = $("encpw");
	var rsa = new RSAKey;

	if (keySplit(session_keys)) {
		rsa.setPublic(evalue, nvalue);
		try{
			encpw.value = rsa.encrypt(
				getLenChar(sessionkey) + sessionkey +
				getLenChar(id.value) + id.value +
				getLenChar(pw.value) + pw.value);
		} catch(e) {
			return false;
		}
		$('enctp').value = 1;
		id.value = "";
		pw.value = "";
		return true;
	}
	else
	{
		getKeyByRuntimeInclude();
		return false;
	}

	return false;
}

해당 함수는 session_keys라는 값을 처리하고 RSA 암호화한 결과를
encpw의 값으로 대체하는 것을 알 수 있습니다.

마찬가지로 해당 명칭을 검색했을 때
session_keys는 Ajax 통신의 응답 결과를 받아오는 것을 확인할 수 있습니다.

session-keys

하지만 네이버 로그인 페이지에서 svctype=262144를 추가적인 파라미터로 입력할 경우
접근할 수 있는 모바일 로그인 페이지에서 해당 값을 확인할 수 있었습니다.

nid-mlogin

다시 encryptIdPw() 함수로 돌아가서 session_keys를 처리하기 위해
keySplit() 함수를 찾아보았습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function keySplit(a) {
	keys = a.split(",");
	if (!a || !keys[0] || !keys[1] || !keys[2] || !keys[3]) {
		return false;
	}
	sessionkey = keys[0];
	keyname = keys[1];
	evalue = keys[2];
	nvalue = keys[3];
	$("encnm").value = keyname;
	return true
}

모바일 페이지에서 볼 수 있는 session_keys 값은 콤마를 기준으로
4개의 값으로 구분되어 있었는데 해당 함수에서는 각각을
sessionKey, encnm, evalue, nvalue으로 분리했습니다.

여기서 encnm 값을 우선적으로 가져올 수 있었고,
다음으로 encpw 값을 찾기 위해 RSA 암호화 부분을 탐색해봅니다.

1
2
3
4
5
rsa.setPublic(evalue, nvalue);
encpw.value = rsa.encrypt(
    getLenChar(sessionkey) + sessionkey +
    getLenChar(id.value) + id.value +
    getLenChar(pw.value) + pw.value);

session_keys에서 분리된 evaluenvalue로 RSA 공개키를 생성하고
마찬가지로 session_keys에 포함된 sessionKey 및 아이디, 비밀번호의 조합을
암호화한 결과가 encpw임을 확인할 수 있습니다.

파이썬에서는 공개키 생성을 rsa.PublicKey() 함수로 수행할 수 있으며
rsa.encrypt() 함수로 RSA 암호화를 진행할 수 있습니다.
해당 과정은 아래와 같이 구현됩니다.

1
2
3
publicKey = rsa.PublicKey(int(nvalue,16), int(evalue,16))
value = ''.join([chr(len(key))+key for key in [sessionKey, id, pw]])
encpw = rsa.encrypt(value.encode(), publicKey).hex()

여기까지의 과정으로 dynamicKey, encpw, encnm의 값을 얻을 수 있습니다.

bvsd 값 생성하기

마지막으로 필요한 bvsd 값에 대한 단서는 응답 문서 내에서
bvsd.1.3.8.min.js란 명칭으로 알기 쉽게 확인할 수 있지만
그 내용은 가독성 면에서 쉽게 해석하기 어려웠습니다.

다른 자료를 참고했을 때 bvsd는 브라우저가 정상적인지 여부를 파악하기 위한 값으로
해당 값이 없을 경우 로그인 과정에서 캡차를 발생시킨다는 것을 알 수 있었습니다.

bvsd.1.3.8.min.js에서 주목할 부분은 uuidencData를 생성하는 부분인데
아래 코드에서 encDatao라는 값을 인코딩하는 것으로 추측됩니다.

bvsd

o 값을 코드 내에서 찾아보니 아래와 같이 디바이스의 마우스 상태 등을
기록한 값임을 확인할 수 있었습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
o = {
    a: n,
    b: "1.3.8",
    c: (0, m["default"])(),
    d: r,
    e: this._deviceOrientation.get(),
    f: this._deviceMotion.get(),
    g: this._mouse.get(),
    j: this._fpDuration || y.NOT_YET,
    h: this._fpHash || "",
    i: this._fpComponent || []
};

하지만 각각의 값을 해석하고 생성하는 것은 쉽지 않았기에
이미 완성된 코드를 참고하여 set_bvsd() 메소드를 정의했습니다.

encData의 인코딩에는 lzstring 모듈의
LZString.compressToEncodedURIComponent() 함수를 활용했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from lzstring import LZString
import uuid

ENC_DATA = lambda uuid, userid, passwd: str({
    "a": f"{uuid}-4",
    "b": "1.3.4",
    "d": [{
        "i": "id",
        "b": {"a": ["0", userid]},
        "d": userid,
        "e": "false",
        "f": "false"
    },
    {
        "i": passwd,
        "e": "true",
        "f": "false"
    }],
    "h": "1f",
    "i": {"a": "Mozilla/5.0"}
}).replace('\'','\"')

class NaverLogin(LoginSpider):
    def set_bvsd(self):
        uuid4 = str(uuid.uuid4())
        encData = LZString.compressToEncodedURIComponent(ENC_DATA(uuid4, self.userid, self.passwd))
        self.bvsd = str({"uuid":uuid4, "encData":encData}).replace('\'','\"')

네이버 로그인 구현

지금까지의 과정을 통해 네이버 로그인에 필요한
dynamicKey, encpw, bvsd, encnm 값을 생성하는 법을 파악했습니다.

이를 NaverLogin 클래스의 메소드로 구현해보겠습니다.

RSA 암호화 구현

먼저 dynamicKey와 함께 encpw, encmn 생성에 필요한
session_keys를 가져오기 위한 메소드 fetch_keys()와,
RSA 암호화를 통해 encpw 값을 구하는 set_encpw() 메소드를 정의합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from bs4 import BeautifulSoup
import rsa

LOGIN_URL = "https://nid.naver.com/nidlogin.login"

class NaverLogin(LoginSpider):
    def fetch_keys(self):
        response = self.get(LOGIN_URL, headers=self.get_headers(host=LOGIN_URL), params={"svctype":"262144"})
        source = BeautifulSoup(response.text, 'lxml')
        keys = source.find("input", {"id":"session_keys"}).attrs.get("value")
        self.sessionKey, self.encnm, n, e = keys.split(",")
        self.dynamicKey = source.find("input", {"id":"dynamicKey"}).attrs.get("value")
        self.publicKey = rsa.PublicKey(int(n,16), int(e,16))

session_keys의 경우 모바일 로그인 페이지에서만 가져올 수 있기 때문에
svctype=262144를 GET 요청의 파라미터로 전달해 모바일 로그인 페이지를 가져옵니다.

nvalueevalue는 별도의 변수로 저장하지 않고
publicKey를 생성해 클래스 변수로 저장합니다.

1
2
3
4
class NaverLogin(LoginSpider):
    def set_encpw(self):
        value = "".join([chr(len(key))+key for key in [self.sessionKey, self.userid, self.passwd]])
        self.encpw = rsa.encrypt(value.encode(), self.publicKey).hex()

앞에서 가져온 sessionKey와 함께 미리 초기화된 네이버 아이디 및 비밀번호를
조합 및 암호화하여 encpw를 생성합니다.

POST 요청 구현

미리 정의한 set_bvsd() 메소드를 포함해 모든 준비 과정이 마무리되었습니다.

클래스 변수로 저장된 암호화된 값들을 데이터에 담아 POST 로그인 요청을 보내는
login() 메소드는 다음과 같이 정의할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NAVER_URL = "https://www.naver.com"

LOGIN_DATA = lambda dynamicKey, encpw, bvsd, encnm: {
    "localechange": "",
    "dynamicKey": dynamicKey,
    "encpw": encpw,
    "enctp": "1",
    "svctype": "1",
    "smart_LEVEL": "1",
    "bvsd": bvsd,
    "encnm": encnm,
    "locale": "ko_KR",
    "url": quote_plus(NAVER_URL),
    "id": "",
    "pw": "",
}

class NaverLogin(LoginSpider):
    def login(self):
        self.fetch_keys()
        self.set_encpw()
        self.set_bvsd()
        data = LOGIN_DATA(self.dynamicKey, self.encpw, self.bvsd, self.encnm)
        headers = self.get_headers(LOGIN_URL, referer=LOGIN_URL)
        headers["Content-Type"] = "application/x-www-form-urlencoded"
        headers["Upgrade-Insecure-Requests"] = "1"
        self.post(LOGIN_URL, data=data, headers=headers)

POST 요청 시 전달되었던 데이터와 동일한 값을 반환하는 LOGIN_DATA 함수를 생성하고
암호화된 값을 전달해 최종적인 POST 데이터를 만들었습니다.

해당 데이터로 요청을 보낼 경우 정상적인 응답을 받게 되고
NaverLogin 세션 객체의 쿠키 값을 확인하면 아래와 같은 결과를 확인할 수 있습니다.

1
2
3
4
5
naver = NaverLogin("userid", "passwd")
naver.login()
naver.get_cookies()
======================================
'NID_AUT=...; NID_JKL=...; NID_SES=...; nid_inf=1228467713'

또한 해당 결과는 개발자 도구에서도 응답 헤더의 set-cookie 값에서 찾아볼 수 있습니다.

nid-cookies

지금까지의 과정으로 네이버 로그인 과정을 거쳤을 때,
게시글의 서두에서 언급한 쿠키 값의 목록 중에서 일부 값을 획득할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cookies = {
    "NNB": "...",
    "nid_inf": "...",
    "NID_AUT": "...",
    "NID_SES": "...",
    "NID_JKL": "...",
    "CBI_SES": "...",
    "CBI_CHK": "...",
    "NSI": "...",
}

이 중에서 NNB의 경우 네이버 페이지 접속 시 기본적으로 부여되는 값이기 때문에 무시하고
NID_AUT, NID_JKL, NID_SES가 채워졌습니다.

나머지 값들은 스마트스토어센터 로그인 과정에서 얻을 수 있기 때문에
다음 게시글에서 다뤄보도록 하겠습니다.