Авторизироваться на vk.com с помощью Dart
Мне необходимо войти в аккаунт на vk.com через мое Flutter приложение (Без создания приложения в ВК и получения access_token). Я проанализировал запросы, которые делает браузер, и выяснил, что сначала необходимо заполнить формы:
{act: login, role: al_frame, expire: , to: aW5kZXgucGhw, recaptcha: , captcha_sid: , captcha_key: , _origin: https://vk.com, ip_h: abcdefgh0123456789, lg_h: 0123456789abcdefgh, ul: , email: +79999999999, pass: 0123456789}
Значения и ключи находятся внутри html (name:value) :
<div id="index_login" class="page_block index_login">
<form method="post" name="login" id="index_login_form" action="https://login.vk.com/?act=login">
<input type="hidden" name="act" id="act" value="login">
<input type="hidden" name="role" value="al_frame">
<input type="hidden" name="expire" id="index_expire_input" value="">
<input type="hidden" name="_origin" value="https://vk.com">
<input type="hidden" name="ip_h" value="abcdefgh0123456789">
<input type="hidden" name="lg_h" value="0123456789abcdefgh">
<input type="text" class="big_text" name="email" id="index_email" value="" placeholder="Телефон или email">
<input type="password" class="big_text" name="pass" id="index_pass" value="" placeholder="Пароль" onkeyup="toggle('index_expire', !!this.value);toggle('index_forgot', !this.value)">
<button id="index_login_button" class="index_login_button flat_button button_big_text">Войти</button>
<div class="forgot">
<div class="checkbox" id="index_expire" onclick="checkbox(this);ge('index_expire_input').value=isChecked(this)?1:'';" role="checkbox" aria-checked="false" tabindex="0">Чужой компьютер</div>
<a id="index_forgot" class="index_forgot" href="/restore" target="_top">Забыли пароль?</a>
</div>
</form>
</div>
А тут находится значение для ключа to (оно всегда равно aW5kZXgucGhw):
window.handlePageParams && handlePageParams({"id":0,"loc":"","noleftmenu":1,"wrap_page":1,"width":960,"width_dec":0,"width_dec_footer":0,"top_home_link_class":"TopHomeLink TopHomeLink--logoWithText","body_class":"index_page new_header_design ","to":"aW5kZXgucGhw","counters":[],"pvbig":0,"pvdark":1});addEvent(document, 'click', onDocumentClick);
Headers я взял из запроса, который сделал браузер (скопировал все, кроме cookie).
Далее по нажатию кнопки "Войти" браузер делает POST запрос по адресу: https://login.vk.com/?act=login (который так же находится внутри html) и передает словарь вышеупомянутых параметров. В ответ приходит 302 код состояния http (т.е. redirect как я понял). Headers, полученные браузером и моим приложением почти совпадают, за исключением location.
location в браузере : https://vk.com/login.php?act=slogin&to=aW5kZXgucGhw&s=1&__q_hash=149c8d2a4784c9a019d4fb720469378e
location в Dart : https://vk.com/login?&to=&s=0&m=1&email=
ОБНОВЛЕНО
Видимо мой POST запрос неверно составлен или что-то в этом роде, раз ответы от сервера отличаются.
Вот мой код на Dart:
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';
Future<void> login() async {
Map<String, String> headers = {
'accept':
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
'cache-control': 'max-age=0',
//no cookies
'referer': 'https://vk.com',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'same-origin',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36',
};
String email='+79999999999'; //email or phone
String password='0123456789';
http.Client client = http.Client();
try {
http.Response uriResponse =
await client.get('https://vk.com/', headers: headers);
// get params for POST request
String html = uriResponse.body;
String start = '<form method="POST" name="login" id="quick_login_form"',
end = "toggle('quick_forgot', !this.value)";
html = html.substring(html.indexOf(start) + start.length);
html = html.substring(0, html.indexOf(end));
start = 'action="';
end = '"';
String action = html.substring(html.indexOf(start) + start.length);
html = action.substring(action.indexOf(end) + end.length);
action = action.substring(0, action.indexOf(end));
print(action); // https://login.vk.com/?act=login
List<String> parts = html.split('<input type="');
Map<String, String> forms = {};
parts.removeAt(0);
String name, value;
parts.forEach((e) {
if (e.startsWith('hidden')) {
start = 'name="';
end = '"';
name = e.substring(e.indexOf(start) + start.length);
name = name.substring(0, name.indexOf(end));
start = 'value="';
value = e.substring(e.indexOf(start) + start.length);
value = value.substring(0, value.indexOf(end));
forms[name] = value;
} else if (e.contains('name="')) {
start = 'name="';
end = '"';
name = e.substring(e.indexOf(start) + start.length);
name = name.substring(0, name.indexOf(end));
if (name == 'email') {
value = email;
forms[name] = value;
} else if (name == 'pass') {
value = password;
forms[name] = value;
}
}
});
html = uriResponse.body;
start = 'handlePageParams(';
html = html.substring(html.indexOf(start) + start.length);
start = '"to":"';
html = html.substring(html.indexOf(start) + start.length);
forms['to'] = html.substring(0, html.indexOf('"'));
print(forms
.toString()); // {act: login, role: al_frame, expire: , to: aW5kZXgucGhw, recaptcha: , captcha_sid: , captcha_key: , _origin: https://vk.com, ip_h: abcdefgh0123456789, lg_h: 0123456789abcdefgh, ul: , email: +79999999999, pass: 0123456789}
//headers['content-length']='210'; error...
headers['content-type']='application/x-www-form-urlencoded';
headers['origin']='https://vk.com';
headers['referer']='https://vk.com/';
headers['sec-fetch-dest']='iframe';
var resp = await client.post(
action,
headers: headers,
body: jsonEncode(forms),
);
print(resp.statusCode); // 302
print(resp.headers); // {connection: keep-alive, x-powered-by: KPHP/7.4.106227, location: https://vk.com/login?&to=&s=0&m=1&email=, cache-control: no-store, set-cookie: remixir=DELETED; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; domain=.vk.com; secure; HttpOnly; SameSite=None,remixlang=0; expires=Sat, 26 Feb 2022 20:14:15 GMT; path=/; domain=.vk.com; secure; SameSite=None, date: Thu, 25 Feb 2021 20:39:20 GMT, content-encoding: gzip, strict-transport-security: max-age=15768000, content-length: 20, p3p: CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT", content-type: text/html; charset=windows-1251, server: kittenx}
} finally {
client.close();
}
}
ДОБАВЛЕНО
Реализация на Python:
import lxml.html
import requests
url='https://vk.com/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language':'ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3',
'Accept-Encoding':'gzip, deflate',
'Connection':'keep-alive',
'DNT':'1'
}
session = requests.session()
data = session.get(url, headers=headers)
page = lxml.html.fromstring(data.content)
form = page.forms[0]
form.fields['email'] = 'EMAIL/PHONE'
form.fields['pass'] = 'PASSWORD'
response = session.post(form.action, data=form.form_values())
print('onLoginDone' in response.text)
Ответы (1 шт):
Все оказалось проще... Неделя была потрачена не зря)))
Необходимы были библиотеки: dio, dio_cookie_manager, cookie_jar.
Одной из главных и самых глупых ошибок было использование обычного Map вместо FormData для отправки POST запроса.
import 'dart:collection';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
Future login() async {
var dio = Dio();
var cookieJar = CookieJar();
dio.interceptors.add(CookieManager(cookieJar));
dio.options.followRedirects = true;
const String phone = 'PHONE';
const String password = 'PASSWORD';
Map<String, String> headers = HashMap();
headers.addAll({
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'DNT': '1',
});
String url = 'https://vk.com/';
dio.options.headers = headers;
cookieJar.loadForRequest(Uri.parse('https://login.vk.com/'));
Response response = await dio.get(url);
String remixstid = response.headers['set-cookie'].toString();
remixstid = remixstid
.substring(remixstid.indexOf('remixstid') + 'remixstid'.length + 1);
remixstid = remixstid.substring(0, remixstid.indexOf(';'));
String remixlgk = response.headers['set-cookie'].toString();
remixlgk =
remixlgk.substring(remixlgk.indexOf('remixlgk') + 'remixlgk'.length + 1);
remixlgk = remixlgk.substring(0, remixlgk.indexOf(';'));
String remixsid = response.headers['set-cookie'].toString();
remixsid =
remixsid.substring(remixsid.indexOf('remixsid') + 'remixsid'.length + 1);
remixsid = remixsid.substring(0, remixsid.indexOf(';'));
String remixusid = response.headers['set-cookie'].toString();
remixusid = remixusid.substring(remixusid.indexOf('remixusid')+'remixusid'.length+1);
remixusid=remixusid.substring(0,remixusid.indexOf(';'));
String html = response.data;
html = html.substring(html.indexOf('id="quick_login_form"'));
html = html.substring(0, html.indexOf('</form>'));
String start = 'action="', end = '"';
String action = html.substring(html.indexOf(start) + start.length);
html = action.substring(action.indexOf(end) + end.length);
action = action.substring(0, action.indexOf(end));
List<String> parts = html.split('<input type="');
Map<String, String> forms = HashMap();
parts.removeAt(0);
String name, value;
parts.forEach((e) {
if (e.startsWith('hidden')) {
start = 'name="';
end = '"';
name = e.substring(e.indexOf(start) + start.length);
name = name.substring(0, name.indexOf(end));
start = 'value="';
value = e.substring(e.indexOf(start) + start.length);
value = value.substring(0, value.indexOf(end));
forms[name] = value;
} else if (e.contains('name="')) {
start = 'name="';
end = '"';
name = e.substring(e.indexOf(start) + start.length);
name = name.substring(0, name.indexOf(end));
if (name == 'email') {
value = phone;
forms[name] = value;
} else if (name == 'pass') {
value = password;
forms[name] = value;
} else
print('No such constant!');
}
});
forms['to'] = 'bG9naW4/bT0xJmVtYWlsPWFzJnRvPWFXNWtaWGd1Y0dodw--';
var forms2 = FormData.fromMap(forms);
response = await dio.post(
'https://login.vk.com/',
queryParameters: {'act': 'login'},
data: forms2,
options: Options(
followRedirects: true,
validateStatus: (status) {
return status < 500;
},
),
);
List<String> lst = [
'remixbdr=0',
'remixsid=$remixsid',
'remixusid=$remixusid',
'remixstid=$remixstid',
'remixflash=0.0.0',
'remixscreen_width=1920',
'remixscreen_height=1080',
'remixscreen_dpr=1',
'remixscreen_depth=24',
'remixscreen_orient=1',
'remixscreen_winzoom=1',
'remixseenads=0',
'remixlang=0',
'remixlhk=$remixlgk',
];
response.headers.forEach((name, values) {
if (name.contains('remixq')) {
lst.add(name + '=' + values.join());
}
});
dio.options.headers['cookie'] = lst.join('; ');
String location = response.headers['location'].toString();
int length = location.length - 1;
location = location.substring(1, length);
Map<String, String> queryParams = {};
String tmp = location.substring(location.indexOf('?') + 1);
location = location.substring(0, location.indexOf('?'));
List<String> params = tmp.split('&');
params.forEach((element) {
List<String> kv = element.split('=');
queryParams.putIfAbsent(kv[0], () => kv[1]);
});
queryParams['to'] = 'aW5kZXgucGhw';
response = await dio.get(location, queryParameters: queryParams);
print('LOGIN : '+response.data.toString().contains('onLoginDone').toString());
}