天天看點

使用JNA解決自動化測試無法做密碼輸入操作的問題

在做頁面自動化(以使用selenium為例)的時候,很常見的一個場景就是輸入密碼。往往對于輸入框都使用webelement的sendkeys(charsequence... keystosend)的方法。

  java代碼

1./**

use this method to simulate typing into an element, which may set its value.

*/

void sendkeys(charsequence... keystosend);

  一般情況下這個方法是可以勝任的,但是現在很多網站為了安全性的考慮都會對密碼輸入框做特殊的處理,而且不同的浏覽器也不同。例如支付寶。

  支付寶輸入密碼控件在chrome浏覽器下

  支付寶輸入密碼控件在firefox浏覽器下

  支付寶輸入密碼控件在ie(ie8)浏覽器下

  可見在不同的浏覽器下是有差異的。那麼現在存在兩個問題。首先,selenium的sendkeys方法無法操作這樣特殊的控件;其次,不同浏覽器又存在差異,搞定了chrome,在ie下又不能用,這樣又要解決浏覽器相容性問題。

如何解決這兩個問題呢?

  我們可以發現平時人工使用鍵盤輸入密碼的時候是沒有這些問題的,那麼我們是否可以模拟人工操作時的鍵盤輸入方式呢?答案是肯定的,使用作業系統的api,模拟鍵盤發送消息事件給作業系統,可以避免所有浏覽器等差異和安全性帶來的問題。

  系統api映射關系在jna的文章中有描述,如下:

  本文中以windows為例示範下如何在支付寶的密碼安全控件中輸入密碼。

  jna中關于windows平台的是com.sun.jna.platform.win32包中user32這個接口。這裡映射了很多windows系統api可以使用。但是我們需要用到的sendmessage卻沒有。是以需要建立一個接口,映射sendmessage函數。代碼如下:

1.import com.sun.jna.native;

2.import com.sun.jna.platform.win32.user32;

3.import com.sun.jna.win32.w32apioptions;

