使用 Java 11 HttpClient 设置会话 cookie

Setting a session cookie with the Java 11 HttpClient

我正在尝试使用 Java 11 HttpClient 获取页面内容。由于该页面使用 OAuth,我想使用我的会话 cookie 进行身份验证。我目前使用以下代码,灵感来自 this question:

public static void main(String[] args) throws IOException, InterruptedException {
  String year = args[0];
  String day = args[1];
  String session = System.getenv("AOCSESSION");
  System.out.println(session);
  
  HttpRequest req = HttpRequest.newBuilder()
      .uri(URI.create("https://adventofcode.com/"+year+"/day/"+day+"/input")).GET().build();
  CookieManager cookieManager = new CookieManager();
  HttpCookie cookie = new HttpCookie("session", session);
  cookieManager.getCookieStore().add(URI.create("https://adventofcode.com"), cookie);
  HttpClient client = HttpClient.newBuilder().cookieHandler(cookieManager).build();

  String body = client.send(req, BodyHandlers.ofString()).body();
  System.out.println(body);
}

但是,这些请求未经身份验证(响应:Puzzle inputs differ by user. Please log in to get your puzzle input.)。我已经尝试向 cookie 添加其他字段:

cookie.setDomain(".adventofcode.com");
cookie.setMaxAge(Instant.parse("2031-11-01T18:26:23.293Z").getEpochSecond());
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(true);

但是并没有解决问题。

如何向请求添加会话 cookie?

下面的解决方案是使用适当的 CookieManager 而不是硬编码 header:

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

        CookieHandler.setDefault(new CookieManager());

        HttpCookie sessionCookie = new HttpCookie("session", "53616c7465645f5f9d467d3ae831ec1b1e7289ef45d256224786e1ed13");
        sessionCookie.setPath("/");
        sessionCookie.setVersion(0);

        ((CookieManager) CookieHandler.getDefault()).getCookieStore().add(new URI("https://adventofcode.com"),
                sessionCookie);

        HttpClient client = HttpClient.newBuilder()
                .cookieHandler(CookieHandler.getDefault())
                .connectTimeout(Duration.ofSeconds(10))
                .build();

        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create("https://adventofcode.com/2020/day/1/input"))
                .GET().build();


        System.out.println(client.send(req, HttpResponse.BodyHandlers.ofString()).body());

    }

sessionCookie.setPath("/"); 重要:Java 有 CookieFilter,它实际上匹配请求 URI 路径,而不是 Cookie 路径。

sessionCookie.setVersion(0); 重要提示:本站不喜欢版本 1(HttpCookie 默认,符合 RFC 2965/2109)

版本 0 符合原始 Netscape cookie,AdventOfCode 喜欢它。

版本 0 和版本 1 之间的区别在于 Cookie 的外观:

版本 0:

INFO: HEADERS: HEADERS FRAME (stream=1)
    :authority: adventofcode.com
    :method: GET
    :path: /2020/day/1/input
    :scheme: https
    Cookie: session=53616c7465645f5f9d467....
    User-Agent: Java-http-client/11.0.7

版本 1:

INFO: HEADERS: HEADERS FRAME (stream=1)
    :authority: adventofcode.com
    :method: GET
    :path: /2020/day/1/input
    :scheme: https
    Cookie: $Version="1"
    Cookie: session="53616c7465645f5f9d467.....13";$Path="/"
    User-Agent: Java-http-client/11.0.7

版本 1 将从网站抛出 500 服务器错误。

为了帮助您调试 HttpClient 日志,请将以下内容添加到您的程序参数中:

-Djdk.httpclient.HttpClient.log=errors,requests,headers,frames[:control:data:window:all],content,ssl,trace,channel,all

日志:

