我在聊天应用程序中使用基于Cookie的令牌身份验证时遇到问题.我正在使用带有标准Net库的GO后端来向响应Cookie添加令牌.当用户通过密码验证时(通过发布到身份验证服务器上的/LOGIN路径),响应Cookie应该包含用于生成API令牌的访问令牌和用于重新生成访问令牌的刷新令牌.

下面是一个标记文件,其中包含我的开发环境中的应用程序服务的 struct .每台服务器都使用本地主机上的Go Net/http在顺序端口上运行(未显示无关的服务).


auth_server (
    dependencies []
    url (scheme "http" domain "localhost" port "8081")
    listenAddress ":8081"
    endpoints (
        /jwtkeypub (
            methods [GET]
        )
        /register (
            methods [POST]
        )
        /logout (
            methods [POST]
        )
        /login (
            methods [POST]
        )
        /apitokens (
            methods [GET]
        )
        /accesstokens (
            methods [GET]
        )
    )
    jwtInfo (
        issuerName "auth_server"
        audienceName "auth_server"
    )
)

message_server (
    dependencies [auth_server]
    url (scheme "http" domain "localhost" port "8083")
    listenAddress ":8083"
    endpoints (
        /ws (
            methods [GET]
        )
    )
    jwtInfo (
        audienceName "message_server"
    )
)

static (
    dependencies [auth_server, message_server]
    url (scheme "http" domain "localhost" port "8080")
    listenAddress ":8080"
)

这是在登录时设置Cookie的代码.这发生在密码判断之后

    // Set a new refresh token
    refreshToken := s.jwtIssuer.StringifyJwt(
        s.jwtIssuer.MintToken(userId, s.jwtIssuer.Name, RefreshTokenTTL),
    )
    kit.SetHttpOnlyCookie(w, "refreshToken", refreshToken, int(RefreshTokenTTL.Seconds()))

    // set a new access token
    accessToken := s.jwtIssuer.StringifyJwt(
        s.jwtIssuer.MintToken(userId, s.jwtAudience.Name, AccessTokenTTL),
    )
    kit.SetHttpOnlyCookie(w, "accessToken", accessToken, int(AccessTokenTTL.Seconds()))
}

func SetHttpOnlyCookie(w http.ResponseWriter, name, value string, maxAge int) {
    http.SetCookie(w, &http.Cookie{
        Name:     name,
        Value:    value,
        HttpOnly: true,
        MaxAge:   maxAge,
    })
}

下面是当用户请求API令牌时我访问Cookie的方式.处理程序调用GetTokenFromCookie()函数,如果返回错误,则返回401响应.错误是这种情况是"http:命名的Cookie不存在"


func GetHttpCookie(r *http.Request, name string) (*http.Cookie, error) {
    return r.Cookie(name)
}

func GetTokenFromCookie(r *http.Request, name string) (jwt.Jwt, error) {
    tokenCookie, err := GetHttpCookie(r, name)
    if err != nil {
        // DEBUG
        log.Println(err)
        return jwt.Jwt{}, err
    }

    return jwt.FromString(tokenCookie.Value)
}

在来自登录终结点的200响应之后,页面重定向到应用程序主页.在此页面上,向身份验证服务器发出请求,以接收用于连接实时聊天消息服务器的API令牌.正如您从身份验证服务器上的日志(log)输出中看到的,请求中没有收到访问令牌Cookie,因此请求返回401代码.

2023/05/19 02:33:57 GET [/jwtkeypub] - 200
2023/05/19 02:33:57 GET [/jwtkeypub] - 200
2023/05/19 02:34:23 POST [/login] - 200
2023/05/19 02:34:23 http: named cookie not present
{{ } {    } []} http: named cookie not present
2023/05/19 02:34:23 GET [/apitokens?aud=MSGSERVICE] - 401

我认为问题出在我使用的是本地主机,而浏览器没有将cookie从本地主机:8080传输到本地主机:8081.我计划实现某种模拟身份验证,以避免读取开发环境的cookie来解决这个问题,但我不确定这是否是我的问题的真正原因.我只是想再看一眼,看看我能不能让它在不需要这样做的情况下工作.

更新:我已经查看了开发工具中的网络选项卡: 图像显示登录后的响应返回Cookie,但它们随后不会发送到端口8081上的身份验证服务器.我还查看了Cookie存储,在从登录获得200响应后,即使在响应中收到它们,也没有Cookie存在.我在私有模式下使用Firefox访问网站.请注意,即使我在GO代码中设置了MaxAge,Cookie中也不包含MaxAge,这似乎是一个问题.

