HTTPS POST 来自 Java 应用程序的请求到 S/4HANA 系统中的 OData 服务,具有有效的 CSRF 令牌

HTTPS POST Request from Java Application to OData-Service in S/4HANA System with valid CSRF-Token

情况如下: 一方面,我创建了一个 OData 服务,它应该在收到 POST-Request 时创建一个条目。该服务在 S/4HANA 系统中创建,可通过 SAP 网关访问。

另一方面,我有一个 Java 应用程序 (OpenJDK 11),它本质上是一个循环,并且必须向每个循环发出一个 POST-Request 到 OData-Service。

我正在使用 IntelliJ IDEA 社区版和 OpenJDK 11。 这也是我第一次将 OData 与 Java 和 SAP 一起使用。

起初我尝试了以下方法:

private static void postRequest() throws IOException {
        //Setting authenticator needed for login
        Authenticator authenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password.toCharArray());
            }
        };
        Authenticator.setDefault(authenticator);
        
        //Creating the connection
        URL url = new URL("<my_service_link>");
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
        con.setRequestMethod("POST");
        con.setRequestProperty("Content-Type", "application/json; utf-8");
        con.setRequestProperty("Accept", "application/json");
        con.setDoOutput(true);
        try(OutputStream os = con.getOutputStream()) {
            byte[] input = this.getJsonRequest().getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        //Reading response
        int status = con.getResponseCode();

        Reader streamReader = null;

        if (status > 299) {
            streamReader = new InputStreamReader(con.getErrorStream());
        } else {
            streamReader = new InputStreamReader(con.getInputStream());
        }

        BufferedReader in = new BufferedReader(streamReader);
        String inputLine;
        StringBuffer content = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            content.append(inputLine);
        }
        in.close();
        con.disconnect();
        System.out.println(content.toString());
    }

但是我收到错误消息,我的 CSRF-Token 无效。

因此,在通过谷歌搜索找出什么是 CSRF 令牌后,我尝试首先使用自己的 HttpsURLConnection:

创建一个 GET 请求
private static String getRequest() {
        //Setting authenticator needed for login
        Authenticator authenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password.toCharArray());
            }
        };
        Authenticator.setDefault(authenticator);
        
        //Creating the connection
        URL url = new URL("<my_service_link>");
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
        con.setRequestMethod("GET");
        con.setRequestProperty("Content-Type", "application/json; utf-8");
        con.setRequestProperty("X-CSRF-Token","fetch");
        con.connect();
        return con.getHeaderField("x-csrf-token").toString();
}

然后我会向同一个 URL 发出实际的 POST-Request 并使用

将之前的 X-CSRF-Token 设置到 HTTPS-Header 中

con.setRequestProperty("X-CSRF-Token",theGETToken);postRequest()

但我还是遇到了同样的错误。

我做错了什么?

经过更多的谷歌搜索,我终于明白我错过了什么。

CSRF-Token仅对特定session用户有效。 session 由 HTTPS-Header 中传递的 cookie 标识。

需要做的事情如下(另见:https://blogs.sap.com/2021/06/04/how-does-csrf-token-work-sap-gateway/):

  1. 通过发出 non-modification 请求打开 session 并指定 header 以获取 CSRF-Token 和 session-cookies
    HTTP-Request:
    Type: GET
    Header-Fields: x-csrf-token = fetch
                   set-cookie = fetch
    
  2. 保存 CSRF-Token 和 session-cookies,因为 POST-Request
  3. 需要它们
  4. 发出一个POST-Request并从保存的值中设置session-cookies和CSRF-Token
    HTTP-Request:
    Type: POST
    Header-Fields: x-csrf-token = <tokenFromGet>
                   cookie = <allSessionCookies>
    

注意请求的 header 字段被命名为 cookie 而不是 set-cookie 并传递 set-cookie 的 Header 字段的所有值到 POST-Request-Header.

同样重要的是要提及,CSRF-Token 以及 session-cookies 在提供或调整的时间范围或对 session 进行任何更改后到期,两者都必须重新获取(参见 https://blogs.sap.com/2021/06/04/how-does-csrf-token-work-sap-gateway/#comment-575524)。

我的工作代码示例:

import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URL;

public class ODataLogger {

    private String sessionCookies;
    private String csrfToken;

    public ODataLogger() {}

    public void logOdata (String user, String pass, String jsonBody) throws IOException {
        this.setDefaultAuthenticator(user, pass);
        fetchSessionHeaderFields();
        postRequest(jsonBody);
    }

    private void setDefaultAuthenticator (String user, String pass) {
        Authenticator authenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, pass.toCharArray());
            }
        };
        Authenticator.setDefault(authenticator);
    }

    private void fetchSessionHeaderFields() throws IOException {
        URL url = new URL("<my-service-link>");
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
        con.setRequestMethod("GET");
        con.setRequestProperty("Content-Type", "application/json");
        con.setRequestProperty("x-csrf-token", "fetch");
        con.setRequestProperty("set-cookie","fetch");

        //Reading Response
        int status = con.getResponseCode();

        Reader streamReader = null;

        if (status < 299) {
            StringBuffer sb = new StringBuffer(con.getHeaderFields().get("set-cookie").toString());
            //Delete leading [ and trailing ] character
            sb.deleteCharAt(this.sessionCookies.length()-1);
            sb.deleteCharAt(0);
            this.sessionCookies = sb.toString();
            this.csrfToken = con.getHeaderField("x-csrf-token");
            return;
        }
    }

    private void postRequest(String jsonBody) throws IOException {
        //Creating the connection
        URL url = new URL("<my-service-link>");
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
        con.setRequestMethod("POST");
        con.setRequestProperty("Content-Type", "application/json");
        con.setRequestProperty("x-csrf-token", this.csrfToken);
        con.setRequestProperty("Cookie", this.sessionCookies);
        con.setRequestProperty("Accept", "application/json");

        //Setting JSON Body
        con.setDoOutput(true);
        try(OutputStream os = con.getOutputStream()) {
            byte[] input = jsonBody.getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        //Reading response
        int status = con.getResponseCode();

        Reader streamReader = null;

        if (status > 299) {
            streamReader = new InputStreamReader(con.getErrorStream());
        } else {
            streamReader = new InputStreamReader(con.getInputStream());
        }

        BufferedReader in = new BufferedReader(streamReader);
        String inputLine;
        StringBuffer content = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            content.append(inputLine);
        }
        in.close();
        con.disconnect();