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