更新:这是登录后的HAR文件.您可以看到响应具有Max-Age,但之后它不会显示在Cookie选项卡中.

{
  "log": {
    "version": "1.2",
    "creator": {
      "name": "Firefox",
      "version": "113.0.1"
    },
    "browser": {
      "name": "Firefox",
      "version": "113.0.1"
    },
    "pages": [
      {
        "startedDateTime": "2023-05-19T12:16:37.081-04:00",
        "id": "page_1",
        "title": "Login Page",
        "pageTimings": {
          "onContentLoad": -8105,
          "onLoad": -8077
        }
      }
    ],
    "entries": [
      {
        "pageref": "page_1",
        "startedDateTime": "2023-05-19T12:16:37.081-04:00",
        "request": {
          "bodySize": 31,
          "method": "POST",
          "url": "http://0.0.0.0:8081/login",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "Host",
              "value": "0.0.0.0:8081"
            },
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0"
            },
            {
              "name": "Accept",
              "value": "*/*"
            },
            {
              "name": "Accept-Language",
              "value": "en-US,en;q=0.5"
            },
            {
              "name": "Accept-Encoding",
              "value": "gzip, deflate"
            },
            {
              "name": "Referer",
              "value": "http://localhost:8080/"
            },
            {
              "name": "Content-Type",
              "value": "text/plain;charset=UTF-8"
            },
            {
              "name": "Content-Length",
              "value": "31"
            },
            {
              "name": "Origin",
              "value": "http://localhost:8080"
            },
            {
              "name": "DNT",
              "value": "1"
            },
            {
              "name": "Connection",
              "value": "keep-alive"
            }
          ],
          "cookies": [],
          "queryString": [],
          "headersSize": 370,
          "postData": {
            "mimeType": "text/plain;charset=UTF-8",
            "params": [],
            "text": "{\"username\":\"a\",\"password\":\"a\"}"
          }
        },
        "response": {
          "status": 200,
          "statusText": "OK",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "Access-Control-Allow-Origin",
              "value": "*"
            },
            {
              "name": "Set-Cookie",
              "value": "refreshToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQMmY2RHg1RWxlYTF5THBUaVpEejBaS3Z1dk1FUkFPZEtBVGkwNDZSc2JNPSIsImF1ZCI6InN0ZWVsaXgiLCJpc3MiOiJzdGVlbGl4IiwiZXhwIjoiMTY4NTExNzc5NyIsImp0aSI6IjIwMUQzODZDNTRBQzlEOUMwRjdCODFBMDVDNDlFQTE1In0.SbxFgEAtZbh0zS-SXZmrVW9iLk-cFz6HcDMU0FHNl-K9BwCeb_boc5igEgImMSYK-NBVQZh1km7YknE-jkBWyF0rIYjSnTzjNUHHwMnn0jE1N-dtEfNRnF1OT0R2bxPSz8gmhtJ3B839xa-jh9uMPMkXEB8BYtABgPH1FqBdijHPUtRVKq6C3ulVleurp2eyF8EHpGLc9rr5wBYSFBk0HQ3FNjjUxfRQLDnzl2xYovoQ2em4grExnkdACxCSpXNtF5bQ7lCnEZyf7-CehrRNwZCpteGKj5ux_wrX_nxma3OEWwrlatML_j-e420TM1tub0C9Ymyt0bMugHw8vaiOGA; Max-Age=604800; HttpOnly"
            },
            {
              "name": "Set-Cookie",
              "value": "accessToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQMmY2RHg1RWxlYTF5THBUaVpEejBaS3Z1dk1FUkFPZEtBVGkwNDZSc2JNPSIsImF1ZCI6InN0ZWVsaXgiLCJpc3MiOiJzdGVlbGl4IiwiZXhwIjoiMTY4NDUxNDE5NyIsImp0aSI6IjY2NjU1QjAyNTc4NkRBRTE1M0VDNDI3MzBGMjMxQ0FGIn0.cIs6KGjRGTHaWX_uFTts_V2a3YcBb7LA0jNOBTZeyDmpPQgRlcABnuYkWUIdjUdR6VYnDitFRV-XK2ZSq6Pk_ZgyfvJ3yRzvWGYjXMu7Nq7MLpVvUh9mLKSbKvlqunW6YVamHSCAbYS8-D_pY9fpWxIcXw0qbwA2XfTdzr0Mrw7ntrkdyK7O1QqWamnEHCmpLfJ2XJlQsU0KaD8FjkL76pO3lWmrca3VYnTmjP1Oo1HEhbK3nImtrNeL2khAyb8ns8ROj2HX41IDNK1aHWPfn9J04pgH3AfBfcwhhqZkrKjTVFQAkSYzuvjKPWOfpgYmBMw3Y5nG_PDf-zlvVPrdpQ; Max-Age=1200; HttpOnly"
            },
            {
              "name": "Date",
              "value": "Fri, 19 May 2023 16:16:37 GMT"
            },
            {
              "name": "Content-Length",
              "value": "0"
            }
          ],
          "cookies": [
            {
              "name": "refreshToken",
              "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQMmY2RHg1RWxlYTF5THBUaVpEejBaS3Z1dk1FUkFPZEtBVGkwNDZSc2JNPSIsImF1ZCI6InN0ZWVsaXgiLCJpc3MiOiJzdGVlbGl4IiwiZXhwIjoiMTY4NTExNzc5NyIsImp0aSI6IjIwMUQzODZDNTRBQzlEOUMwRjdCODFBMDVDNDlFQTE1In0.SbxFgEAtZbh0zS-SXZmrVW9iLk-cFz6HcDMU0FHNl-K9BwCeb_boc5igEgImMSYK-NBVQZh1km7YknE-jkBWyF0rIYjSnTzjNUHHwMnn0jE1N-dtEfNRnF1OT0R2bxPSz8gmhtJ3B839xa-jh9uMPMkXEB8BYtABgPH1FqBdijHPUtRVKq6C3ulVleurp2eyF8EHpGLc9rr5wBYSFBk0HQ3FNjjUxfRQLDnzl2xYovoQ2em4grExnkdACxCSpXNtF5bQ7lCnEZyf7-CehrRNwZCpteGKj5ux_wrX_nxma3OEWwrlatML_j-e420TM1tub0C9Ymyt0bMugHw8vaiOGA"
            },
            {
              "name": "accessToken",
              "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJQMmY2RHg1RWxlYTF5THBUaVpEejBaS3Z1dk1FUkFPZEtBVGkwNDZSc2JNPSIsImF1ZCI6InN0ZWVsaXgiLCJpc3MiOiJzdGVlbGl4IiwiZXhwIjoiMTY4NDUxNDE5NyIsImp0aSI6IjY2NjU1QjAyNTc4NkRBRTE1M0VDNDI3MzBGMjMxQ0FGIn0.cIs6KGjRGTHaWX_uFTts_V2a3YcBb7LA0jNOBTZeyDmpPQgRlcABnuYkWUIdjUdR6VYnDitFRV-XK2ZSq6Pk_ZgyfvJ3yRzvWGYjXMu7Nq7MLpVvUh9mLKSbKvlqunW6YVamHSCAbYS8-D_pY9fpWxIcXw0qbwA2XfTdzr0Mrw7ntrkdyK7O1QqWamnEHCmpLfJ2XJlQsU0KaD8FjkL76pO3lWmrca3VYnTmjP1Oo1HEhbK3nImtrNeL2khAyb8ns8ROj2HX41IDNK1aHWPfn9J04pgH3AfBfcwhhqZkrKjTVFQAkSYzuvjKPWOfpgYmBMw3Y5nG_PDf-zlvVPrdpQ"
            }
          ],
          "content": {
            "mimeType": "text/plain",
            "size": 0,
            "text": ""
          },
          "redirectURL": "",
          "headersSize": 1347,
          "bodySize": 1748
        },
        "cache": {},
        "timings": {
          "blocked": 0,
          "dns": 0,
          "connect": 0,
          "ssl": 0,
          "send": 0,
          "wait": 13,
          "receive": 0
        },
        "time": 13,
        "_securityState": "insecure",
        "serverIPAddress": "0.0.0.0",
        "connection": "8081"
      }
    ]
  }
}

