加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

使用JNA解决Selenium无法做密码输入操作的问题

(2014-03-07 18:16:28)
标签:

杂谈

分类: 信息技术
 

JNAselenium自动化

在做页面自动化(以使用selenium为例)的时候,很常见的一个场景就是输入密码。往往对于输入框都使用WebElement的sendKeys(CharSequence... keysToSend)的方法。

Java代码 http://liwx2000.iteye.com/images/icon_star.png

  1. void sendKeys(CharSequence... keysToSend); 

一般情况下这个方法是可以胜任的,但是现在很多网站为了安全性的考虑都会对密码输入框做特殊的处理,而且不同的浏览器也不同。例如支付宝。
http://dl.iteye.com/upload/attachment/0082/5996/d0b42366-1e61-3a34-9037-bd35c2476fe2.jpg
支付宝输入密码控件在Chrome浏览器下
http://dl.iteye.com/upload/attachment/0082/5999/0715c3c9-15e7-392e-9227-381f24a16c49.jpg
支付宝输入密码控件在Firefox浏览器下
http://dl.iteye.com/upload/attachment/0082/6004/a551804a-9a50-3c32-80af-874f79c0815e.jpg
支付宝输入密码控件在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映射。如下图:
http://dl.iteye.com/upload/attachment/0082/6085/91b1f2be-fd43-3054-8f4f-2a99b02692f8.jpg
系统API映射关系在JNA的文章中有描述,如下:
http://dl.iteye.com/upload/attachment/0082/6091/80c42fe4-9e77-3f43-a8db-bda14c74a1af.jpg
数据类型的映射参见:https://github.com/twall/jna/blob/master/www/Mappings.md
本文中以windows为例演示下如何在支付宝的密码安全控件中输入密码。
JNA中关于windows平台的是com.sun.jna.platform.win32包中User32这个接口。这里映射了很多windows系统API可以使用。但是我们需要用到的SendMessage却没有。所以需要新建一个接口,映射SendMessage函数。代码如下:

Java代码 http://liwx2000.iteye.com/images/icon_star.png

  1. import com.sun.jna.Native; 
  2. import com.sun.jna.platform.win32.User32; 
  3. import com.sun.jna.win32.W32APIOptions; 
  4. public interface User32Ext extends User32 { 
  5.     User32Ext USER32EXT = (User32Ext) Native.loadLibrary("user32", User32Ext.class, W32APIOptions.DEFAULT_OPTIONS); 
  6.     HWND FindWindowEx(HWND lpParent, HWND lpChild, String lpClassName, String lpWindowName); 
  7.     HWND GetDesktopWindow(); 
  8. int SendMessage(HWND hWnd, int dwFlags, byte bVk, int dwExtraInfo); 
  9. int SendMessage(HWND hWnd, int Msg, int wParam, String lParam); 
  10. void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo); 
  11. void SwitchToThisWindow(HWND hWnd, boolean fAltTab); 

系统API映射好以后,利用这个接口写了如下的工具类,包含点击和输入各种操作。代码如下:

