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/):
- 通过发出 non-modification 请求打开 session 并指定 header 以获取 CSRF-Token 和 session-cookies
HTTP-Request:
Type: GET
Header-Fields: x-csrf-token = fetch
set-cookie = fetch
- 保存 CSRF-Token 和 session-cookies,因为 POST-Request
需要它们
- 发出一个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();
情况如下: 一方面,我创建了一个 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
:
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/):
- 通过发出 non-modification 请求打开 session 并指定 header 以获取 CSRF-Token 和 session-cookies
HTTP-Request: Type: GET Header-Fields: x-csrf-token = fetch set-cookie = fetch
- 保存 CSRF-Token 和 session-cookies,因为 POST-Request 需要它们
- 发出一个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();