使用JNA解决自动化测试无法做密码输入操作的问题
来源:互联网 发布:制作毕业证软件 编辑:程序博客网 时间:2024/05/16 18:09
/**
* 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,所以会失效。
- 使用JNA解决自动化测试无法做密码输入操作的问题
- linux下mysql 使用mysql -uroot -p输入密码无法登录问题的解决
- JNA使用问题及解决
- Python-Selenium2做Web自动化测试(6)-解决使用Webdrive打开Firefox不含有插件的问题
- 解决Android LogCat 输出乱码的问题(转) -相当有用做自动化测试时
- Centos 开机无法输入密码的问题
- 功能测试自动化视频---解决自定义控件的无法识别问题
- 公司自动化测试--需要解决的问题。
- Ubuntu 9.04更新后桌面无法显示问题的解决(输入密码后死机)
- 解决Ubuntu输入密码后无法进入桌面,一直停留在登陆界面的问题
- 关于使用JNA爬过的坑,JNA路径问题
- 解决sqoop需要输入密码的问题
- 解决使用junit测试时,无法加载redisCache的问题
- Ubuntu 锁定屏幕后即使输入正确密码也被提示密码错误导致无法解锁 问题的解决
- 解决outlook无法保存密码的问题
- 用python做自动化测试--web 自动化测试(1)-Selenium 3.x使用系列问题集
- Appium自动化测试-软键盘隐藏后无法使用的问题
- 解决Emacs无法输入中文的问题
- 详细介绍一下 BVT
- Android、布局优化、include、merge、
- 手机Android音视频采集与直播推送,实现单兵、移动监控类应用
- Android 万能取色 Button
- Android关于Activity的一些使用和全局管理技巧
- 使用JNA解决自动化测试无法做密码输入操作的问题
- 腾讯bugly crash追踪平台之符号表创建(二)(定位更加准确!)
- vector
- DiscreteSeekBar使用简介,一个带气泡的SeekBar
- 手动配置servlet犯的一个低级错误
- 《剑指Offer》读书笔记07:斐波那契数列
- 关于UITableView 中两个困惑的问题
- POJ1260 DP
- hadoop federation部署实践,亲测