enter image description here

The response appears to have the cookies, but they don't get saved. enter image description here

enter image description here

并且对身份验证服务器的下一个请求没有添加任何Cookie.

enter image description here

推荐答案

TL;DR:

  1. Cookie不会在0.0.0.0localhost之间共享.
  2. 会话Cookie和普通Cookie都可以在http://localhost:8080http://localhost:8081之间共享.
  3. 从第http://localhost:8080/页发送到第http://localhost:8081/页的请求被视为跨域请求.
  4. fetch发出的跨域请求需要初始化为credentials: 'include',以使浏览器保存cookie.

HAR显示网页的URL是http://localhost:8080/,但登录端点是http://0.0.0.0:8081/login.0.0.0.0的Cookie不会与localhost共享.

您可以运行下面的演示来观察其行为:

  1. 运行演示:go run main.go

  2. 在浏览器中打开http://localhost:8080/.该网页将执行以下操作:

    1. http://0.0.0.0:8081/login1发送请求(目的是验证0.0.0.0的cookie不会与localhost共享;
    2. 它向http://localhost:8081/login2发送请求(目的是验证会话cookie将在http://localhost:8080http://localhost:8081之间共享;
    3. http://localhost:8081/login3发送请求(目的是验证http://localhost:8080http://localhost:8081之间是否共享正常的Cookie;
    4. 它导航到http://localhost:8080/resource,服务器将转储该请求.它显示此标头已发送到服务器:Cookie: login2=localhost-session; login3=localhost.

