利用Java编码测试CSRF令牌验证的Web API

简介: 前一篇拙文是利用了Jmeter来测试带有CSRF令牌验证的Web API;最近几天趁着项目不忙,练习了用编码的方式实现。有了之前Jmeter脚本的基础,基本上难点也就在两个地方:获取CSRF令牌、Cookie的传递。

前一篇拙文是利用了Jmeter来测试带有CSRF令牌验证的Web API;最近几天趁着项目不忙,练习了用编码的方式实现。

有了之前Jmeter脚本的基础,基本上难点也就在两个地方:获取CSRF令牌、Cookie的传递。

首先添加依赖,在POM.xml中添加以下内容:

        <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.11.3</version>
        </dependency>

解释作用:

 - httpClient:用来创建httpClient、管理Get和Post的方法、获取请求报文头、应答报文内容、管理CookieStore等等;

 - jsoup:用来解析应答报文,获得CSRF令牌的值。

 

创建一个Web API测试类:

public class LoginEHR {

    private final static String EHR_ADDRESS = "http://ourTestEHRServer:8083";

    static BasicCookieStore cookieStore = new BasicCookieStore();
    static CloseableHttpClient httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();


}

我选择了CookieStore的方式管理会话;HttpClient现在还有另一种Context的方式实现会话持久,以后再做深入研究。

先写一个打印应答报文的方法,并不做什么处理,纯打印;根据实际需要调用或者注释:

public class LoginEHR {

private static void printResponse(HttpResponse httpResponse)
throws ParseException, IOException {
        // 获取响应消息实体
        HttpEntity entity = httpResponse.getEntity();
        // 响应状态
        System.out.println("--------Status: " + httpResponse.getStatusLine());
        System.out.println("--------Headers: ");
        HeaderIterator iterator = httpResponse.headerIterator();
        while (iterator.hasNext()) {
            System.out.println("\t" + iterator.next());
        }
        // 判断响应实体是否为空
        if (entity != null) {
            String responseString = EntityUtils.toString(entity);
            System.out.println("--------Response length: " + responseString.length());
            System.out.println("--------Response content: "
                    + responseString.replace("\r\n", ""));
        }
    }

 现在开始写测试方法,虽然篇幅较长,仍然写在main()方法里,便于展示:

public class LoginEHR {

    private final static String EHR_ADDRESS = "http://ourTestEHRServer:8083";

    static BasicCookieStore cookieStore = new BasicCookieStore();
    static CloseableHttpClient httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();

    public static void main(String[] args) throws Exception {

        String username = "00022222";
        String password = "abc123456";

        CloseableHttpResponse httpResponse = null;

        try {
            HttpGet httpGet = new HttpGet(EHR_ADDRESS);
            httpResponse = httpClient.execute(httpGet);
            System.out.println("--------Cookie store for the 1st GET: " + cookieStore.getCookies());
            // 唯一的作用是打印应答报文,没有任何处理;实际测试时,可以不执行
//            printResponse(httpResponse);

            // 取出第一次请求时,服务器端返回的JSESSIONID;
            // 实际上此处只是取出JSESSIONID用作打印;cookieStore自动保存了本次会话的Cookie信息
//            List cookies = cookieStore.getCookies();
//            String cookie = cookies.toString();
//            String sessionID = cookie.substring("[[version: 0][name: JSESSIONID][value: ".length(),
//                    cookie.indexOf("][domain"));
//            System.out.println("--------The current JSESSIONID is: " + sessionID);




httpClient.close(); }
catch (Exception ex) { ex.printStackTrace(); } }

private static void printResponse(HttpResponse httpResponse)
throws ParseException, IOException { ...... }

根据之前Jmeter测试脚本的经验,先发送一次Get请求,从应答报文中得到CSRF令牌和JSESSIONID。

大家注意我注释掉的那几行打印JSESSIONID的代码,之前在没有引入CookieStore之前,我想的是自己写一个新的Cookie,并把它赋给后面几次请求。

当使用CookieStore之后,就不需要自己封装Cookie、以及添加到Request的Header了,这过程会自动完成。没有删掉也是为了需要的时候打印。

 

交代完Cookie之后,该轮到处理CSRF令牌了。如果打印出第一次Get的应答,我们能看到令牌的格式是如下呈现的:

之前在Jmeter脚本中,我是添加了一个正则表达式提取器,把_csrf的content提取出来。

现在我将用jsoup来解析和返回content的内容,代码如下:

private static String getCsrfToken(HttpEntity responseEntity) throws IOException{
        //获取网页内容,指定编码
        String web = EntityUtils.toString(responseEntity,"utf-8");
        Document doc= Jsoup.parse(web);
        // 选择器,选取特征信息
        String token = doc.select("meta[name=_csrf]").get(0).attr("content");
        System.out.println( "--------The current CSRF Token is: " + token);

        return token;
    }

在main()中调用此方法:

            // 利用Jsoup从应答报文中读取CSRF Token
HttpEntity responseEntity = httpResponse.getEntity();
       String token = getCsrfToken(responseEntity);

然后再封装POST的请求内容:

            // 获取到CSRF Token后,用Post方式登录
            HttpPost httpPost = new HttpPost(EHR_ADDRESS);

            // 拼接Post的消息体
            List<NameValuePair> nvps = new ArrayList<NameValuePair>();
            nvps.add(new BasicNameValuePair("username", username));
            nvps.add(new BasicNameValuePair("password", password));
            nvps.add(new BasicNameValuePair("_csrf", token));
            HttpEntity loginParams = new UrlEncodedFormEntity(nvps, "utf-8");
            httpPost.setEntity(loginParams);

            // 第二次请求,带有CSRF Token
            httpResponse = httpClient.execute(httpPost);
//            System.out.println("--------Cookie store for the POST: " + cookieStore.getCookies());
            printResponse(httpResponse);

然后。。。这里发生了一点小意外:

按照设想,应该能跳转到登录成功、或者验证失败的页面;而Post方法执行后,从服务器返回的状态码是302,被跳转到另一个网址。

如果放任不管,直接提交后面的业务查询,是不会得到成功的;执行的结果是又回到了登录页面。

我在网上爬了一会,发现提问Post得到301、302的人还不在少数,说明这个坑还是给很多人造成了困扰。

简单的说,如果得到了服务器重定向到新的地址,我们也要跟着执行一次新地址的访问;否则服务器会认为这次请求没有得到正确处理,即便我之后的请求带着全套的验证令牌和Cookie,也会被拦截在系统外。

有了这个认识,下面我需要完成的就是对Code:302的处理;添加代码如下:

            // 取POST方法返回的HTTP状态码;不出意外的话是302
            int code = httpResponse.getStatusLine().getStatusCode();
            if (code == 302) {
                Header header = httpResponse.getFirstHeader("location"); // 跳转的目标地址是在 HTTP-HEAD 中的
                String newUri = header.getValue(); // 这就是跳转后的地址,再向这个地址发出新申请,以便得到跳转后的信息是啥。
                // 实际打印出来的是接口服务地址,不包括IP Address部分
                System.out.println("--------Redirect to new location: " + newUri);
                httpGet = new HttpGet(EHR_ADDRESS + newUri);

                httpResponse = httpClient.execute(httpGet);
//                printResponse(httpResponse);
            }

这里需要注意的地方是跳转的location内容。在我这里,服务器给的只是一个单词【/work】,最好加一个打印的步骤。

确认不是一个完整的URL之后,需要把链接拼完整,然后进行一次httpGet请求。

这个httpGet执行之后,我可以确认已经登录成功(或者,又被送回登录页面,当然我这里是成功了)。

 

接下来是提交一次业务查询的Get,确认能够在系统中进行业务操作:

