使用JNA解决自动化测试无法做密码输入操作的问题
https://blog.csdn.net/bolg_hero/article/details/50072859
在做頁面自動化(以使用selenium為例)的時候,很常見的一個場景就是輸入密碼。往往對于輸入框都使用WebElement的sendKeys(CharSequence... keysToSend)的方法。
/**
?? * 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,模擬鍵盤發送消息事件給操作系統,可以避免所有瀏覽器等差異和安全性帶來的問題。
我個人建議使用JNA(https://github.com/twall/jna),JNA是一種和JNI類似的技術,但是相對JNI來說更加易用。 JNA共有jna.jar和platform.jar兩個依賴庫,都需要引入,我們需要用到的在platform.jar中。從包結構可以看出,JNA中包含了mac、unix、win32等各類操作系統的系統API映射。如下圖:
系統API映射關系在JNA的文章中有描述,如下:
數據類型的映射參見:https://github.com/twall/jna/blob/master/www/Mappings.md
本文中以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;?? 4.?? 5.public interface User32Ext extends User32 {?? 6.?? 7.??? User32Ext USER32EXT = (User32Ext) Native.loadLibrary("user32", User32Ext.class, W32APIOptions.DEFAULT_OPTIONS);?? 8.?????? 9.??? /**? 10.???? * 查找窗口? 11.???? * @param lpParent 需要查找窗口的父窗口? 12.???? * @param lpChild 需要查找窗口的子窗口? 13.???? * @param lpClassName 類名? 14.???? * @param lpWindowName 窗口名? 15.???? * @return 找到的窗口的句柄? 16.???? */?? 17.??? HWND FindWindowEx(HWND lpParent, HWND lpChild, String lpClassName, String lpWindowName);?? 18.?? 19.??? /**? 20.???? * 獲取桌面窗口,可以理解為所有窗口的root? 21.???? * @return 獲取的窗口的句柄? 22.???? */?? 23.??? HWND GetDesktopWindow();?? 24.?????? 25.??? /**? 26.???? * 發送事件消息? 27.???? * @param hWnd 控件的句柄? 28.???? * @param dwFlags 事件類型? 29.???? * @param bVk 虛擬按鍵碼? 30.???? * @param dwExtraInfo 擴展信息,傳0即可? 31.???? * @return? 32.???? */?? 33.??? int SendMessage(HWND hWnd, int dwFlags, byte bVk, int dwExtraInfo);?? 34.?? 35.??? /**? 36.???? * 發送事件消息? 37.???? * @param hWnd 控件的句柄? 38.???? * @param Msg 事件類型? 39.???? * @param wParam 傳0即可? 40.???? * @param lParam 需要發送的消息,如果是點擊操作傳null? 41.???? * @return? 42.???? */?? 43.??? int SendMessage(HWND hWnd, int Msg, int wParam, String lParam);?? 44.?????? 45.??? /**? 46.???? * 發送鍵盤事件? 47.???? * @param bVk 虛擬按鍵碼? 48.???? * @param bScan 傳 ((byte)0) 即可? 49.???? * @param dwFlags 鍵盤事件類型? 50.???? * @param dwExtraInfo 傳0即可? 51.???? */?? 52.??? void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);?? 53.?????? 54.??? /**? 55.???? * 激活指定窗口(將鼠標焦點定位于指定窗口)? 56.???? * @param hWnd 需激活的窗口的句柄? 57.???? * @param fAltTab 是否將最小化窗口還原? 58.???? */?? 59.??? void SwitchToThisWindow(HWND hWnd, boolean fAltTab);?? 60.?????? 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;?? 6.?? 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;?? 11.?? 12./**? 13. * Window組件操作工具類? 14. *?? 15. * @author sunju? 16. *?? 17. */?? 18.public class Win32Util {?? 19.?? 20.??? private static final int N_MAX_COUNT = 512;?? 21.?? 22.??? private Win32Util() {?? 23.??? }?? 24.?? 25.??? /**? 26.???? * 從桌面開始查找指定類名的組件,在超時的時間范圍內,如果未找到任何匹配的組件則反復查找? 27.???? * @param className 組件的類名? 28.???? * @param timeout 超時時間? 29.???? * @param unit 超時時間的單位? 30.???? * @return 返回匹配的組件的句柄,如果匹配的組件大于一個,返回第一個查找的到的;如果未找到或超時則返回<code>null</code>? 31.???? */?? 32.??? public static HWND findHandleByClassName(String className, long timeout, TimeUnit unit) {?? 33.??????? return findHandleByClassName(USER32EXT.GetDesktopWindow(), className, timeout, unit);?? 34.??? }?? 35.?? 36.??? /**? 37.???? * 從桌面開始查找指定類名的組件? 38.???? * @param className 組件的類名? 39.???? * @return 返回匹配的組件的句柄,如果匹配的組件大于一個,返回第一個查找的到的;如果未找到任何匹配則返回<code>null</code>? 40.???? */?? 41.??? public static HWND findHandleByClassName(String className) {?? 42.??????? return findHandleByClassName(USER32EXT.GetDesktopWindow(), className);?? 43.??? }?? 44.?? 45.??? /**? 46.???? * 從指定位置開始查找指定類名的組件? 47.???? * @param root 查找組件的起始位置的組件的句柄,如果為<code>null</code>則從桌面開始查找? 48.???? * @param className 組件的類名? 49.???? * @param timeout 超時時間? 50.???? * @param unit 超時時間的單位? 51.???? * @return 返回匹配的組件的句柄,如果匹配的組件大于一個,返回第一個查找的到的;如果未找到或超時則返回<code>null</code>? 52.???? */?? 53.??? public static HWND findHandleByClassName(HWND root, String className, long timeout, TimeUnit unit) {?? 54.??????? if(null == className || className.length() <= 0) {?? 55.??????????? return null;?? 56.??????? }?? 57.??????? long start = System.currentTimeMillis();?? 58.??????? HWND hwnd = findHandleByClassName(root, className);?? 59.??????? while(null == hwnd && (System.currentTimeMillis() - start < unit.toMillis(timeout))) {?? 60.??????????? hwnd = findHandleByClassName(root, className);?? 61.??????? }?? 62.??????? return hwnd;?? 63.??? }?? 64.?? 65.??? /**? 66.???? * 從指定位置開始查找指定類名的組件? 67.???? * @param root 查找組件的起始位置的組件的句柄,如果為<code>null</code>則從桌面開始查找? 68.???? * @param className 組件的類名? 69.???? * @return 返回匹配的組件的句柄,如果匹配的組件大于一個,返回第一個查找的到的;如果未找到任何匹配則返回<code>null</code>? 70.???? */?? 71.??? public static HWND findHandleByClassName(HWND root, String className) {?? 72.??????? if(null == className || className.length() <= 0) {?? 73.??????????? return null;?? 74.??????? }?? 75.??????? HWND[] result = new HWND[1];?? 76.??????? findHandle(result, root, className);?? 77.??????? return result[0];?? 78.??? }?? 79.?? 80.??? private static boolean findHandle(final HWND[] target, HWND root, final String className) {?? 81.??????? if(null == root) {?? 82.??????????? root = USER32EXT.GetDesktopWindow();?? 83.??????? }?? 84.??????? return USER32EXT.EnumChildWindows(root, new WNDENUMPROC() {?? 85.?? 86.??????????? @Override?? 87.??????????? public boolean callback(HWND hwnd, Pointer pointer) {?? 88.??????????????? char[] winClass = new char[N_MAX_COUNT];?? 89.??????????????? USER32EXT.GetClassName(hwnd, winClass, N_MAX_COUNT);?? 90.??????????????? if(USER32EXT.IsWindowVisible(hwnd) && className.equals(Native.toString(winClass))) {?? 91.??????????????????? target[0] = hwnd;?? 92.??????????????????? return false;?? 93.??????????????? } else {?? 94.??????????????????? return target[0] == null || findHandle(target, hwnd, className);?? 95.??????????????? }?? 96.??????????? }?? 97.?? 98.??????? }, Pointer.NULL);?? 99.??? }?? 100.?? 101.??? /**? 102.???? * 模擬鍵盤按鍵事件,異步事件。使用win32 keybd_event,每次發送KEYEVENTF_KEYDOWN、KEYEVENTF_KEYUP兩個事件。默認10秒超時? 103.???? * @param hwnd 被鍵盤操作的組件句柄? 104.???? * @param keyCombination 鍵盤的虛擬按鍵碼(<a href="http://msdn.microsoft.com/ZH-CN/library/windows/desktop/dd375731.aspx">Virtual-Key?Code</a>),或者使用{@link?java.awt.event.KeyEvent}</br>? 105.???? *????????????????????? 二維數組第一維中的一個元素為一次按鍵操作,包含組合操作,第二維中的一個元素為一個按鍵事件,即一個虛擬按鍵碼? 106.???? * @return 鍵盤按鍵事件放入windows消息隊列成功返回<code>true</code>,鍵盤按鍵事件放入windows消息隊列失敗或超時返回<code>false</code>? 107.???? */?? 108.??? public static boolean simulateKeyboardEvent(HWND hwnd, int[][] keyCombination) {?? 109.??????? if(null == hwnd) {?? 110.??????????? return false;?? 111.??????? }?? 112.??????? USER32EXT.SwitchToThisWindow(hwnd, true);?? 113.??????? USER32EXT.SetFocus(hwnd);?? 114.??????? for(int[] keys : keyCombination) {?? 115.??????????? for(int i = 0; i < keys.length; i++) {?? 116.??????????????? USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYDOWN, 0); // key down?? 117.??????????? }?? 118.??????????? for(int i = keys.length - 1; i >= 0; i--) {?? 119.??????????????? USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYUP, 0); // key up?? 120.??????????? }?? 121.??????? }?? 122.??????? return true;?? 123.??? }?? 124.?? 125.??? /**? 126.???? * 模擬字符輸入,同步事件。使用win32 SendMessage API發送WM_CHAR事件。默認10秒超時? 127.???? * @param hwnd 被輸入字符的組件的句柄? 128.???? * @param content 輸入的內容。字符串會被轉換成<code>char[]</code>后逐個字符輸入? 129.???? * @return 字符輸入事件發送成功返回<code>true</code>,字符輸入事件發送失敗或超時返回<code>false</code>? 130.???? */?? 131.??? public static boolean simulateCharInput(final HWND hwnd, final String content) {?? 132.??????? if(null == hwnd) {?? 133.??????????? return false;?? 134.??????? }?? 135.??????? try {?? 136.??????????? return execute(new Callable<Boolean>() {?? 137.?? 138.??????????????? @Override?? 139.??????????????? public Boolean call() throws Exception {?? 140.??????????????????? USER32EXT.SwitchToThisWindow(hwnd, true);?? 141.??????????????????? USER32EXT.SetFocus(hwnd);?? 142.??????????????????? for(char c : content.toCharArray()) {?? 143.??????????????????????? Thread.sleep(5);?? 144.??????????????????????? USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0);?? 145.??????????????????? }?? 146.??????????????????? return true;?? 147.??????????????? }?? 148.?? 149.??????????? });?? 150.??????? } catch(Exception e) {?? 151.??????????? return false;?? 152.??????? }?? 153.??? }?? 154.?????? 155.??? public static boolean simulateCharInput(final HWND hwnd, final String content, final long sleepMillisPreCharInput) {?? 156.??????? if(null == hwnd) {?? 157.??????????? return false;?? 158.??????? }?? 159.??????? try {?? 160.??????????? return execute(new Callable<Boolean>() {?? 161.?? 162.??????????????? @Override?? 163.??????????????? public Boolean call() throws Exception {?? 164.??????????????????? USER32EXT.SwitchToThisWindow(hwnd, true);?? 165.??????????????????? USER32EXT.SetFocus(hwnd);?? 166.??????????????????? for(char c : content.toCharArray()) {?? 167.??????????????????????? Thread.sleep(sleepMillisPreCharInput);?? 168.??????????????????????? USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0);?? 169.??????????????????? }?? 170.??????????????????? return true;?? 171.??????????????? }?? 172.?? 173.??????????? });?? 174.??????? } catch(Exception e) {?? 175.??????????? return false;?? 176.??????? }?? 177.??? }?? 178.?? 179.??? /**? 180.???? * 模擬文本輸入,同步事件。使用win32 SendMessage API發送WM_SETTEXT事件。默認10秒超時? 181.???? * @param hwnd 被輸入文本的組件的句柄? 182.???? * @param content 輸入的文本內容? 183.???? * @return 文本輸入事件發送成功返回<code>true</code>,文本輸入事件發送失敗或超時返回<code>false</code>? 184.???? */?? 185.??? public static boolean simulateTextInput(final HWND hwnd, final String content) {?? 186.??????? if(null == hwnd) {?? 187.??????????? return false;?? 188.??????? }?? 189.??????? try {?? 190.??????????? return execute(new Callable<Boolean>() {?? 191.?? 192.??????????????? @Override?? 193.??????????????? public Boolean call() throws Exception {?? 194.??????????????????? USER32EXT.SwitchToThisWindow(hwnd, true);?? 195.??????????????????? USER32EXT.SetFocus(hwnd);?? 196.??????????????????? USER32EXT.SendMessage(hwnd, WM_SETTEXT, 0, content);?? 197.??????????????????? return true;?? 198.??????????????? }?? 199.?? 200.??????????? });?? 201.??????? } catch(Exception e) {?? 202.??????????? return false;?? 203.??????? }?? 204.??? }?? 205.?? 206.??? /**? 207.???? * 模擬鼠標點擊,同步事件。使用win32 SendMessage API發送BM_CLICK事件。默認10秒超時? 208.???? * @param hwnd 被點擊的組件的句柄? 209.???? * @return 點擊事件發送成功返回<code>true</code>,點擊事件發送失敗或超時返回<code>false</code>? 210.???? */?? 211.??? public static boolean simulateClick(final HWND hwnd) {?? 212.??????? if(null == hwnd) {?? 213.??????????? return false;?? 214.??????? }?? 215.??????? try {?? 216.??????????? return execute(new Callable<Boolean>() {?? 217.?? 218.??????????????? @Override?? 219.??????????????? public Boolean call() throws Exception {?? 220.??????????????????? USER32EXT.SwitchToThisWindow(hwnd, true);?? 221.??????????????????? USER32EXT.SendMessage(hwnd, BM_CLICK, 0, null);?? 222.??????????????????? return true;?? 223.??????????????? }?? 224.?? 225.??????????? });?? 226.??????? } catch(Exception e) {?? 227.??????????? return false;?? 228.??????? }?? 229.??? }?? 230.?? 231.??? private static <T> T execute(Callable<T> callable) throws Exception {?? 232.??????? ExecutorService executor = Executors.newSingleThreadExecutor();?? 233.??????? try {?? 234.??????????? Future<T> task = executor.submit(callable);?? 235.??????????? return task.get(10, TimeUnit.SECONDS);?? 236.??????? } finally {?? 237.??????????? executor.shutdown();?? 238.??????? }?? 239.??? }?? 240.} |
?
其中用到的各種事件類型定義如下:
| 1.public class Win32MessageConstants {?? 2.?? 3.??? public static final int WM_SETTEXT = 0x000C; //輸入文本?? 4.?????? 5.??? public static final int WM_CHAR = 0x0102; //輸入字符?? 6.?? 7.??? public static final int BM_CLICK = 0xF5; //點擊事件,即按下和抬起兩個動作?? 8.?? 9.??? public static final int KEYEVENTF_KEYUP = 0x0002; //鍵盤按鍵抬起?? 10.?????? 11.??? public static final int KEYEVENTF_KEYDOWN = 0x0; //鍵盤按鍵按下?? 12.?? 13.} |
下面寫一段測試代碼來測試支付寶密碼安全控件的輸入,測試代碼如下:
| 1.import java.util.concurrent.TimeUnit;?? 2.?? 3.import static org.hamcrest.core.Is.is;?? 4.import static org.junit.Assert.assertThat;?? 5.?? 6.import static org.hamcrest.core.IsNull.notNullValue;?? 7.import org.junit.Test;?? 8.?? 9.import com.sun.jna.platform.win32.WinDef;?? 10.import com.sun.jna.platform.win32.WinDef.HWND;?? 11.?? 12.public class AlipayPasswordInputTest {?? 13.?? 14.??? @Test?? 15.??? public void testAlipayPasswordInput() {?? 16.??????? String password = "your password";?? 17.??????? HWND alipayEdit = findHandle("Chrome_RenderWidgetHostHWND", "Edit"); //Chrome瀏覽器,使用Spy++可以抓取句柄的參數?? 18.??????? assertThat("獲取支付寶密碼控件失敗。", alipayEdit, notNullValue());?? 19.??????? boolean isSuccess = Win32Util.simulateCharInput(alipayEdit, password);?? 20.??????? assertThat("輸入支付寶密碼["+ password +"]失敗。", isSuccess,? is(true));?? 21.??? }?? 22.?????? 23.??? private WinDef.HWND findHandle(String browserClassName, String alieditClassName) {?? 24.??????? WinDef.HWND browser = Win32Util.findHandleByClassName(browserClassName, 10, TimeUnit.SECONDS);?? 25.??????? return Win32Util.findHandleByClassName(browser, alieditClassName, 10, TimeUnit.SECONDS);?? 26.??? }?? 27.} |
測試一下,看看是不是輸入成功了!
最后說下這個方法的缺陷,任何方法都有不可避免的存在一些問題,完美的事情很少。
1、sendMessage和postMessage有很多重載的函數,不是每種都有效,從上面的Win32Util中就能看出,實現了很多個方法,需要嘗試下,成本略高;
2、輸入時需要注意頻率,輸入太快可能導致瀏覽器中安全控件崩潰,支付寶的安全控件在Firefox下輸入太快就會崩潰;
3、因為是系統API,所以MAC、UNIX、WINDOWS下都不同,如果只是在windows環境下運行,可以忽略;
4、從測試代碼可以看到,是針對Chrome瀏覽器的,因為每種瀏覽器的窗口句柄不同,所以要區分,不過這個相對簡單,只是名稱不同;
5、如果你使用Selenium的RemoteDriver,并且是在遠程機器上運行腳本,這個方法會失效。因為remoteDriver最終是http操作,對操作系統API的操作是客戶端行為,不能被翻譯成Http Command,所以會失效。
轉載于:https://www.cnblogs.com/davidwang456/articles/8662087.html
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的使用JNA解决自动化测试无法做密码输入操作的问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 滑块验证码识别 java版本
- 下一篇: 【机器人系列】支付宝支付控件输入框模拟输