使用JNA解決自動化測試無法做密碼輸入操作的問題
/**
* 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,所以會失效。