            // 请求一次绩效;确认登录成功
            String queryUrl = EHR_ADDRESS + "/emp/performance/mt/query";
            httpGet = new HttpGet(queryUrl);
            httpResponse = httpClient.execute(httpGet);
            System.out.println("--------Result of the Cardpunch Query: ");
            printResponse(httpResponse);

最后确认查询的结果无误后,整个脚本完成;只需要修改最后的业务查询,就可以生成其他的测试脚本了。

 

完整的源码如下:

package com.jason.apitest;

import org.apache.http.Header;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class LoginEHR {

    private final static String EHR_ADDRESS = "http://ourTestEHRServer:8083";

    static BasicCookieStore cookieStore = new BasicCookieStore();
    static CloseableHttpClient httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();

    public static void main(String[] args) throws Exception {

        String username = "00022222";
        String password = "abc123456";

        HttpResponse httpResponse = null;

        try {
            HttpGet httpGet = new HttpGet(EHR_ADDRESS);
            httpResponse = httpClient.execute(httpGet);
            System.out.println("--------Cookie store for the 1st GET: " + cookieStore.getCookies());
            // 唯一的作用是打印应答报文,没有任何处理;实际测试时,可以不执行
//            printResponse(httpResponse);

            // 取出第一次请求时,服务器端返回的JSESSIONID;
            // 实际上此处只是取出JSESSIONID用作打印;cookieStore自动保存了本次会话的Cookie信息
//            List cookies = cookieStore.getCookies();
//            String cookie = cookies.toString();
//            String sessionID = cookie.substring("[[version: 0][name: JSESSIONID][value: ".length(),
//                    cookie.indexOf("][domain"));
//            System.out.println("--------The current JSESSIONID is: " + sessionID);

            // 利用Jsoup从应答报文中读取CSRF Token
            HttpEntity responseEntity = httpResponse.getEntity();
            String token = getCsrfToken(responseEntity);

            // 获取到CSRF Token后,用Post方式登录
            HttpPost httpPost = new HttpPost(EHR_ADDRESS);

            // 拼接Post的消息体
            List<NameValuePair> nvps = new ArrayList<NameValuePair>();
            nvps.add(new BasicNameValuePair("username", username));
            nvps.add(new BasicNameValuePair("password", password));
            nvps.add(new BasicNameValuePair("_csrf", token));
            HttpEntity loginParams = new UrlEncodedFormEntity(nvps, "utf-8");
            httpPost.setEntity(loginParams);

            // 第二次请求,带有CSRF Token
            httpResponse = httpClient.execute(httpPost);
//            System.out.println("--------Cookie store for the POST: " + cookieStore.getCookies());
            printResponse(httpResponse);

            // 取POST方法返回的HTTP状态码;不出意外的话是302
            int code = httpResponse.getStatusLine().getStatusCode();
            if (code == 302) {
                Header header = httpResponse.getFirstHeader("location"); // 跳转的目标地址是在 HTTP-HEAD 中的
                String newUri = header.getValue(); // 这就是跳转后的地址,再向这个地址发出新申请,以便得到跳转后的信息是啥。
                // 实际打印出来的是接口服务地址,不包括IP Address部分
                System.out.println("--------Redirect to new location: " + newUri);
                httpGet = new HttpGet(EHR_ADDRESS + newUri);

                httpResponse = httpClient.execute(httpGet);
//                printResponse(httpResponse);
            }


            // 请求一次绩效;确认登录成功
            String queryUrl = EHR_ADDRESS + "/emp/performance/mt/query";
            httpGet = new HttpGet(queryUrl);
            httpResponse = httpClient.execute(httpGet);
            System.out.println("--------Result of the Cardpunch Query: ");
            printResponse(httpResponse);

            httpClient.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }

    private static void printResponse(HttpResponse httpResponse)
            throws ParseException, IOException {
        // 获取响应消息实体
        HttpEntity entity = httpResponse.getEntity();
        // 响应状态
        System.out.println("--------Status: " + httpResponse.getStatusLine());
        System.out.println("--------Headers: ");
        HeaderIterator iterator = httpResponse.headerIterator();
        while (iterator.hasNext()) {
            System.out.println("\t" + iterator.next());
        }
        // 判断响应实体是否为空
        if (entity != null) {
            String responseString = EntityUtils.toString(entity);
            System.out.println("--------Response length: " + responseString.length());
            System.out.println("--------Response content: "
                    + responseString.replace("\r\n", ""));
        }
    }

    private static String getCsrfToken(HttpEntity responseEntity) throws IOException{
        //获取网页内容,指定编码
        String web = EntityUtils.toString(responseEntity,"utf-8");
        Document doc= Jsoup.parse(web);
        // 选择器,选取特征信息
        String token = doc.select("meta[name=_csrf]").get(0).attr("content");
        System.out.println( "--------The current CSRF Token is: " + token);

        return token;
    }


}

 

补充:如果使用HttpClientContext方式来维持会话,与CookieStore很接近;直接帖上需要修改的部分内容:

// 创建httpClient和context
static CloseableHttpClient httpClient = HttpClients.createDefault();
static HttpClientContext context = HttpClientContext.create();

// 下面的代码写在main()方法中
CloseableHttpResponse httpResponse = null;
// 先发起一个Get请求,获取CSRF令牌和Cookie
HttpGet httpGet = new HttpGet(EHR_ADDRESS);
// 保存context上下文
httpResponse = httpClient.execute(httpGet, context);
...
// 处理完CSRF令牌后,准备发起POST请求
HttpPost httpPost = new HttpPost(EHR_ADDRESS);
... // 封装POST报文

// 发起POST请求
httpResponse = httpClient.execute(httpPost, context);

// 处理HTTP 302和业务查询操作的GET,也要携带着context
httpResponse = httpClient.execute(httpGet, context);

 

相关文章
|
24天前
|
机器学习/深度学习
神经网络与深度学习---验证集(测试集)准确率高于训练集准确率的原因
本文分析了神经网络中验证集(测试集)准确率高于训练集准确率的四个可能原因,包括数据集大小和分布不均、模型正则化过度、批处理后准确率计算时机不同,以及训练集预处理过度导致分布变化。
|
1天前
|
SQL 安全 数据库
深度揭秘:Python Web安全攻防战,SQL注入、XSS、CSRF一网打尽!
在Web开发领域,Python虽强大灵活,但安全挑战不容小觑。本文剖析Python Web应用中的三大安全威胁:SQL注入、XSS及CSRF,并提供防御策略。通过示例代码展示如何利用参数化查询、HTML转义与CSRF令牌构建安全防线,助您打造更安全的应用。安全是一场持久战,需不断改进优化。
10 3
|
11天前
|
机器学习/深度学习 Python
训练集、测试集与验证集:机器学习模型评估的基石
在机器学习中,数据集通常被划分为训练集、验证集和测试集,以评估模型性能并调整参数。训练集用于拟合模型,验证集用于调整超参数和防止过拟合,测试集则用于评估最终模型性能。本文详细介绍了这三个集合的作用,并通过代码示例展示了如何进行数据集的划分。合理的划分有助于提升模型的泛化能力。
|
7天前
|
SQL 存储 安全
Web安全-CSRF跨站请求伪造
Web安全-CSRF跨站请求伪造
24 5
|
8天前
|
SQL 安全 数据库
从入门到精通:Python Web安全守护指南,SQL注入、XSS、CSRF全防御!
【9月更文挑战第13天】在开发Python Web应用时,安全性至关重要。本文通过问答形式,详细介绍如何防范SQL注入、XSS及CSRF等常见威胁。通过使用参数化查询、HTML转义和CSRF令牌等技术,确保应用安全。附带示例代码,帮助读者从入门到精通Python Web安全。
30 6
|
9天前
|
SQL 安全 JavaScript
告别Web安全小白!Python实战指南:抵御SQL注入、XSS、CSRF的秘密武器!
【9月更文挑战第12天】在Web开发中,安全漏洞如同暗礁,尤其对初学者而言,SQL注入、跨站脚本(XSS)和跨站请求伪造(CSRF)是常见挑战。本文通过实战案例,展示如何利用Python应对这些威胁。首先,通过参数化查询防止SQL注入;其次,借助Jinja2模板引擎自动转义机制抵御XSS攻击;最后,使用Flask-WTF库生成和验证CSRF令牌,确保转账功能安全。掌握这些技巧,助你构建更安全的Web应用。
14 5
|
20天前
|
Java Maven Android开发
解锁Web开发新技能:从零开始的Struts 2之旅——让你的Java编程之路更加宽广,首个应用实例带你飞!
【8月更文挑战第31天】对于初学者,掌握 Struts 2 框架不仅能提升 Web 开发能力,还能深入了解 MVC 架构。Struts 2 是一个基于 Servlet 的 Java 框架,提供表单验证、文件上传、国际化等功能,便于快速构建易维护的 Web 应用。本文通过示例演示如何从零开始搭建环境并创建一个简单的 Struts 2 项目,包括配置 `struts.xml`、编写 Action 类及视图文件,并配置 web.xml。通过这些步骤,你将学会基本的开发流程,为进一步学习高级功能打下基础。
29 0
|
20天前
|
开发者 Java 前端开发
Struts 2验证框架:如何让数据校验成为Web开发的隐形守护者?揭秘前后端一致性的秘诀
【8月更文挑战第31天】在现代Web开发中,数据验证对确保应用健壮性和良好用户体验至关重要。随着前后端分离架构的普及,保证数据校验一致性尤为关键。Struts 2 验证框架基于 JavaBean 验证 API(JSR 303/JSR 380),允许开发者通过注解或 XML 配置轻松定义验证规则,确保输入数据在执行业务逻辑前已通过验证。此外,Struts 2 支持与前端 JavaScript 验证相结合,确保前后端数据校验一致,提升用户体验。通过注解、XML 配置和资源文件,开发者可以轻松定义和调整验证规则,实现前后端一致的数据校验,提高应用健壮性。
28 0
|
20天前
|
安全 Java 云计算
JSF 应用究竟何去何从?云端部署能否成为其全新突破点?快来一探究竟!
【8月更文挑战第31天】本文介绍了将JavaServer Faces(JSF)应用部署到云平台的过程。首先,根据成本、功能、可靠性和安全性选择合适的云平台。接着,展示了构建简单JSF应用的示例代码。最后,以AWS Elastic Beanstalk为例,详细说明了部署流程。部署至云端可提升应用的可用性、扩展性和安全性。
30 0
|
21天前
|
安全 前端开发 PHP
构建与验证表单:传统PHP与Laravel框架的比较分析——探索Web开发中表单处理的优化策略和最佳实践
【8月更文挑战第31天】在 Web 开发中,表单构建与数据验证至关重要。传统 PHP 方法需手动处理 HTML 表单和数据验证,而 Laravel 框架则提供了一种更现代、高效的解决方案。本文通过对比传统 PHP 和 Laravel 的方法,探讨表单构建与验证的最佳实践。Laravel 通过简洁的语法糖、内置的数据过滤和验证机制,显著提升了代码的安全性和可维护性,适用于大型项目或需要快速开发的场景。然而,在追求灵活性的小型项目中,直接使用 PHP 仍是不错的选择。了解两者的优劣,有助于开发者根据项目需求做出最佳决策。
24 0