利用Java編碼測試CSRF令牌驗證的Web API


前一篇拙文是利用了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);

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM