Selenuim ChromeDriver自動登入試驗
1.環境
- chrome版本
chrome://version/
Google Chrome 67.0.3396.99 (正式版本) (32 位) (cohort: Stable)
chrome安裝位置
%USERPROFILE%\AppData\Local\Google\Chrome\Application
- chromdriver版本
>chromedriver -v
ChromeDriver 2.40.565498 (ea082db3280dd6843ebfb08a625e3eb905c4f5ab)
符合ChromeDriver與Chrome的對應關係:
http://chromedriver.storage.googleapis.com/2.40/notes.txt
----------ChromeDriver v2.40 (2018-06-07)----------
Supports Chrome v66-68
- selenium-java.jar版本
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.12.0</version>
</dependency>
2.登入頁面
2個登入頁面
(1)https://login.taobao.com/member/login.jhtml
(2)https://login.taobao.com/member/login.jhtml?style=mini
(1)預設是手機掃碼,安全登入,需要切換到密碼登入J_Quick2Static
driver.findElement(By.id("J_Quick2Static")).click();
(2)不需要切換,否則產生以下異常:
org.openqa.selenium.ElementNotVisibleException: element not visible
#J_Quick2Static是不可見的.
試驗採用https://login.taobao.com/member/login.jhtml?style=mini
3.基本程式碼
基本程式碼如下:
public class ExampleForChrome { static String chromePath = "C:/Users/Think/AppData/Local/Google/Chrome/Application/"; static int DEFAULT_TIMEOUT = 15; static String url = "https://login.taobao.com/member/login.jhtml?style=mini"; static String username = "wherer"; static String password = "xxxx"; public static void main(String[] args) { test1(); ///< 具體的測試函式 } static public void waitTime(int time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } }
後續測試替換不同的測試函式.
4.測試記錄
4.1測試1
特點:
.計算偏移,拖動滑塊
.執行一次
程式碼:
static void test1() {
System.setProperty("webdriver.chrome.driver", chromePath + "chromedriver.exe");
ChromeOptions Options = new ChromeOptions();
WebDriver driver = new ChromeDriver(Options);
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
driver.get(url);
driver.findElement(By.id("TPL_username_1")).sendKeys(username);
driver.findElement(By.id("TPL_password_1")).sendKeys(password);
/// < 滑軌
WebElement sliderWay = driver.findElement(By.id("nc_1_n1t"));
Dimension dWay = sliderWay.getSize();
/// < 滑塊
WebElement slider = driver.findElement(By.xpath("//*[@id='nc_1_n1z']"));
Dimension dSlider = slider.getSize();
/// < 拖動滑塊提示,出錯,驗證通過資訊
WebElement scaleText = driver.findElement(By.className("nc-lang-cnt"));/// ("nc_1__scale_text"));
String text = scaleText.getText();
/// < 拖動滑塊偏移(x)
int offset = dWay.width - dSlider.width;
Actions action = new Actions(driver);
action.clickAndHold(slider).moveByOffset(offset, 0).perform();
text = driver.findElement(By.className("nc-lang-cnt")).getText();
while (text.startsWith("載入中") || text.isEmpty()) {
waitTime(1000);
text = driver.findElement(By.className("nc-lang-cnt")).getText();
}
if (text.startsWith("哎呀")) {
System.out.println(text); /// < 程式進入這裡,提示"哎呀,出錯了,點選重新整理再來一次"
/// < 重新整理滑塊
driver.findElement(By.xpath("//*[@id='nocaptcha']/div/span/a")).click();
return;
}
driver.findElement(By.id("J_SubmitStatic")).click();
/// < 檢查頁面是否跳轉
String newUrl = driver.getCurrentUrl();
driver.close();
}
結果:
.提示"哎呀,出錯了,點選重新整理再來一次"
4.2測試2
特點:
.滑塊拖動採用增量方式
.固定增量和隨機增量,x,y方向
.重複測試
程式碼:
static void test2() {
System.setProperty("webdriver.chrome.driver", chromePath + "chromedriver.exe");
ChromeOptions Options = new ChromeOptions();
WebDriver driver = new ChromeDriver(Options);
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
driver.get(url);
driver.findElement(By.id("TPL_username_1")).sendKeys(username);
driver.findElement(By.id("TPL_password_1")).sendKeys(password);
/// < 滑軌
WebElement sliderWay = driver.findElement(By.id("nc_1_n1t"));
/// < 滑塊
WebElement slider = driver.findElement(By.xpath("//*[@id='nc_1_n1z']"));
/// < 拖動滑塊提示,出錯,驗證通過資訊
WebElement scaleText = driver.findElement(By.className("nc-lang-cnt"));/// ("nc_1__scale_text"));
String text = scaleText.getText();
/// < 拖動滑塊偏移(x)
int offset = 200; /// < 初始偏移
int step = 5;/// < 增量步長
int yOffset = 0;
Actions action = new Actions(driver);
action.clickAndHold(slider);
action.moveByOffset(offset, 0).perform();
int flag = 0; /// 滑塊驗證是否通過
do {
text = driver.findElement(By.className("nc-lang-cnt")).getText();
while (text.startsWith("載入中") || text.isEmpty()) {
waitTime(1000);
text = driver.findElement(By.className("nc-lang-cnt")).getText();
}
if (text.startsWith("請按住滑塊,拖動到最右邊")) {
// action.moveByOffset(step,0); ///< 每次x偏移固定的量,y不變
int y = 0; /// < 每次x,y偏移在一個範圍內隨機,模擬人工操作.
do {
y = (int) (Math.random() * 10);
if (y % 2 == 0)
y *= -1;
yOffset += y;
if (Math.abs(yOffset) < 10)
break;
} while (true);
int x = (int) (Math.random() * step);
action.moveByOffset(x, y).perform();
offset += x;
if (x > 0)
waitTime(2000);
System.out.println(MessageFormat.format("x_offset={0},y_offset={1}", offset, yOffset));
continue;
}
if (text.startsWith("哎呀")) {
System.out.println(text); /// < 程式進入這裡,提示"哎呀,出錯了,點選重新整理再來一次"
/// < 重新整理滑塊
driver.findElement(By.xpath("//*[@id='nocaptcha']/div/span/a")).click();
// break;
/// < 模擬重新整理後再次嘗試
action = new Actions(driver);
slider = driver.findElement(By.xpath("//*[@id='nc_1_n1z']"));
action.clickAndHold(slider);
offset = 200;
yOffset = 0;
action.moveByOffset(offset, 0).perform();
continue;
}
flag = 1;
} while (flag == 0);
if (flag == 1) {
driver.findElement(By.id("J_SubmitStatic")).click();
/// < 檢查頁面是否跳轉
String newUrl = driver.getCurrentUrl();
}
driver.close();
}
結果:
.重複測試過程中,始終出現"哎呀,出錯了,點選重新整理再來一次"
4.3測試3
特點:
.試圖利用本地的cookie.
關於利用cookie,
(1)cookie來源
在chrome瀏覽器中開啟登入頁面,登入成功後通過開發者工具複製出cookies內容儲存在磁碟檔案中.這個檔案是可以被網頁資料抓取程式使用的,只是會過期.這也是此試驗的原因.
(1)在導航到頁面(driver.get)之前不能設定cookie.
driver.manage().addCookie(c);
driver.get(url);
addCookie產生異常:
org.openqa.selenium.WebDriverException: unable to set cookie
https://github.com/detro/ghostdriver/issues/178
其中,關於WebDriver的規範描述如下:
After discussing with @AutomatedTester, I filed an issue against the WebDriver W3C specs https://www.w3.org/Bugs/Public/show_bug.cgi?id=20975
The correct behaviour is to FORBID setting cookies against a Session that has not navigated to a concrete domain.
Firefox behaviour is wrong.
意思可能是在導航到一個具體的域之前,禁止對session設定cookies,是W3C關於WebDriver的規範。而Firefox沒有遵守這一點。
(2)driver.get開啟網頁後,從cookie檔案中讀取,再driver.manage().addCookie(c).
driver.get(url);
後增加以下程式碼
load(cookiesFile);
for (Entry<String,Cookie> entry : cookies.entrySet()) {
driver.manage().addCookie(entry.getValue());
}
相關程式碼:
static IdentityHashMap<String, Cookie> cookies;
static void load(String cookiesFileName) {
cookies = new IdentityHashMap<String, Cookie>();
try {
File file = new File(cookiesFileName);
InputStreamReader isr = new InputStreamReader(new FileInputStream(file));
BufferedReader bufferedReader = new BufferedReader(isr);
String line = null;
while ((line = bufferedReader.readLine()) != null) {
String[] v = line.split("\\s+");
String s = v[4].replace("Z", " UTC");
Date d = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS Z");
try {
d = sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
Cookie c = new Cookie(v[0], v[1], v[2], v[3], d);
cookies.put(v[0], c);
}
isr.close();
} catch (Exception e) {
e.printStackTrace();
}
}
問題依舊!
(3)document.cookie設定cookie
來自下文的啟示:
https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/1227
Another work around is to just set cookies via javascript until addCookie is fixed.
程式碼如下:
driver.get(url);
load(cookiesFile);
JavascriptExecutor executor = (JavascriptExecutor) driver;
String js = "document.cookie=\"";
for (Entry<String, Cookie> entry : cookies.entrySet()) {
js += String.format("%s=%s; ", entry.getKey(), entry.getValue().getValue());
}
js += "\"";
executor.executeScript(js);
cookie之間用分號+空格分隔
問題依舊!
(4)直接利用chrome的cookies
Can I run Selenium ChromeDriver with cookies from actual Chrome installation?
https://stackoverflow.com/questions/34179420/can-i-run-selenium-chromedriver-with-cookies-from-actual-chrome-installation
ChromeOptions options = new ChromeOptions();
Options.addArguments("user-data-dir=C:/Users/Think/AppData/Local/Google/Chrome/User Data");
執行產生異常:
Exception in thread "main" org.openqa.selenium.WebDriverException: unknown error: failed to write prefs file
對此類異常,網上有的說法:
1.chrome安裝所在磁碟空間:解決方法有轉到其它磁碟用mklink連線; 清理臨時檔案
2. 2個併發的chromedriver不能使用相同的user-data-dir
My solution was to specify option user-data-dir. Two concurrent Chromedriver should not use same user data directory
我的情況是:磁碟肯定有空間; 本機已kill所有chromedriver,chrome程序----仍舊是報錯!
user-data-dir更換到其它目錄是可以消除此錯誤的,但違背了利用相同環境的初衷.
3.有的說不同的版本表現不同:
chromedriver 2.12.301324沒有此問題,但2.15則重現了.
https://stackoverflow.com/questions/35458272/selenium-failed-to-write-prefs-file
Exception "unknown error: failed to write prefs file" is thrown when same user-data-dir and profile is used by two chrome instances in parallel.
This is reproducible in chromedriver:2.15
I am using chromedriver 2.12.301324 and do not have this problem.
網上說chromedriver版本過期,不支援chrome。---我的都是最新版本的,且匹配。
https://stackoverflow.com/questions/21001652/chrome-driver-error-using-selenium-unable-to-discover-open-pages
5.前端程式碼
(1)拖動滑塊響應
驗證需要與伺服器驗證,請求url:
https://cf.aliyun.com/nocaptcha/analyze.jsonp
主要程式碼:
https://g.alicdn.com/sd/ncpc/nc.js?t=2018062922
呼叫堆疊:
(anonymous) (index.js:formatted:533)
jsonp (nc.js?t=2018062922:formatted:1468)
waitForUmx (nc.js?t=2018062922:formatted:4250)
onScaleReady (nc.js?t=2018062922:formatted:4296)
a (nc.js?t=2018062922:formatted:4729)
r (nc.js?t=2018062922:formatted:4743)
jsonp發起呼叫.
返回內容如下:
jsonp_030083264854398717({
"result": {
"csessionid": "01JMJ3tV_LVnWLUTbouHo5IxXzjp5ZZQYVK4OmuIOT6ThqUrKTCoWLhmuVdl5RNc25usTewr2VuQ3GRULoUGbBvSN97axihY17TVZ4ZqHfGtKFI_0nxJpkPm-CPuYkq9aUzkG0yuDQ3-PsoKvbRbSI8BGVZ2mBOoLq1jkJWTPRlUfICsAjhUS6YghmCEuKVS2wCkp4_yY6dw3nhAQyKoRpU7PJCYdFAnBzQz1IvVs5ozA-hRzK9zSC7WIXgWL-bga-Vyleni8r6UQuO3-lOsEzN-1ZHfe45wd8Pg5td_7d-5Oq7jEk4Tr5cVW_tpA5xTvy1b_6vkGFVhIqCduPjV0uS0knk_BgXOiCZ4MaVegvn4mb6xO2lZmK5tDapwFM4jOd",
"value": "block",
"code": 300
},
"success": true
});
其中result.code=300表示錯誤.
onScaleReadyCallback(nc.js中)根據code處理.
(2)錯誤提示資訊
nc.js 第721行:
中文是採用unicode編碼儲存的
部分內容如下:
cn: {
_yesTEXT: "\u9a8c\u8bc1\u901a\u8fc7", --- 驗證通過
_Loading: "\u52a0\u8f7d\u4e2d", ---載入中
_errorServer: "\u670d\u52a1\u5668\u9519\u8bef\u6216\u8005\u8d85\u65f6",
_error300: ["\u54ce\u5440\uff0c\u51fa\u9519\u4e86\uff0c\u70b9\u51fb", d, "\u5237\u65b0", "\u518d\u6765\u4e00\u6b21"],
---哎呀,出錯了,點選
}
tw: en:分別為臺灣繁體,英文
(3)重新整理
debugger:VM1320的內容:
noCaptcha.reset(1)
noCaptcha.reset的程式碼在:
https://g.alicdn.com/sd/ncpc/nc.js?t=2018062812:formatted
r.reset = function(e) {
var t = r.getByIndex(e);
t ? t.reset() : window.outer_nc_list && window.outer_nc_list[e].reset()
}
reset: function() {
this.__nc_afterUM = !1,
win.UA_Opt && (UA_Opt.Token = (new Date).getTime() + ":" + opt.token);
var e;
opt.renderTo && opt.appkey && opt.token && (e = _.id(opt.renderTo),
e && util.addClass(el_render_to, "nc-container"),
e.innerHTML = '<div id="' + nc_prefix + 'nocaptcha"><div id="' + nc_prefix + 'wrapper" class="nc_wrapper"><div id="' + nc_prefix + '_n1t_loading" class="nc_scale"><div id="' + nc_prefix + '_bg" class="nc_bg" style="width: 0;"></div><div id="' + nc_prefix + '_scale_text_loading" class="scale_text">' + language[opt.language]._Loading + loading_circle_html + "</div></div></div></div>",
"undefined" == typeof win.acjs ? this.loaduab() : (UA_Opt.LogVal = "_n",
this.initUaParam(),
UA_Opt.Token = (new Date).getTime() + ":" + opt.token,
UA_Opt.reload && UA_Opt.reload()),
this.afterUA())
},
6.結論
試驗失敗,沒有實現自動登入的預期,無法繞過滑塊驗證.
https://www.zhihu.com/question/35538123
該文提及破解難度,需要解析大量的js程式碼,而且演算法經常改變。
關於淘寶UA演算法分析的資料:
http://livezingy.com/ua_inputid-in-taobao-ua/
http://livezingy.com/simple-understanding-about-taobao-ua/
遺留問題有:
.jsonp呼叫是怎麼發生的? XHR請求,但沒跟蹤到ajax呼叫。
.jsonp請求的資料都有哪些,來源是什麼,有什麼演算法處理
.同一個登入頁面,直接在chrome開啟和chromedriver開啟,效果不一樣,前者有cookies記錄的使用者名稱,密碼資訊,且沒有滑塊;後者則沒有使用者,密碼,有滑塊.是什麼原因?
已沒有新的思路繼續,就此打住,不再浪費精力了.
7.其它HOWTO
- 設定為Headless模式
Options.addArguments("--headless");
- 遮蔽"Chrome is being controlled by automated test software "
ChromeOptions options = new ChromeOptions();
options.addArguments("disable-infobars");
- 使用RemoteWebDriver
ChromeDriverService service = new ChromeDriverService.Builder().usingDriverExecutable(
new File(chromePath+"chromedriver.exe")) .usingAnyFreePort().build();
service.start();
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless");
DesiredCapabilities capabilities = DesiredCapabilities.chrome();
capabilities.setCapability(ChromeOptions.CAPABILITY, options);
WebDriver driver = new RemoteWebDriver(service.getUrl(), capabilities);