开发者社区> 玄学酱> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

使用JNA解决自动化测试无法做密码输入操作的问题

简介:
+关注继续查看

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

  Java代码

1./**

    • Use this method to simulate typing into an element, which may set its value.
  1. */
  2. 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;

  1. 5.public interface User32Ext extends User32 {
  2. User32Ext USER32EXT = (User32Ext) Native.loadLibrary("user32", User32Ext.class, W32APIOptions.DEFAULT_OPTIONS);
  3. /**
    • 查找窗口
    • @param lpParent 需要查找窗口的父窗口
    • @param lpChild 需要查找窗口的子窗口
    • @param lpClassName 类名
    • @param lpWindowName 窗口名
    • @return 找到的窗口的句柄
  4. */
  5. HWND FindWindowEx(HWND lpParent, HWND lpChild, String lpClassName, String lpWindowName);
  6. /**
    • 获取桌面窗口,可以理解为所有窗口的root
    • @return 获取的窗口的句柄
  7. */
  8. HWND GetDesktopWindow();
  9. /**
    • 发送事件消息
    • @param hWnd 控件的句柄
    • @param dwFlags 事件类型
    • @param bVk 虚拟按键码
    • @param dwExtraInfo 扩展信息,传0即可
    • @return
  10. */
  11. int SendMessage(HWND hWnd, int dwFlags, byte bVk, int dwExtraInfo);
  12. /**
    • 发送事件消息
    • @param hWnd 控件的句柄
    • @param Msg 事件类型
    • @param wParam 传0即可
    • @param lParam 需要发送的消息,如果是点击操作传null
    • @return
  13. */
  14. int SendMessage(HWND hWnd, int Msg, int wParam, String lParam);
  15. /**
    • 发送键盘事件
    • @param bVk 虚拟按键码
    • @param bScan 传 ((byte)0) 即可
    • @param dwFlags 键盘事件类型
    • @param dwExtraInfo 传0即可
  16. */
  17. void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);
  18. /**
    • 激活指定窗口(将鼠标焦点定位于指定窗口)
    • @param hWnd 需激活的窗口的句柄
    • @param fAltTab 是否将最小化窗口还原
  19. */
  20. void SwitchToThisWindow(HWND hWnd, boolean fAltTab);
  21. 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;

  1. 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;

  1. 12./**
    • Window组件操作工具类
    • @author sunju
  2. */
    18.public class Win32Util {
  3. private static final int N_MAX_COUNT = 512;
  4. private Win32Util() {
  5. }
  6. /**
    • 从桌面开始查找指定类名的组件,在超时的时间范围内,如果未找到任何匹配的组件则反复查找
    • @param className 组件的类名
    • @param timeout 超时时间
    • @param unit 超时时间的单位
    • @return 返回匹配的组件的句柄,如果匹配的组件大于一个,返回第一个查找的到的;如果未找到或超时则返回null
  7. */
  8. public static HWND findHandleByClassName(String className, long timeout, TimeUnit unit) {
  9. return findHandleByClassName(USER32EXT.GetDesktopWindow(), className, timeout, unit);
  10. }
  11. /**
    • 从桌面开始查找指定类名的组件
    • @param className 组件的类名
    • @return 返回匹配的组件的句柄,如果匹配的组件大于一个,返回第一个查找的到的;如果未找到任何匹配则返回null
  12. */
  13. public static HWND findHandleByClassName(String className) {
  14. return findHandleByClassName(USER32EXT.GetDesktopWindow(), className);
  15. }
  16. /**
    • 从指定位置开始查找指定类名的组件
    • @param root 查找组件的起始位置的组件的句柄,如果为null则从桌面开始查找
    • @param className 组件的类名
    • @param timeout 超时时间
    • @param unit 超时时间的单位
    • @return 返回匹配的组件的句柄,如果匹配的组件大于一个,返回第一个查找的到的;如果未找到或超时则返回null
  17. */
  18. public static HWND findHandleByClassName(HWND root, String className, long timeout, TimeUnit unit) {
  19. if(null == className || className.length() <= 0) {
  20. return null;
  21. }
  22. long start = System.currentTimeMillis();
  23. HWND hwnd = findHandleByClassName(root, className);
  24. while(null == hwnd && (System.currentTimeMillis() - start < unit.toMillis(timeout))) {
  25. hwnd = findHandleByClassName(root, className);
  26. }
  27. return hwnd;
  28. }
  29. /**
    • 从指定位置开始查找指定类名的组件
    • @param root 查找组件的起始位置的组件的句柄,如果为null则从桌面开始查找
    • @param className 组件的类名
    • @return 返回匹配的组件的句柄,如果匹配的组件大于一个,返回第一个查找的到的;如果未找到任何匹配则返回null
  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. /**
    • 模拟键盘按键事件,异步事件。使用win32 keybd_event,每次发送KEYEVENTF_KEYDOWN、KEYEVENTF_KEYUP两个事件。默认10秒超时
    • @param hwnd 被键盘操作的组件句柄
    • @param keyCombination 键盘的虚拟按键码(Virtual-Key Code),或者使用{@link java.awt.event.KeyEvent}
    • 二维数组第一维中的一个元素为一次按键操作,包含组合操作,第二维中的一个元素为一个按键事件,即一个虚拟按键码
    • @return 键盘按键事件放入windows消息队列成功返回true,键盘按键事件放入windows消息队列失败或超时返回false
  58. */
  59. public static boolean simulateKeyboardEvent(HWND hwnd, int[][] keyCombination) {
  60. if(null == hwnd) {
  61. return false;
  62. }
  63. USER32EXT.SwitchToThisWindow(hwnd, true);
  64. USER32EXT.SetFocus(hwnd);
  65. for(int[] keys : keyCombination) {
  66. for(int i = 0; i < keys.length; i++) {
  67. USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYDOWN, 0); // key down
  68. }
  69. for(int i = keys.length - 1; i >= 0; i--) {
  70. USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYUP, 0); // key up
  71. }
  72. }
  73. return true;
  74. }
  75. /**
    • 模拟字符输入,同步事件。使用win32 SendMessage API发送WM_CHAR事件。默认10秒超时
    • @param hwnd 被输入字符的组件的句柄
    • @param content 输入的内容。字符串会被转换成char[]后逐个字符输入
    • @return 字符输入事件发送成功返回true,字符输入事件发送失败或超时返回false
  76. */
  77. public static boolean simulateCharInput(final HWND hwnd, final String content) {
  78. if(null == hwnd) {
  79. return false;
  80. }
  81. try {
  82. return execute(new Callable() {
  83. @Override
  84. public Boolean call() throws Exception {
  85. USER32EXT.SwitchToThisWindow(hwnd, true);
  86. USER32EXT.SetFocus(hwnd);
  87. for(char c : content.toCharArray()) {
  88. Thread.sleep(5);
  89. USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0);
  90. }
  91. return true;
  92. }
  93. });
  94. } catch(Exception e) {
  95. return false;
  96. }
  97. }
  98. public static boolean simulateCharInput(final HWND hwnd, final String content, final long sleepMillisPreCharInput) {
  99. if(null == hwnd) {
  100. return false;
  101. }
  102. try {
  103. return execute(new Callable() {
  104. @Override
  105. public Boolean call() throws Exception {
  106. USER32EXT.SwitchToThisWindow(hwnd, true);
  107. USER32EXT.SetFocus(hwnd);
  108. for(char c : content.toCharArray()) {
  109. Thread.sleep(sleepMillisPreCharInput);
  110. USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0);
  111. }
  112. return true;
  113. }
  114. });
  115. } catch(Exception e) {
  116. return false;
  117. }
  118. }
  119. /**
    • 模拟文本输入,同步事件。使用win32 SendMessage API发送WM_SETTEXT事件。默认10秒超时
    • @param hwnd 被输入文本的组件的句柄
    • @param content 输入的文本内容
    • @return 文本输入事件发送成功返回true,文本输入事件发送失败或超时返回false
  120. */
  121. public static boolean simulateTextInput(final HWND hwnd, final String content) {
  122. if(null == hwnd) {
  123. return false;
  124. }
  125. try {
  126. return execute(new Callable() {
  127. @Override
  128. public Boolean call() throws Exception {
  129. USER32EXT.SwitchToThisWindow(hwnd, true);
  130. USER32EXT.SetFocus(hwnd);
  131. USER32EXT.SendMessage(hwnd, WM_SETTEXT, 0, content);
  132. return true;
  133. }
  134. });
  135. } catch(Exception e) {
  136. return false;
  137. }
  138. }
  139. /**
    • 模拟鼠标点击,同步事件。使用win32 SendMessage API发送BM_CLICK事件。默认10秒超时
    • @param hwnd 被点击的组件的句柄
    • @return 点击事件发送成功返回true,点击事件发送失败或超时返回false
  140. */
  141. public static boolean simulateClick(final HWND hwnd) {
  142. if(null == hwnd) {
  143. return false;
  144. }
  145. try {
  146. return execute(new Callable() {
  147. @Override
  148. public Boolean call() throws Exception {
  149. USER32EXT.SwitchToThisWindow(hwnd, true);
  150. USER32EXT.SendMessage(hwnd, BM_CLICK, 0, null);
  151. return true;
  152. }
  153. });
  154. } catch(Exception e) {
  155. return false;
  156. }
  157. }
  158. private static T execute(Callable callable) throws Exception {
  159. ExecutorService executor = Executors.newSingleThreadExecutor();
  160. try {
  161. Future task = executor.submit(callable);
  162. return task.get(10, TimeUnit.SECONDS);
  163. } finally {
  164. executor.shutdown();
  165. }
  166. }
    240.}

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

