我在聊天应用程序中遇到基于 cookie 的令牌身份验证问题。我正在使用带有标准网络库的 go 后端来将令牌添加到响应 cookie。当用户通过密码验证(通过 post 到身份验证服务器上的 /login 路径)时,响应 cookie 应包含用于生成 api 令牌的访问令牌和用于重新生成访问令牌的刷新令牌。
这是一个标记文件,其中包含我的开发环境中应用程序服务的结构。每个服务器都在本地主机上使用 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 令牌。从auth服务器上的日志输出可以看到,请求中没有收到访问令牌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
我相信问题在于我使用的是 localhost,并且浏览器不会将 cookie 从 locahost:8080 传输到 localhost:8081。我正计划实现某种模拟身份验证,绕过读取开发环境的 cookie 来解决这个问题,但我不确定这是否真的是我的问题的原因。只是想再看一下,看看我是否可以让它工作而不需要这样做。
更新:我已经查看了开发工具中的网络选项卡: 图像显示登录后的响应返回了 cookie,但它们随后不会发送到端口 8081 上的身份验证服务器。在获得登录的 200 响应后,我还查看了 cookie 存储,即使在之后也没有 cookie在响应中接收它们。我正在使用 firefox 的私人模式来访问该网站。请注意,即使我在 go 代码中设置了 maxage,cookie 也不包含 maxage,这似乎是一个问题。
更新:这是登录后的 har 文件。您可以看到响应有 max-age,但之后它不会显示在 cookies 选项卡中。
{
"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"
}
]
}
}
响应似乎有 cookie,但它们没有被保存。
并且对身份验证服务器的下一个请求没有添加任何 cookie。
正确答案
tl;dr:
- cookie 不会在
0.0.0.0
和localhost
之间共享。 - 会话 cookie 和普通 cookie 都可以在
http://localhost:8080
和http://localhost:8081
之间共享。 - 从
http://localhost:8080/
页面发送到http://localhost:8081/
的请求将被视为跨域请求。 fetch
发送的跨域请求应使用credentials: 'include'
进行初始化,以使浏览器保存 cookie。
har显示网页的url为http://localhost:8080/
,但登录端点为http://0.0.0.0:8081/login
。 0.0.0.0
的 cookie 不会与 localhost
共享。
您可以运行下面的演示来观察行为:
运行演示:
go run main.go
;在浏览器中打开
http://localhost:8080/
。该网页将执行以下操作:- 它向
http://0.0.0.0:8081/login1
发送请求(目的是验证0.0.0.0
的cookie不会与localhost
共享; - 它向
http://localhost:8081/login2
发送请求(目的是验证会话 cookie 将在http://localhost:8080
和http://localhost:8081
之间共享; - 它向
http://localhost:8081/login3
发送请求(目的是验证正常的cookie将在http://localhost:8080
和http://localhost:8081
之间共享; - 它导航到
http://localhost:8080/resource
并且服务器将转储请求。表明这个头被发送到服务器:cookie:login2=localhost-session; login3=localhost
。
- 它向
注释: 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>
`