Что случилось?
Здравствуй, дорогой читатель. Если тебе хотя бы однажды доводилось работать с API Вконтакте и при этом писать все на python, вероятно, авторизация приложения заставила тебя сделать несколько приседаний, после которых ног либо не чувствуешь и падаешь в обморок, либо вкачиваешь квадрицепс и все же пробиваешь API, как Ван Дамм.
По какой-то причине этот, казалось бы, самый непримечательный этап поначалу отнимает огромное количество сил и времени. Моя задача: помочь читателям Хабра избежать травм ног.
Далее я предлагаю рассмотреть небольшую библиотеку, позволяющую в одну строчку авторизовать свое приложение для конкретного пользователя и получить access_token. В конце статьи представлена ссылка на github-репозиторий этой библиотеки с quickstart'ом в README-файле.
Задача
Хотим небольшой модуль, который позволяет провести авторизацию красиво, универсально и максимально надежно, а использовать который очень просто.
Стоит сказать, что данное решение является усовершенствованием и обобщением варианта, предложенного в этой статье.
Итак, используем python3.5, библиотеку для html запросов requests и getpass для скрытого ввода пароля.
Наша задача: несколько раз обратиться по верному адресу, каждый раз парсить <form>, отправлять ответ и наконец получить желанный access_token.
Реализация
Начнем с создания класса. При инициализации будем требовать список "разрешений", к которым приложение хочет получить доступ, id этого приложения и версию API VK. Плюсом добавим несколько необязательных параметров, значение каждого из которых прояснится далее.
class VKAuth(object):
def __init__(self, permissions, app_id, api_v, email=None, pswd=None, two_factor_auth=False, security_code=None, auto_access=True):
"""
Args:
permissions: list of Strings with permissions to get from API
app_id: (String) vk app id that one can get from vk.com
api_v: (String) vk API version
"""
self.session = requests.Session()
self.form_parser = FormParser()
self.user_id = None
self.access_token = None
self.response = None
self.permissions = permissions
self.api_v = api_v
self.app_id = app_id
self.two_factor_auth= two_factor_auth
self.security_code = security_code
self.email = email
self.pswd = pswd
self.auto_access = auto_access
if security_code != None and two_factor_auth == False:
raise RuntimeError('Security code provided for non-two-factor authorization')
Как было сказано в уже упомянутой статье, нам необходимо искусно ворочать cookie и redirect'ы. Все это за нас делает библиотека requests с объектом класса Session. Заведем и себе такой в поле self.session. Для парсинга html документа используется стандартный класс HTMLParser из модуля html.parser. Для парсера тоже написан класс (FormParser), разбирать который большого смысла нет, так как он почти полностью повторяет таковой из упомянутой статьи. Существенное отличие лишь в том, что использованный здесь позволяет изящно отклонить авторизацию приложения на последнем шаге, если вы вдруг передумали.
Поля user_id и access_token будут заполнены после успешной авторизации, response хранит в себе результат последнего html запроса.
Пользователю библиотеки предоставим один-единственный метод – authorize, который совершает 3 шага:
- запрос на авторизацию приложения
- авторизация пользователя
2.1 введение кода-ключа в случае двух-факторной авторизации - подтверждение разрешения на использование
permissions
Пройдемся по каждому шагу.
Шаг 1. Запрос на авторизацию приложения
Аккуратно составляем url запроса (про параметры можно прочитать здесь), отправляем запрос и парсим полученный html.
def authorize(self):
api_auth_url = 'https://oauth.vk.com/authorize'
app_id = self.app_id
permissions = self.permissions
redirect_uri = 'https://oauth.vk.com/blank.html'
display = 'wap'
api_version = self.api_v
auth_url_template = '{0}?client_id={1}&scope={2}&redirect_uri={3}&display={4}&v={5}&response_type=token'
auth_url = auth_url_template.format(api_auth_url, app_id, ','.join(permissions), redirect_uri, display, api_version)
self.response = self.session.get(auth_url)
# look for <form> element in response html and parse it
if not self._parse_form():
raise RuntimeError('No <form> element found. Please, check url address')Шаг 2. Авторизация пользователя
Реализованы методы _log_in() и _two_fact_auth() для [не]успешной авторизации пользователя в вк, если он не авторизован (а он точно не авторизован). Оба метода используют ранее определенные поля email, pswd, two_factor_auth и security_code. Если какое-то из полей не было подано аргументом при инициализации объекта класса VKAuth, их попросят ввести в консоли, а случае неудачи попросят ввести заново. Двух-факторная авторизация опциональна и по умолчанию отключена, и наш модуль уведомляет пользователя о ее присутствии ошибкой.
#look for <form> element in response html and parse it
if not self._parse_form():
raise RuntimeError('No <form> element found. Please, check url address')
else:
# try to log in with email and password (stored or expected to be entered)
while not self._log_in():
pass;
# handling two-factor authentication
# expecting a security code to enter here
if self.two_factor_auth:
self._two_fact_auth()def _log_in(self):
if self.email == None:
self.email = ''
while self.email.strip() == '':
self.email = input('Enter an email to log in: ')
if self.pswd == None:
self.pswd = ''
while self.pswd.strip() == '':
self.pswd = getpass.getpass('Enter the password: ')
self._submit_form({'email': self.email, 'pass': self.pswd})
if not self._parse_form():
raise RuntimeError('No <form> element found. Please, check url address')
# if wrong email or password
if 'pass' in self.form_parser.params:
print('Wrong email or password')
self.email = None
self.pswd = None
return False
elif 'code' in self.form_parser.params and not self.two_factor_auth:
raise RuntimeError('Two-factor authentication expected from VK.\nChange `two_factor_auth` to `True` and provide a security code.')
else:
return True
def _two_fact_auth(self):
prefix = 'https://m.vk.com'
if prefix not in self.form_parser.url:
self.form_parser.url = prefix + self.form_parser.url
if self.security_code == None:
self.security_code = input('Enter security code for two-factor authentication: ')
self._submit_form({'code': self.security_code})
if not self._parse_form():
raise RuntimeError('No <form> element found. Please, check url address')Шаг 3. Подтверждение permissions и получение access_token
Самое сложное позади. Теперь дело за малым. Используем наше усовершенствование парсера формы, чтоб найти в только что поступившем к нам html документе кнопку с надписью "Allow" и вытащить из нее url подтверждения авторизации. Рядом находится кнопка с отказом – сохраним и ее url. Поле auto_access по умолчанию находится в состоянии True, так что это подтверждение ни чуть не должно осложнить нам жизнь.
Наконец, сохраним полученные access_token и user_id из url, который был передан после подтверждения авторизации.
Теперь можно весело пользоваться VK API.
http://REDIRECT_URI#access_token= 533bacf01e11f55b536a565b57531ad114461ae8736d6506a3&expires_in=86400&user_id=8492
# allow vk to use this app and access self.permissions
self._allow_access()
# now get access_token and user_id
self._get_params()def _allow_access(self):
parser = self.form_parser
if 'submit_allow_access' in parser.params and 'grant_access' in parser.url:
if not self.auto_access:
answer = ''
msg = 'Application needs access to the following details in your profile:\n' + str(self.permissions) + '\n' + 'Allow it to use them? (yes or no)'
attempts = 5
while answer not in ['yes', 'no'] and attempts > 0:
answer = input(msg).lower().strip()
attempts-=1
if answer == 'no' or attempts == 0:
self.form_parser.url = self.form_parser.denial_url
print('Access denied')
self._submit_form({}) def _get_params(self):
try:
params = self.response.url.split('#')[1].split('&')
self.access_token = params[0].split('=')[1]
self.user_id = params[2].split('=')[1]
except IndexError(e):
print(e)
print('Coudln\'t fetch token')github: VKAuth
Оставляйте комментарии и отзывы здесь и на github. Удачи на полях сражений, и берегите ноги.
Комментарии (7)