5.public interface user32ext extends user32 {

user32ext user32ext = (user32ext) native.loadlibrary("user32", user32ext.class, w32apioptions.default_options);

/**

查找視窗

@param lpparent 需要查找視窗的父視窗

@param lpchild 需要查找視窗的子視窗

@param lpclassname 類名

@param lpwindowname 視窗名

@return 找到的視窗的句柄

hwnd findwindowex(hwnd lpparent, hwnd lpchild, string lpclassname, string lpwindowname);

擷取桌面視窗,可以了解為所有視窗的root

@return 擷取的視窗的句柄

hwnd getdesktopwindow();

發送事件消息

@param hwnd 控件的句柄

@param dwflags 事件類型

@param bvk 虛拟按鍵碼

@param dwextrainfo 擴充資訊,傳0即可

@return

int sendmessage(hwnd hwnd, int dwflags, byte bvk, int dwextrainfo);

@param msg 事件類型

@param wparam 傳0即可

@param lparam 需要發送的消息,如果是點選操作傳null

int sendmessage(hwnd hwnd, int msg, int wparam, string lparam);

發送鍵盤事件

@param bscan 傳 ((byte)0) 即可

@param dwflags 鍵盤事件類型

@param dwextrainfo 傳0即可

void keybd_event(byte bvk, byte bscan, int dwflags, int dwextrainfo);

激活指定視窗(将滑鼠焦點定位于指定視窗)

@param hwnd 需激活的視窗的句柄

@param falttab 是否将最小化視窗還原

void switchtothiswindow(hwnd hwnd, boolean falttab);

61.}

 系統api映射好以後,利用這個接口寫了如下的工具類,包含點選和輸入各種操作。代碼如下:

1.import java.util.concurrent.callable;

2.import java.util.concurrent.executorservice;

3.import java.util.concurrent.executors;

4.import java.util.concurrent.future;

5.import java.util.concurrent.timeunit;

7.import com.sun.jna.native;

8.import com.sun.jna.pointer;

9.import com.sun.jna.platform.win32.windef.hwnd;

10.import com.sun.jna.platform.win32.winuser.wndenumproc;

12./**

window元件操作工具類

@author sunju

18.public class win32util {

private static final int n_max_count = 512;

private win32util() {

}

從桌面開始查找指定類名的元件,在逾時的時間範圍内,如果未找到任何比對的元件則反複查找

@param classname 元件的類名

@param timeout 逾時時間

@param unit 逾時時間的機關

@return 傳回比對的元件的句柄,如果比對的元件大于一個,傳回第一個查找的到的;如果未找到或逾時則傳回<code>null</code>

public static hwnd findhandlebyclassname(string classname, long timeout, timeunit unit) {

return findhandlebyclassname(user32ext.getdesktopwindow(), classname, timeout, unit);

從桌面開始查找指定類名的元件

@return 傳回比對的元件的句柄,如果比對的元件大于一個,傳回第一個查找的到的;如果未找到任何比對則傳回<code>null</code>

public static hwnd findhandlebyclassname(string classname) {

return findhandlebyclassname(user32ext.getdesktopwindow(), classname);

從指定位置開始查找指定類名的元件

@param root 查找元件的起始位置的元件的句柄,如果為<code>null</code>則從桌面開始查找

public static hwnd findhandlebyclassname(hwnd root, string classname, long timeout, timeunit unit) {

if(null == classname || classname.length() &lt;= 0) {

return null;

long start = system.currenttimemillis();

hwnd hwnd = findhandlebyclassname(root, classname);

while(null == hwnd &amp;&amp; (system.currenttimemillis() - start &lt; unit.tomillis(timeout))) {

hwnd = findhandlebyclassname(root, classname);

return hwnd;

public static hwnd findhandlebyclassname(hwnd root, string classname) {

hwnd[] result = new hwnd[1];

findhandle(result, root, classname);

return result[0];

private static boolean findhandle(final hwnd[] target, hwnd root, final string classname) {

if(null == root) {

root = user32ext.getdesktopwindow();

return user32ext.enumchildwindows(root, new wndenumproc() {

@override

public boolean callback(hwnd hwnd, pointer pointer) {

char[] winclass = new char[n_max_count];

user32ext.getclassname(hwnd, winclass, n_max_count);

if(user32ext.iswindowvisible(hwnd) &amp;&amp; classname.equals(native.tostring(winclass))) {

target[0] = hwnd;

return false;

} else {

return target[0] == null || findhandle(target, hwnd, classname);

}, pointer.null);

模拟鍵盤按鍵事件,異步事件。使用win32 keybd_event,每次發送keyeventf_keydown、keyeventf_keyup兩個事件。預設10秒逾時

@param hwnd 被鍵盤操作的元件句柄

二維數組第一維中的一個元素為一次按鍵操作,包含組合操作,第二維中的一個元素為一個按鍵事件,即一個虛拟按鍵碼

@return 鍵盤按鍵事件放入windows消息隊列成功傳回<code>true</code>,鍵盤按鍵事件放入windows消息隊列失敗或逾時傳回<code>false</code>

public static boolean simulatekeyboardevent(hwnd hwnd, int[][] keycombination) {

if(null == hwnd) {

user32ext.switchtothiswindow(hwnd, true);

user32ext.setfocus(hwnd);

for(int[] keys : keycombination) {

for(int i = 0; i &lt; keys.length; i++) {

user32ext.keybd_event((byte) keys[i], (byte) 0, keyeventf_keydown, 0); // key down

for(int i = keys.length - 1; i &gt;= 0; i--) {

user32ext.keybd_event((byte) keys[i], (byte) 0, keyeventf_keyup, 0); // key up

return true;

模拟字元輸入,同步事件。使用win32 sendmessage api發送wm_char事件。預設10秒逾時

@param hwnd 被輸入字元的元件的句柄

@param content 輸入的内容。字元串會被轉換成<code>char[]</code>後逐個字元輸入

@return 字元輸入事件發送成功傳回<code>true</code>,字元輸入事件發送失敗或逾時傳回<code>false</code>

public static boolean simulatecharinput(final hwnd hwnd, final string content) {

try {

return execute(new callable() {

public boolean call() throws exception {

for(char c : content.tochararray()) {

thread.sleep(5);

user32ext.sendmessage(hwnd, wm_char, (byte) c, 0);

});

} catch(exception e) {

public static boolean simulatecharinput(final hwnd hwnd, final string content, final long sleepmillisprecharinput) {

thread.sleep(sleepmillisprecharinput);

模拟文本輸入,同步事件。使用win32 sendmessage api發送wm_settext事件。預設10秒逾時

@param hwnd 被輸入文本的元件的句柄

@param content 輸入的文本内容

@return 文本輸入事件發送成功傳回<code>true</code>,文本輸入事件發送失敗或逾時傳回<code>false</code>

public static boolean simulatetextinput(final hwnd hwnd, final string content) {

user32ext.sendmessage(hwnd, wm_settext, 0, content);

模拟滑鼠點選,同步事件。使用win32 sendmessage api發送bm_click事件。預設10秒逾時

@param hwnd 被點選的元件的句柄

@return 點選事件發送成功傳回<code>true</code>,點選事件發送失敗或逾時傳回<code>false</code>

public static boolean simulateclick(final hwnd hwnd) {

user32ext.sendmessage(hwnd, bm_click, 0, null);

private static t execute(callable callable) throws exception {

executorservice executor = executors.newsinglethreadexecutor();

future task = executor.submit(callable);

return task.get(10, timeunit.seconds);

} finally {

executor.shutdown();

240.}

其中用到的各種事件類型定義如下:

1.public class win32messageconstants {

public static final int wm_settext = 0x000c; //輸入文本

public static final int wm_char = 0x0102; //輸入字元

public static final int bm_click = 0xf5; //點選事件,即按下和擡起兩個動作

public static final int keyeventf_keyup = 0x0002; //鍵盤按鍵擡起

public static final int keyeventf_keydown = 0x0; //鍵盤按鍵按下

13.}

  下面寫一段測試代碼來測試支付寶密碼安全控件的輸入,測試代碼如下:

1.import java.util.concurrent.timeunit;

3.import static org.hamcrest.core.is.is;

4.import static org.junit.assert.assertthat;

6.import static org.hamcrest.core.isnull.notnullvalue;

7.import org.junit.test;

9.import com.sun.jna.platform.win32.windef;

10.import com.sun.jna.platform.win32.windef.hwnd;

12.public class alipaypasswordinputtest {

@test

public void testalipaypasswordinput() {

string password = "your password";

hwnd alipayedit = findhandle("chrome_renderwidgethosthwnd", "edit"); //chrome浏覽器,使用spy++可以抓取句柄的參數

assertthat("擷取支付寶密碼控件失敗。", alipayedit, notnullvalue());

boolean issuccess = win32util.simulatecharinput(alipayedit, password);

assertthat("輸入支付寶密碼["+ password +"]失敗。", issuccess, is(true));

private windef.hwnd findhandle(string browserclassname, string alieditclassname) {

windef.hwnd browser = win32util.findhandlebyclassname(browserclassname, 10, timeunit.seconds);

return win32util.findhandlebyclassname(browser, alieditclassname, 10, timeunit.seconds);

27.}

  測試一下,看看是不是輸入成功了!

  最後說下這個方法的缺陷,任何方法都有不可避免的存在一些問題,完美的事情很少。

  1、sendmessage和postmessage有很多重載的函數,不是每種都有效,從上面的win32util中就能看出,實作了很多個方法,需要嘗試下,成本略高;

  2、輸入時需要注意頻率,輸入太快可能導緻浏覽器中安全控件崩潰,支付寶的安全控件在firefox下輸入太快就會崩潰;

  3、因為是系統api,是以mac、unix、windows下都不同,如果隻是在windows環境下運作,可以忽略;

  4、從測試代碼可以看到,是針對chrome浏覽器的,因為每種浏覽器的視窗句柄不同,是以要區分,不過這個相對簡單,隻是名稱不同;

  5、如果你使用selenium的remotedriver,并且是在遠端機器上運作腳本,這個方法會失效。因為remotedriver最終是http操作,對作業系統api的操作是用戶端行為,不能被翻譯成http command,是以會失效。

繼續閱讀