1. 程式人生 > >Selenuim ChromeDriver自動登入試驗

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);