使用JNA解决自动化测试无法做密码输入操作的问题
在做页面自动化(以使用selenium为例)的时候,很常见的一个场景就是输入密码。往往对于输入框都使用WebElement的sendKeys(CharSequence... keysToSend)的方法。
Java代码
1./**
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;
系统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;
8.import com.sun.jna.Pointer;
9.import com.sun.jna.platform.win32.WinDef.HWND;
10.import com.sun.jna.platform.win32.WinUser.WNDENUMPROC;
18.public class Win32Util { private static final int N_MAX_COUNT = 512; private Win32Util() { 从桌面开始查找指定类名的组件,在超时的时间范围内,如果未找到任何匹配的组件则反复查找 @param className 组件的类名 @param timeout 超时时间 @param unit 超时时间的单位 @return 返回匹配的组件的句柄,如果匹配的组件大于一个,返回第一个查找的到的;如果未找到或超时则返回null public static HWND findHandleByClassName(String className, long timeout, TimeUnit unit) { return findHandleByClassName(USER32EXT.GetDesktopWindow(), className, timeout, unit); 从桌面开始查找指定类名的组件 @param className 组件的类名 @return 返回匹配的组件的句柄,如果匹配的组件大于一个,返回第一个查找的到的;如果未找到任何匹配则返回null public static HWND findHandleByClassName(String className) { return findHandleByClassName(USER32EXT.GetDesktopWindow(), className); 从指定位置开始查找指定类名的组件 @param root 查找组件的起始位置的组件的句柄,如果为null则从桌面开始查找 @param className 组件的类名 @param timeout 超时时间 @param unit 超时时间的单位 @return 返回匹配的组件的句柄,如果匹配的组件大于一个,返回第一个查找的到的;如果未找到或超时则返回null public static HWND findHandleByClassName(HWND root, String className, long timeout, TimeUnit unit) { if(null == className || className.length() = 0) { return null; long start = System.currentTimeMillis(); HWND hwnd = findHandleByClassName(root, className); while(null == hwnd (System.currentTimeMillis() - start unit.toMillis(timeout))) { hwnd = findHandleByClassName(root, className); return hwnd; 从指定位置开始查找指定类名的组件 @param root 查找组件的起始位置的组件的句柄,如果为null则从桌面开始查找 @param className 组件的类名 @return 返回匹配的组件的句柄,如果匹配的组件大于一个,返回第一个查找的到的;如果未找到任何匹配则返回null public static HWND findHandleByClassName(HWND root, String className) { if(null == className || className.length() = 0) { return null; HWND[] result = new HWND[1]; findHandle(result, root, className); return result[0]; private static boolean findHandle(final HWND[] target, HWND root, final String className) { if(null == root) { root = USER32EXT.GetDesktopWindow(); return USER32EXT.EnumChildWindows(root, new WNDENUMPROC() { @Override public boolean callback(HWND hwnd, Pointer pointer) { char[] winClass = new char[N_MAX_COUNT]; USER32EXT.GetClassName(hwnd, winClass, N_MAX_COUNT); if(USER32EXT.IsWindowVisible(hwnd) className.equals(Native.toString(winClass))) { target[0] = hwnd; return false; } else { return target[0] == null || findHandle(target, hwnd, className); }, Pointer.NULL); 模拟键盘按键事件,异步事件。使用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 public static boolean simulateKeyboardEvent(HWND hwnd, int[][] keyCombination) { if(null == hwnd) { return false; USER32EXT.SwitchToThisWindow(hwnd, true); USER32EXT.SetFocus(hwnd); for(int[] keys : keyCombination) { for(int i = 0; i keys.length; i++) { USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYDOWN, 0); // key down for(int i = keys.length - 1; i i--) { USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYUP, 0); // key up return true; 模拟字符输入,同步事件。使用win32 SendMessage API发送WM_CHAR事件。默认10秒超时 @param hwnd 被输入字符的组件的句柄 @param content 输入的内容。字符串会被转换成char[]后逐个字符输入 @return 字符输入事件发送成功返回true,字符输入事件发送失败或超时返回false public static boolean simulateCharInput(final HWND hwnd, final String content) { if(null == hwnd) { return false; try { return execute(new Callable() { @Override public Boolean call() throws Exception { USER32EXT.SwitchToThisWindow(hwnd, true); USER32EXT.SetFocus(hwnd); for(char c : content.toCharArray()) { Thread.sleep(5); USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0); return true; } catch(Exception e) { return false; public static boolean simulateCharInput(final HWND hwnd, final String content, final long sleepMillisPreCharInput) { if(null == hwnd) { return false; try { return execute(new Callable() { @Override public Boolean call() throws Exception { USER32EXT.SwitchToThisWindow(hwnd, true); USER32EXT.SetFocus(hwnd); for(char c : content.toCharArray()) { Thread.sleep(sleepMillisPreCharInput); USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0); return true; } catch(Exception e) { return false; 模拟文本输入,同步事件。使用win32 SendMessage API发送WM_SETTEXT事件。默认10秒超时 @param hwnd 被输入文本的组件的句柄 @param content 输入的文本内容 @return 文本输入事件发送成功返回true,文本输入事件发送失败或超时返回false public static boolean simulateTextInput(final HWND hwnd, final String content) { if(null == hwnd) { return false; try { return execute(new Callable() { @Override public Boolean call() throws Exception { USER32EXT.SwitchToThisWindow(hwnd, true); USER32EXT.SetFocus(hwnd); USER32EXT.SendMessage(hwnd, WM_SETTEXT, 0, content); return true; } catch(Exception e) { return false; 模拟鼠标点击,同步事件。使用win32 SendMessage API发送BM_CLICK事件。默认10秒超时 @param hwnd 被点击的组件的句柄 @return 点击事件发送成功返回true,点击事件发送失败或超时返回false public static boolean simulateClick(final HWND hwnd) { if(null == hwnd) { return false; try { return execute(new Callable() { @Override public Boolean call() throws Exception { USER32EXT.SwitchToThisWindow(hwnd, true); USER32EXT.SendMessage(hwnd, BM_CLICK, 0, null); return true; } catch(Exception e) { return false; private static T execute(Callable callable) throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); try { Future task = executor.submit(callable); return task.get(10, TimeUnit.SECONDS); } finally { executor.shutdown(); }
240.}
其中用到的各种事件类型定义如下:
1.public class Win32MessageConstants {
下面写一段测试代码来测试支付宝密码安全控件的输入,测试代码如下:
1.import java.util.concurrent.TimeUnit;
3.import static org.hamcrest.core.Is.is;4.import static org.junit.Assert.assertThat;
6.import static org.hamcrest.core.IsNull.notNullValue;7.import org.junit.Test;
9.import com.sun.jna.platform.win32.WinDef;10.import com.sun.jna.platform.win32.WinDef.HWND;
12.public class AlipayPasswordInputTest { @Test public void testAlipayPasswordInput() { String password = "your password"; HWND alipayEdit = findHandle("Chrome_RenderWidgetHostHWND", "Edit"); //Chrome浏览器,使用Spy++可以抓取句柄的参数 assertThat("获取支付宝密码控件失败。", alipayEdit, notNullValue()); boolean isSuccess = Win32Util.simulateCharInput(alipayEdit, password); assertThat("输入支付宝密码["+ password +"]失败。", isSuccess, is(true)); private WinDef.HWND findHandle(String browserClassName, String alieditClassName) { WinDef.HWND browser = Win32Util.findHandleByClassName(browserClassName, 10, TimeUnit.SECONDS); return Win32Util.findHandleByClassName(browser, alieditClassName, 10, TimeUnit.SECONDS); }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/
python+pytest接口自动化(9)-cookie绕过登录(保持登录状态) 在编写接口自动化测试用例或其他脚本的过程中,经常会遇到需要绕过用户名/密码或验证码登录,去请求接口的情况,一是因为有时验证码会比较复杂,比如有些图形验证码,难以通过接口的方式去处理;再者,每次请求接口前如果都需要先去登录一次,这样不仅效率低,还耗费资源。 有些网站是使用cookie辨别用户身份的,此时我们便可以先登录一次,拿到登录成功后的cookie,后续请求时在请求头中加入该cookie,便可保持登录状态直接请求。
相关文章
- 注册、登录、密码修改页面渗透测试经验小结
- php中密码强中弱判断
- win10 WiFi 密码查询 命令
- 渗透测试-xss漏洞之反射型(post)获取用户密码
- linux下去掉pdf的密码(前提:知道密码)
- AndroidStudio制作“我”的界面,设置,修改密码,设置密保和找回密码
- Kali Linux 2020.1修改root用户密码
- web渗透测试----5、暴力破解漏洞--(4)Telnet密码破解
- web渗透测试----5、暴力破解漏洞--(3)FTP密码破解
- 基于python的密码生成器实例解析
- docker 默认用户和密码
- chage 修改用户密码有效期限的命令
- 使用jQuery实现“记住密码”功能,超简单
- 混合密码系统
- easyui 判断密码是否输入一致
- 读书笔记_卓越程序员密码
- Oracle忘记用户名和密码
- vmware vcenter orchestrator configuration提示“用户名密码错误或登录失败超过次数被锁定”