Что случилось?


Здравствуй, дорогой читатель. Если тебе хотя бы однажды доводилось работать с API Вконтакте и при этом писать все на python, вероятно, авторизация приложения заставила тебя сделать несколько приседаний, после которых ног либо не чувствуешь и падаешь в обморок, либо вкачиваешь квадрицепс и все же пробиваешь API, как Ван Дамм.


По какой-то причине этот, казалось бы, самый непримечательный этап поначалу отнимает огромное количество сил и времени. Моя задача: помочь читателям Хабра избежать травм ног.


Далее я предлагаю рассмотреть небольшую библиотеку, позволяющую в одну строчку авторизовать свое приложение для конкретного пользователя и получить access_token. В конце статьи представлена ссылка на github-репозиторий этой библиотеки с quickstart'ом в README-файле.


Задача


Хотим небольшой модуль, который позволяет провести авторизацию красиво, универсально и максимально надежно, а использовать который очень просто.
Стоит сказать, что данное решение является усовершенствованием и обобщением варианта, предложенного в этой статье.


Итак, используем python3.5, библиотеку для html запросов requests и getpass для скрытого ввода пароля.


Наша задача: несколько раз обратиться по верному адресу, каждый раз парсить <form>, отправлять ответ и наконец получить желанный access_token.


Реализация


Начнем с создания класса. При инициализации будем требовать список "разрешений", к которым приложение хочет получить доступ, id этого приложения и версию API VK. Плюсом добавим несколько необязательных параметров, значение каждого из которых прояснится далее.


Метод __init__
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 шага:


  1. запрос на авторизацию приложения
  2. авторизация пользователя
    2.1 введение кода-ключа в случае двух-факторной авторизации
  3. подтверждение разрешения на использование permissions

Пройдемся по каждому шагу.


Шаг 1. Запрос на авторизацию приложения


Аккуратно составляем url запроса (про параметры можно прочитать здесь), отправляем запрос и парсим полученный html.


Метод authorize для Шага 1
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, их попросят ввести в консоли, а случае неудачи попросят ввести заново. Двух-факторная авторизация опциональна и по умолчанию отключена, и наш модуль уведомляет пользователя о ее присутствии ошибкой.


Метод authorize для Шага 2 (продолжение Шага 1)
#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()

Метод _log_in для Шага 2
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

Метод _two_fact_auth для Шага 2
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

Метод authorize для Шага 3
    # allow vk to use this app and access self.permissions
    self._allow_access()

     # now get access_token and user_id
     self._get_params()

Метод _allow_access для Шага 3
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({})

Метод _get_params для Шага 3
    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)


  1. Tsyganov_Ivan
    20.07.2016 16:22
    +2

    Опять либа для ВКонтакта и опять на Питоне?
    Есть же хорошее решение, которое отлично работает — https://github.com/dimka665/vk


    1. good_move
      20.07.2016 17:41
      +1

      После 40 минут безуспешного поиска столь хорошего решения захотелось взять все в свои руки и написать что-нибудь не менее прекрасное :)


  1. rockin
    20.07.2016 16:45

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


  1. 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
    


    1. good_move
      20.07.2016 17:45

      Если посмотреть внимательнее, то можно заметить self.response в методах _parse_form, _submit_form и еще парочке…
      Перебрасывание же сделано исключительно в целях эстетики)


  1. G-M-A-X
    20.07.2016 23:10

    1. А капчу обрабатывать? :)
    2. А почему не переадресовывать пользователя на сайт ВК для авторизации/разрешения?
    Как-то стремно водить свои данные где попало.


    1. good_move
      21.07.2016 06:13

      1. Об этом, признаться, даже не думал. Интересно, что пока и без капчи все работает стабильно (хотя есть возможность ошибаться и вводить данные еще раз)
      2. Все переадресации специально скрыты и обрабатываются под капотом. Изначально данная либа написана скорее для максимально быстрого личного использования из терминала.