Notes:credentials: 'include'要求将Access-Control-Allow-Origin报头设置为确切的原点(这意味着*将被拒绝),并将Access-Control-Allow-Credentials报头设置为true.

package main

import (
    "fmt"
    "log"
    "net/http"
    "net/http/httputil"
)

func setHeader(w http.ResponseWriter, cookieName, cookieValue string, maxAge int) {
    w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8080")
    w.Header().Set("Access-Control-Allow-Credentials", "true")
    http.SetCookie(w, &http.Cookie{
        Name:     cookieName,
        Value:    cookieValue,
        MaxAge:   maxAge,
        HttpOnly: true,
    })
}

func main() {
    muxWeb := http.NewServeMux()
    // serve the HTML page.
    muxWeb.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        _, err := w.Write([]byte(page))
        if err != nil {
            panic(err)
        }
    }))
    // Dump the request to see what cookies is sent to the server.
    muxWeb.Handle("/resource", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        dump, err := httputil.DumpRequest(r, false)
        if err != nil {
            panic(err)
        }
        _, _ = w.Write(dump)
    }))
    web := &http.Server{
        Addr:    ":8080",
        Handler: muxWeb,
    }
    go func() {
        log.Fatal(web.ListenAndServe())
    }()

    muxAPI := http.NewServeMux()
    muxAPI.Handle("/login1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        setHeader(w, "login1", "0.0.0.0", 1200)
    }))
    muxAPI.Handle("/login2", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        setHeader(w, "login2", "localhost-session", 0)
    }))
    muxAPI.Handle("/login3", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        setHeader(w, "login3", "localhost", 1200)
    }))
    api := &http.Server{
        Addr:    ":8081",
        Handler: muxAPI,
    }
    go func() {
        log.Fatal(api.ListenAndServe())
    }()

    fmt.Println("Open http://localhost:8080/ in the browser")

    select {}
}

var page string = `
<!DOCTYPE html>
<html>
  <body>
    <script type="module">
      async function login(url) {
        const response = await fetch(url, {
          mode: 'cors',
          credentials: 'include',
        });
      }
      await login('http://0.0.0.0:8081/login1');
      await login('http://localhost:8081/login2');
      await login('http://localhost:8081/login3');

      window.location = '/resource';
    </script>
  </body>
</html>
`

Go相关问答推荐

golang 的通用 map 功能

使用一元或服务器流将切片从GRPC服务器返回到客户端

创建服务时云运行触发器执行失败

Go:拆分一个由逗号分隔的键/值对字符串,并在给定的键/价值对中嵌入可能的逗号

在golang中以JSON格式获取xwwwformurlencoded请求的嵌套键值对

使用 Go Colly 抓取所有可能的标签并将它们放入一个变量中

当客户端同时是服务器时,需要什么 mTLS 证书?

MQTT 客户端没有收到另一个客户端发送的消息

数据流中的无根单元错误,从 Golang 中的 PubSub 到 Bigquery

来自洪流公告的奇怪同行字段

GRPC 元数据未在 Go 中更新

如何使用 Go 获取 X11 中的窗口列表

如何使用特定的 Go 版本运行 govulncheck?

try 与 golang testify/suite 并行运行测试失败

使用 xml.Name 将 xml 解组为 [] struct

如何从字符串中删除多个换行符`\n`但只保留一个?

不理解切片和指针

Scanner.Buffer - 最大值对自定义拆分没有影响?

如何动态解析 Go Fiber 中的请求正文?

空接口与泛型接口有何不同?