jdk.internal.net.http.HttpClientImpl$SelectorManager run
INFO: CHANNEL: HttpClient-1-SelectorManager: starting
nov. 04, 2021 3:35:39 EM jdk.internal.net.http.MultiExchange requestFilters
INFO: MISC: Applying request filters
nov. 04, 2021 3:35:39 EM jdk.internal.net.http.MultiExchange requestFilters
INFO: MISC: Applying jdk.internal.net.http.AuthenticationFilter@49ec71f8
nov. 04, 2021 3:35:39 EM jdk.internal.net.http.MultiExchange requestFilters
INFO: MISC: Applying jdk.internal.net.http.RedirectFilter@8f4ea7c
nov. 04, 2021 3:35:39 EM jdk.internal.net.http.MultiExchange requestFilters
INFO: MISC: Applying jdk.internal.net.http.CookieFilter@436813f3
nov. 04, 2021 3:35:39 EM jdk.internal.net.http.CookieFilter request
INFO: MISC: Request: adding cookies for https://adventofcode.com/2020/day/1/input
nov. 04, 2021 3:35:39 EM jdk.internal.net.http.MultiExchange requestFilters
INFO: MISC: All filters applied
nov. 04, 2021 3:35:39 EM jdk.internal.net.http.AbstractAsyncSSLConnection createSSLParameters
INFO: SSL: AbstractAsyncSSLConnection: Setting application protocols: [h2, http/1.1]
nov. 04, 2021 3:35:39 EM jdk.internal.net.http.AbstractAsyncSSLConnection <init>
INFO: SSL: SSLParameters:
    cipher: TLS_AES_128_GCM_SHA256
    cipher: TLS_AES_256_GCM_SHA384
    cipher: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
    cipher: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
    cipher: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
    cipher: TLS_RSA_WITH_AES_256_GCM_SHA384
    cipher: TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384
    cipher: TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384
    cipher: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
    cipher: TLS_DHE_DSS_WITH_AES_256_GCM_SHA384
    cipher: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    cipher: TLS_RSA_WITH_AES_128_GCM_SHA256
    cipher: TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256
    cipher: TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256
    cipher: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
    cipher: TLS_DHE_DSS_WITH_AES_128_GCM_SHA256
    cipher: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
    cipher: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
    cipher: TLS_RSA_WITH_AES_256_CBC_SHA256
    cipher: TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384
    cipher: TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384
    cipher: TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
    cipher: TLS_DHE_DSS_WITH_AES_256_CBC_SHA256
    cipher: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
    cipher: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
    cipher: TLS_RSA_WITH_AES_256_CBC_SHA
    cipher: TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA
    cipher: TLS_ECDH_RSA_WITH_AES_256_CBC_SHA
    cipher: TLS_DHE_RSA_WITH_AES_256_CBC_SHA
    cipher: TLS_DHE_DSS_WITH_AES_256_CBC_SHA
    cipher: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
    cipher: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
    cipher: TLS_RSA_WITH_AES_128_CBC_SHA256
    cipher: TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256
    cipher: TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256
    cipher: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
    cipher: TLS_DHE_DSS_WITH_AES_128_CBC_SHA256
    cipher: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
    cipher: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
    cipher: TLS_RSA_WITH_AES_128_CBC_SHA
    cipher: TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA
    cipher: TLS_ECDH_RSA_WITH_AES_128_CBC_SHA
    cipher: TLS_DHE_RSA_WITH_AES_128_CBC_SHA
    cipher: TLS_DHE_DSS_WITH_AES_128_CBC_SHA
    cipher: TLS_EMPTY_RENEGOTIATION_INFO_SCSV
    application protocol: h2
    application protocol: http/1.1
    protocol: TLSv1.3
    protocol: TLSv1.2
    endpointIdAlg: HTTPS
    server name: type=host_name (0), value=adventofcode.com