Java代码 http://liwx2000.iteye.com/images/icon_star.png

  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. import com.sun.jna.Native; 
  7. import com.sun.jna.Pointer; 
  8. import com.sun.jna.platform.win32.WinDef.HWND; 
  9. import com.sun.jna.platform.win32.WinUser.WNDENUMPROC; 
  10. public class Win32Util { 
  11. private static final int N_MAX_COUNT = 512; 
  12. private Win32Util() { 
  13.     } 
  14. public static HWND findHandleByClassName(String className, long timeout, TimeUnit unit) { 
  15. return findHandleByClassName(USER32EXT.GetDesktopWindow(), className, timeout, unit); 
  16.     } 
  17. public static HWND findHandleByClassName(String className) { 
  18. return findHandleByClassName(USER32EXT.GetDesktopWindow(), className); 
  19.     } 
  20. public static HWND findHandleByClassName(HWND root, String className, long timeout, TimeUnit unit) { 
  21. if(null == className || className.length() <= 0) { 
  22. return null; 
  23.         } 
  24. long start = System.currentTimeMillis(); 
  25.         HWND hwnd = findHandleByClassName(root, className); 
  26. while(null == hwnd && (System.currentTimeMillis() - start < unit.toMillis(timeout))) { 
  27.             hwnd = findHandleByClassName(root, className); 
  28.         } 
  29. return hwnd; 
  30.     } 
  31. public static HWND findHandleByClassName(HWND root, String className) { 
  32. if(null == className || className.length() <= 0) { 
  33. return null; 
  34.         } 
  35.         HWND[] result = new HWND[1]; 
  36.         findHandle(result, root, className); 
  37. return result[0]; 
  38.     } 
  39. private static boolean findHandle(final HWND[] target, HWND root, final String className) { 
  40. if(null == root) { 
  41.             root = USER32EXT.GetDesktopWindow(); 
  42.         } 
  43. return USER32EXT.EnumChildWindows(root, new WNDENUMPROC() { 
  44. @Override
  45. public boolean callback(HWND hwnd, Pointer pointer) { 
  46. char[] winClass = new char[N_MAX_COUNT]; 
  47.                 USER32EXT.GetClassName(hwnd, winClass, N_MAX_COUNT); 
  48. if(USER32EXT.IsWindowVisible(hwnd) && className.equals(Native.toString(winClass))) { 
  49.                     target[0] = hwnd; 
  50. return false; 
  51.                 } else { 
  52. return target[0] == null || findHandle(target, hwnd, className); 
  53.                 } 
  54.             } 
  55.         }, Pointer.NULL); 
  56.     } 
  57. public static boolean simulateKeyboardEvent(HWND hwnd, int[][] keyCombination) { 
  58. if(null == hwnd) { 
  59. return false; 
  60.         } 
  61.         USER32EXT.SwitchToThisWindow(hwnd, true); 
  62.         USER32EXT.SetFocus(hwnd); 
  63. for(int[] keys : keyCombination) { 
  64. for(int i = 0; i < keys.length; i++) { 
  65.                 USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYDOWN, 0); // key down
  66.             } 
  67. for(int i = keys.length - 1; i >= 0; i--) { 
  68.                 USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYUP, 0); // key up
  69.             } 
  70.         } 
  71. return true; 
  72.     } 
  73. public static boolean simulateCharInput(final HWND hwnd, final String content) { 
  74. if(null == hwnd) { 
  75. return false; 
  76.         } 
  77. try { 
  78. return execute(new Callable<Boolean>() { 
  79. @Override
  80. public Boolean call() throws Exception { 
  81.                     USER32EXT.SwitchToThisWindow(hwnd, true); 
  82.                     USER32EXT.SetFocus(hwnd); 
  83. for(char c : content.toCharArray()) { 
  84.                         Thread.sleep(5); 
  85.                         USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0); 
  86.                     } 
  87. return true; 
  88.                 } 
  89.             }); 
  90.         } catch(Exception e) { 
  91. return false; 
  92.         } 
  93.     } 
  94. public static boolean simulateCharInput(final HWND hwnd, final String content, final long sleepMillisPreCharInput) { 
  95. if(null == hwnd) { 
  96. return false; 
  97.         } 
  98. try { 
  99. return execute(new Callable<Boolean>() { 
  100. @Override
  101. public Boolean call() throws Exception { 
  102.                     USER32EXT.SwitchToThisWindow(hwnd, true); 
  103.                     USER32EXT.SetFocus(hwnd); 
  104. for(char c : content.toCharArray()) { 
  105.                         Thread.sleep(sleepMillisPreCharInput); 
  106.                         USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0); 
  107.                     } 
  108. return true; 
  109.                 } 
  110.             }); 
  111.         } catch(Exception e) { 
  112. return false; 
  113.         } 
  114.     } 
  115. public static boolean simulateTextInput(final HWND hwnd, final String content) { 
  116. if(null == hwnd) { 
  117. return false; 
  118.         } 
  119. try { 
  120. return execute(new Callable<Boolean>() { 
  121. @Override
  122. public Boolean call() throws Exception { 
  123.                     USER32EXT.SwitchToThisWindow(hwnd, true); 
  124.                     USER32EXT.SetFocus(hwnd); 
  125.                     USER32EXT.SendMessage(hwnd, WM_SETTEXT, 0, content); 
  126. return true; 
  127.                 } 
  128.             }); 
  129.         } catch(Exception e) { 
  130. return false; 
  131.         } 
  132.     } 
  133. public static boolean simulateClick(final HWND hwnd) { 
  134. if(null == hwnd) { 
  135. return false; 
  136.         } 
  137. try { 
  138. return execute(new Callable<Boolean>() { 
  139. @Override
  140. public Boolean call() throws Exception { 
  141.                     USER32EXT.SwitchToThisWindow(hwnd, true); 
  142.                     USER32EXT.SendMessage(hwnd, BM_CLICK, 0, null); 
  143. return true; 
  144.                 } 
  145.             }); 
  146.         } catch(Exception e) { 
  147. return false; 
  148.         } 
  149.     } 
  150. private static <T> T execute(Callable<T> callable) throws Exception { 
  151.         ExecutorService executor = Executors.newSingleThreadExecutor(); 
  152. try { 
  153.             Future<T> task = executor.submit(callable); 
  154. return task.get(10, TimeUnit.SECONDS); 
  155.         } finally { 
  156.             executor.shutdown(); 
  157.         } 
  158.     } 