rockin
20.07.2016 16:45По-моему, в авторизации на ВК нет вообще ничего сложного
Намного более интересна авторизация на хабре ;) без участия пользователя

synedra
20.07.2016 17:30Объясните, а зачем вы делаете вот это? На этом шаге же не проверяется ни валидность данных, ничего, они просто перебрасываются из аттрибута в переменную. Зато response, который вне этой функции не нужен, зачем-то летит в аттрибут.
app_id = self.app_id permissions = self.permissions redirect_uri = 'https://oauth.vk.com/blank.html' display = 'wap' api_version = self.api_v
good_move
20.07.2016 17:45Если посмотреть внимательнее, то можно заметить self.response в методах _parse_form, _submit_form и еще парочке…
Перебрасывание же сделано исключительно в целях эстетики)

G-M-A-X
20.07.2016 23:101. А капчу обрабатывать? :)
2. А почему не переадресовывать пользователя на сайт ВК для авторизации/разрешения?
Как-то стремно водить свои данные где попало.
good_move
21.07.2016 06:131. Об этом, признаться, даже не думал. Интересно, что пока и без капчи все работает стабильно (хотя есть возможность ошибаться и вводить данные еще раз)
2. Все переадресации специально скрыты и обрабатываются под капотом. Изначально данная либа написана скорее для максимально быстрого личного использования из терминала.
Tsyganov_Ivan
Опять либа для ВКонтакта и опять на Питоне?
Есть же хорошее решение, которое отлично работает — https://github.com/dimka665/vk
good_move
После 40 минут безуспешного поиска столь хорошего решения захотелось взять все в свои руки и написать что-нибудь не менее прекрасное :)