Что случилось?
Здравствуй, дорогой читатель. Если тебе хотя бы однажды доводилось работать с 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. Удачи на полях сражений, и берегите ноги.