其中用到的各种事件类型定义如下:

Java代码 http://liwx2000.iteye.com/images/icon_star.png

  1. public class Win32MessageConstants { 
  2. public static final int WM_SETTEXT = 0x000C; //输入文本
  3. public static final int WM_CHAR = 0x0102; //输入字符
  4. public static final int BM_CLICK = 0xF5; //点击事件,即按下和抬起两个动作
  5. public static final int KEYEVENTF_KEYUP = 0x0002; //键盘按键抬起
  6. public static final int KEYEVENTF_KEYDOWN = 0x0; //键盘按键按下

下面写一段测试代码来测试支付宝密码安全控件的输入,测试代码如下:

Java代码 http://liwx2000.iteye.com/images/icon_star.png

  1. import java.util.concurrent.TimeUnit; 
  2. import static org.hamcrest.core.Is.is; 
  3. import static org.junit.Assert.assertThat; 
  4. import static org.hamcrest.core.IsNull.notNullValue; 
  5. import org.junit.Test; 
  6. import com.sun.jna.platform.win32.WinDef; 
  7. import com.sun.jna.platform.win32.WinDef.HWND; 
  8. public class AlipayPasswordInputTest { 
  9. @Test
  10. public void testAlipayPasswordInput() { 
  11.         String password = "your password"; 
  12.         HWND alipayEdit = findHandle("Chrome_RenderWidgetHostHWND", "Edit"); //Chrome浏览器,使用Spy++可以抓取句柄的参数
  13.         assertThat("获取支付宝密码控件失败。", alipayEdit, notNullValue()); 
  14. boolean isSuccess = Win32Util.simulateCharInput(alipayEdit, password); 
  15.         assertThat("输入支付宝密码["+ password +"]失败。", isSuccess,  is(true)); 
  16.     } 
  17. private WinDef.HWND findHandle(String browserClassName, String alieditClassName) { 
  18.         WinDef.HWND browser = Win32Util.findHandleByClassName(browserClassName, 10, TimeUnit.SECONDS); 
  19. return Win32Util.findHandleByClassName(browser, alieditClassName, 10, TimeUnit.SECONDS); 
  20.     } 

测试一下,看看是不是输入成功了!
最后说下这个方法的缺陷,任何方法都有不可避免的存在一些问题,完美的事情很少。
1.sendMessage和postMessage有很多重载的函数,不是每种都有效,从上面的Win32Util中就能看出,实现了很多个方法,需要尝试下,成本略高;
2.输入时需要注意频率,输入太快可能导致浏览器中安全控件崩溃,支付宝的安全控件在Firefox下输入太快就会崩溃;
3.因为是系统API,所以MAC、UNIX、WINDOWS下都不同,如果只是在windows环境下运行,可以忽略;
4.从测试代码可以看到,是针对Chrome浏览器的,因为每种浏览器的窗口句柄不同,所以要区分,不过这个相对简单,只是名称不同;
5.如果你使用Selenium的RemoteDriver,并且是在远程机器上运行脚本,这个方法会失效。因为remoteDriver最终是http操作,对操作系统API的操作是客户端行为,不能被翻译成Http Command,所以会失效。

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有