1.public class Win32MessageConstants {

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

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

1.import java.util.concurrent.TimeUnit;

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

4.import static org.junit.Assert.assertThat;

  1. 6.import static org.hamcrest.core.IsNull.notNullValue;

7.import org.junit.Test;

  1. 9.import com.sun.jna.platform.win32.WinDef;

10.import com.sun.jna.platform.win32.WinDef.HWND;

  1. 12.public class AlipayPasswordInputTest {
  2. @Test
  3. public void testAlipayPasswordInput() {
  4. String password = "your password";
  5. HWND alipayEdit = findHandle("Chrome_RenderWidgetHostHWND", "Edit"); //Chrome浏览器,使用Spy++可以抓取句柄的参数
  6. assertThat("获取支付宝密码控件失败。", alipayEdit, notNullValue());
  7. boolean isSuccess = Win32Util.simulateCharInput(alipayEdit, password);
  8. assertThat("输入支付宝密码["+ password +"]失败。", isSuccess, is(true));
  9. }
  10. private WinDef.HWND findHandle(String browserClassName, String alieditClassName) {
  11. WinDef.HWND browser = Win32Util.findHandleByClassName(browserClassName, 10, TimeUnit.SECONDS);
  12. return Win32Util.findHandleByClassName(browser, alieditClassName, 10, TimeUnit.SECONDS);
  13. }
    27.}

  测试一下,看看是不是输入成功了!

  最后说下这个方法的缺陷,任何方法都有不可避免的存在一些问题,完美的事情很少。

  1、sendMessage和postMessage有很多重载的函数,不是每种都有效,从上面的Win32Util中就能看出,实现了很多个方法,需要尝试下,成本略高;

  2、输入时需要注意频率,输入太快可能导致浏览器中安全控件崩溃,支付宝的安全控件在Firefox下输入太快就会崩溃;

  3、因为是系统API,所以MAC、UNIX、WINDOWS下都不同,如果只是在windows环境下运行,可以忽略;

  4、从测试代码可以看到,是针对Chrome浏览器的,因为每种浏览器的窗口句柄不同,所以要区分,不过这个相对简单,只是名称不同;

  5、如果你使用Selenium的RemoteDriver,并且是在远程机器上运行脚本,这个方法会失效。因为remoteDriver最终是http操作,对操作系统API的操作是客户端行为,不能被翻译成Http Command,所以会失效。

最新内容请见作者的GitHub页:http://qaseven.github.io/

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
测试利器 | 一款开源的Diffy自动化测试框架:超详细实战教程讲解
测试利器 | 一款开源的Diffy自动化测试框架:超详细实战教程讲解
73 0
JSON 反序列化 Long 变 Integer 或 Double 问题
工作中可能会遇到对 Map<String,Object> 进行 JSON 序列化,其中值中包含 Long 类型的数据,反序列化后强转 Long 时报类型转换异常的问题。 本文简单探讨下该问题,并给出解决方案,如果你想直接看建议,直接翻到第三部分即可。
575 0
Git pull[push] 不用每次输入用户名和密码
网上的很多教程都是使用ssh key来实现免密码操作,其实没那么麻烦,新增一个配置就好了 在当前用户目录下新建.gitconfig文件 cd ~ vim .
1594 0
【技术贴】局域网设置 使用自动配置脚本 有对钩被选中取消不掉的解决办法
【技术贴】局域网设置 使用自动配置脚本 有对钩被选中取消不掉的解决办法     今天用代理,发现了一个很奇怪的现象,我去啊,总是闲着没事,我的代理就被更改了,我把代理关了,发现那个选项还是在,是个9090端口的使用自动配置脚本。
2678 0
WinCE开机密码的输入方法
前两天,客户送回来一台设备,说是系统崩溃了,还写了详细的出错过程。设备采用的是WinCE系统,通过修改注册表,屏蔽了开机运行explorer.exe,直接运行自己的程序。所谓崩溃就是自己的程序运行不起了,其实系统是没有问题的。
893 0
自动化测试框架比较
编辑器加载中...最近在研究自动化测试框架,也和网上的很多朋友聊了很多各种自动化框架的实现,我对其总结归纳比较下。当然,一家之言,仅供参考:   1、以QTP为核心的框架   QTP是大家最常用的测试工具。
1346 0
+关注
玄学酱
这个时候,玄酱是不是应该说点什么...
20683
文章
438
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载