nov. 04, 2021 3:35:39 EM jdk.internal.net.http.HttpClientImpl registerTimer
INFO: MISC: Registering timer ConnectTimerEvent, TimeoutEvent[id=1, duration=PT10S, deadline=2021-11-04T14:35:49.817109800Z]
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.SocketTube$InternalReadPublisher$InternalReadSubscription handleSubscribeEvent
INFO: CHANNEL: Start reading from java.nio.channels.SocketChannel[connected local=/100.120.31.175:57725 remote=adventofcode.com/54.166.48.177:443]
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.SocketTube$InternalWriteSubscriber startSubscription
INFO: CHANNEL: Start requesting bytes for writing to channel: java.nio.channels.SocketChannel[connected local=/100.120.31.175:57725 remote=adventofcode.com/54.166.48.177:443]
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.HttpClientImpl cancelTimer
INFO: MISC: Canceling timer ConnectTimerEvent, TimeoutEvent[id=1, duration=PT10S, deadline=2021-11-04T14:35:49.817109800Z]
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection <init>
INFO: MISC: Connection send window size 65 535 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection sendConnectionPreface
INFO: MISC: /100.120.31.175:57725: start sending connection preface to adventofcode.com/54.166.48.177:443
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection sendConnectionPreface
INFO: FRAME: OUT: SETTINGS: length=30, streamid=0, flags=0  Settings: HEADER_TABLE_SIZE=16384 ENABLE_PUSH=1 MAX_CONCURRENT_STREAMS=100 INITIAL_WINDOW_SIZE=16777216 MAX_FRAME_SIZE=16384 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection sendConnectionPreface
INFO: MISC: PREFACE_BYTES sent
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection sendConnectionPreface
INFO: MISC: Settings Frame sent
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection sendConnectionPreface
INFO: CHANNEL: Sending initial connection window update frame: 33 488 897 (33 554 432 - 65 535)
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection encodeFrame
INFO: FRAME: OUT: WINDOW_UPDATE: length=4, streamid=0, flags=0  WindowUpdate: 33488897
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection sendConnectionPreface
INFO: MISC: finished sending connection preface
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream sendHeadersAsync
INFO: REQUEST: https://adventofcode.com/2020/day/1/input GET
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection encodeHeaders
INFO: HEADERS: HEADERS FRAME (stream=1)
    :authority: adventofcode.com
    :method: GET
    :path: /2020/day/1/input
    :scheme: https
    Cookie: session=53616c7465645f5f9d467d3ae831
    User-Agent: Java-http-client/11.0.7


nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection lambda$encodeFrames
INFO: FRAME: OUT: HEADERS: length=126, streamid=1, flags=END_STREAM END_HEADERS 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream getResponseAsync
INFO: MISC: Response future (stream=1) is: jdk.internal.net.http.common.MinimalFuture@374f597e[Not completed] (id=43)
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection processFrame
INFO: FRAME: IN: SETTINGS: length=18, streamid=0, flags=0  Settings: MAX_CONCURRENT_STREAMS=128 INITIAL_WINDOW_SIZE=65536 MAX_FRAME_SIZE=16777215 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection encodeFrame
INFO: FRAME: OUT: SETTINGS: length=0, streamid=0, flags=ACK  Settings: 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection processFrame
INFO: FRAME: IN: WINDOW_UPDATE: length=4, streamid=0, flags=0  WindowUpdate: 2147418112
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection processFrame
INFO: FRAME: IN: SETTINGS: length=0, streamid=0, flags=ACK  Settings: 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection processFrame
INFO: FRAME: IN: HEADERS: length=118, streamid=1, flags=END_HEADERS 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream$HeadersConsumer onDecoded
INFO: MISC: RECEIVED HEADER (streamid=1): :status: 200
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream$HeadersConsumer onDecoded
INFO: MISC: RECEIVED HEADER (streamid=1): date: Thu, 04 Nov 2021 14:35:40 GMT
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream$HeadersConsumer onDecoded
INFO: MISC: RECEIVED HEADER (streamid=1): content-type: text/plain
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream$HeadersConsumer onDecoded
INFO: MISC: RECEIVED HEADER (streamid=1): content-length: 990
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream$HeadersConsumer onDecoded
INFO: MISC: RECEIVED HEADER (streamid=1): server: Apache
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream$HeadersConsumer onDecoded
INFO: MISC: RECEIVED HEADER (streamid=1): server-ip: 172.31.16.87
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream$HeadersConsumer onDecoded
INFO: MISC: RECEIVED HEADER (streamid=1): vary: Accept-Encoding
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream$HeadersConsumer onDecoded
INFO: MISC: RECEIVED HEADER (streamid=1): strict-transport-security: max-age=300
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream incoming
INFO: MISC: handling response (streamid=1)
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream handleResponse
INFO: HEADERS: RESPONSE HEADERS:
    :status: 200
    content-length: 990
    content-type: text/plain
    date: Thu, 04 Nov 2021 14:35:40 GMT
    server: Apache
    server-ip: 172.31.16.87
    strict-transport-security: max-age=300
    vary: Accept-Encoding

nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream completeResponse
INFO: MISC: Completing response (streamid=1): jdk.internal.net.http.common.MinimalFuture@113d2103[Not completed, 1 dependents] (id=42)
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Exchange lambda$wrapForLog
INFO: RESPONSE: (GET https://adventofcode.com/2020/day/1/input) 200 HTTP_2 Local port:  57725
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.MultiExchange responseFilters
INFO: MISC: Applying response filters
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.MultiExchange responseFilters
INFO: MISC: Applying jdk.internal.net.http.CookieFilter@436813f3
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.CookieFilter response
INFO: MISC: Response: processing cookies for https://adventofcode.com/2020/day/1/input
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.CookieFilter response
INFO: MISC: Response: parsing cookies from {:status=[200], content-length=[990], content-type=[text/plain], date=[Thu, 04 Nov 2021 14:35:40 GMT], server=[Apache], server-ip=[172.31.16.87], strict-transport-security=[max-age=300], vary=[Accept-Encoding]}
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.MultiExchange responseFilters
INFO: MISC: Applying jdk.internal.net.http.RedirectFilter@8f4ea7c
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.MultiExchange responseFilters
INFO: MISC: Applying jdk.internal.net.http.AuthenticationFilter@49ec71f8
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.MultiExchange responseFilters
INFO: MISC: All filters applied
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream readBodyAsync
INFO: MISC: Reading body on stream 1
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection processFrame
INFO: FRAME: IN: DATA: length=990, streamid=1, flags=0 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream schedule
INFO: MISC: responseSubscriber.onNext 990
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Http2Connection processFrame
INFO: FRAME: IN: DATA: length=0, streamid=1, flags=END_STREAM 
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream schedule
INFO: MISC: responseSubscriber.onComplete
1337
1906
2007
1939
818
1556
2005
1722
1484
1381
1682
1253
1967
1718
2002
1398
1439
1689
1746
1979
1985
1387
1509
1566
1276
1625
1853
882
1750
1390
1731
1555
1860
1675
1457
1554
1506
1639
1543
1849
1062
1869
1769
1858
1916
1504
1747
1925
1275
1273
1383
1816
1814
1481
1649
1993
1759
1949
1499
1374
1613
1424
783
1765
1576
1933
1270
1844
1856
1634
1261
1293
1741
668
1573
1599
1877
1474
1918
476
1515
1029
202
1589
1867
1503
1582
1605
1557
587
1462
1955
1806
1834
1739
1343
1594
1622
1972
1527
1798
1719
1866
134
2000
1992
1966
1909
1340
1621
1921
1256
1365
1314
1748
1963
1379
1627
1848
1977
1917
1826
1716
1631
1404
1936
1677
1661
1986
1997
1603
1932
1780
1902
2009
1257
1871
1362
1662
1507
1255
1539
1962
1886
1513
1264
1873
1700
807
1426
1697
1698
1519
1791
1240
1542
1497
1761
1640
1502
1770
1437
1333
1805
1591
1644
1420
1809
1587
1421
1540
1942
470
1940
1831
1247
1632
1975
1774
1919
1829
1944
1553
1361
1483
1995
1868
1601
1552
1854
1490
1855
1987
1538
1389
1454
1427
1686
1456
1974

nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream close
INFO: MISC: Closing stream 1
nov. 04, 2021 3:35:40 EM jdk.internal.net.http.Stream close
INFO: MISC: Stream 1 closed

Process finished with exit code 0

ps:弄清楚这个问题非常有趣 :D

Java 11 HttpClient in-built 支持 Session-Cookie。如果您像这样创建一个客户端:

var client = HttpClient.newBuilder()
                .cookieHandler(new CookieManager())
                .version(HttpClient.Version.HTTP_2)
                .build();

然后客户端会自动保存第一次授权请求中的所有cookies。此客户端发出的任何后续请求都将设置 cookie。

如果您想在发送初始请求之前为 cookie 设置一些值,请尝试:

var cookieManager = new cookieManager();
cookieManager.getCookieStore().add(someHostURI, someSessionCookie);

//Now this cookie can be used to create the client:
var client = HttpClient.newBuilder()
                .cookieHandler(cookieManager)
                .version(HttpClient.Version.HTTP_2)
                .build();

有关更详细的说明,请查看 this article