如何修复 Golang 中的“http: named cookie not present”错误?

8
我正在开发一个小型的晚餐/计划管理应用程序(使用微服务),面向我认识的一些人。目的是每个人都可以登录到自己的帐户,然后使用承载令牌(JWT)对其他服务进行身份验证。
这个承载令牌存储在cookie中。但是,当我尝试检索它时,无法找到已设置的cookie。
最终导致错误。
http: named cookie not present

为什么请求的响应体为空?为什么我的GET请求没有发送任何cookie?我该如何解决这个问题?

我在网上搜索了一下,并尝试了以下方法:

  • Net/http cookie: 这似乎是最简单的实现方法,也是我在这里展示的方法。看起来这个简单的例子应该可以工作。

  • Cookiejar implementation: 我尝试使用cookiejar实现来设置和检索浏览器和postman中的cookies,但结果相同。我使用的cookiejar实现在https://golang.org/pkg/net/http/cookiejar/?m=all#New中描述。

  • Setting to specific URL and extra GET request: 我尝试将cookie放置在域内不同的特定URL上。在某些情况下,似乎只能从某个特定的绝对URL中检索到cookie,但事实并非如此。

  • httputil DumpRequestOut: 我发现net/http的utility包中有一个名为DumpRequestOut的函数,这个函数可能能够从请求中提取正文,但这也是空的。

  • Setting cookie 'secure' flag to false: 我发现有一个建议,即安全标志使得cookie无法读取。不幸的是,更改安全标志没有效果。


Postman清楚地显示了Cookie确实存在。我的浏览器(火狐)也显示Cookie存在,但它们被赋予了相当抽象的名称。 可以在https://www.getpostman.com/collections/fccea5d5dc22e7107664找到Postman请求。

如果我尝试使用golang的"net/http"包检索Cookie,则响应正文为空。

在验证了用户/密码组合后,我设置会话令牌并直接重定向客户端。

// SetTokenAndRedirect sets an access token to the cookies
func SetTokenAndRedirect(w http.ResponseWriter, r *http.Request, db *mgo.Session, u *user.User, redirectURL string) *handler.AppError {
    // Generate a unique ID for the session token.
    tokenID := uuid.Must(uuid.NewV4()).String()
    //set the expiration time (found in config.config.go)
    expirationTime := time.Now().Add(config.ExpireTime)
    // Set the cookie with the JWT
    http.SetCookie(w, &http.Cookie{
        Name:     config.AccessTokenName,
        Value:    createToken(u.UserID, expirationTime, tokenID, r.Header.Get("User-Agent")),
        Expires:  expirationTime,
        HttpOnly: true,
        Secure:   false,
    })

    // Redirects user to provided redirect URL
    if redirectURL == "" {
        return handler.AppErrorf(417, nil, "No redirect URL has been provided")
    }
    http.Redirect(w, r, redirectURL, 200)
    return nil
}

我尝试按以下方式验证传入的请求和JWT令牌。

// All handlers will have this adapted serveHTTP function 
func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := Authorize(w, r); err != nil {
        http.Error(w, fmt.Sprintf("Not Authorized: %v", err), http.StatusUnauthorized)
        return
    }
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        log.Printf("Handler error: status code: %d, message: %s, underlying err: %#v",
            e.Code, e.Message, e.Error)

        http.Error(w, e.Message, e.Code)
    }
}

// Claims defines what will be stored in a JWT access token
type Claims struct {
    ProgramVersion string `json:"programVersion"`
    UserAgent      string `json:"userAgent"`
    jwt.StandardClaims
}

// Authorize checks if the jwt token is valid
func Authorize(w http.ResponseWriter, r *http.Request) error {
    c, err := r.Cookie("access_token")
    if err != nil {
        if err == http.ErrNoCookie {
            // The program returns this error
            return err
        }
        return err
    }

    tokenString := c.Value

    claim := &Claims{}

    tkn, err := jwt.ParseWithClaims(tokenString, claim, func(tkn *jwt.Token) (interface{}, error) {
        return config.JwtSigningSecret, nil
    })
    if !tkn.Valid {
        return err
    }
    if err != nil {
        if err == jwt.ErrSignatureInvalid {
            return err
        }
        return err
    }

    // JWT token is valid
    return nil
}


在设置cookie时,请求的结构如下所示。
// Pretty printed version
Host: localhost:8080
content-type: application/json
user-agent: PostmanRuntime/7.11.0
cache-control: no-cache
accept-encoding: gzip, deflate
content-length: 68
connection: keep-alive
accept: */*
postman-token: 36268859-a342-4630-9fb4-c286f76d868b
cookie: access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9ncmFtVmVyc2lvbiI6IjEuMC4wIiwidXNlckFnZW50IjoiUG9zdG1hblJ1bnRpbWUvNy4xMS4wIiwiZXhwIjoxNTU2MjA0MTg3LCJqdGkiOiJlZDlmMThhZi01NTAwLTQ0YTEtYmRkZi02M2E4YWVhM2M0ZDEiLCJpYXQiOjE1NTYyMDM1ODcsImlzcyI6ImdrLmp3dC5wcm9maWxlU2VydmljZS5hIn0.bssnjTZ8woKwIncdz_EOwYbCtt9t6V-7PmLxfq7GVyo

// Raw Version
&{POST /auth/users/login?redirect=/ HTTP/1.1 1 1 map[Cache-Control:[no-cache] Postman-Token:[d33a093e-c7ab-4eba-8c1e-914e85a0d289] Cookie:[access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9ncmFtVmVyc2lvbiI6IjEuMC4wIiwidXNlckFnZW50IjoiUG9zdG1hblJ1bnRpbWUvNy4xMS4wIiwiZXhwIjoxNTU2MjA0NDU4LCJqdGkiOiIzOTk1MmI1NS0yOWQzLTQ4NGQtODhhNC1iMDlhYmI1OWEyNzgiLCJpYXQiOjE1NTYyMDM4NTgsImlzcyI6ImdrLmp3dC5wcm9maWxlU2VydmljZS5hIn0.DFA7KBET3C2q1A9N1hXGMT0QbabHgaVcDBpAYpBdbi8] Accept-Encoding:[gzip, deflate] Connection:[keep-alive] Content-Type:[application/json] User-Agent:[PostmanRuntime/7.11.0] Accept:[*/*] Content-Length:[68]] 0xc0001ba140 <nil> 68 [] false localhost:8080 map[redirect:[/]] map[] <nil> map[] [::1]:36584 /auth/users/login?redirect=/ <nil> <nil> <nil> 0xc00016a2a0}

在获取 cookie 时,请求的结构如下:
// Pretty printed version
Host: localhost:8080
cache-control: no-cache
postman-token: 20f7584f-b59d-46d8-b50f-7040d9d40062
accept-encoding: gzip, deflate
connection: keep-alive
user-agent: PostmanRuntime/7.11.0
accept: */*

// Raw version
2019/04/25 12:22:56 &{GET /path/provide HTTP/1.1 1 1 map[User-Agent:[PostmanRuntime/7.11.0] Accept:[*/*] Cache-Control:[no-cache] Postman-Token:[b79a73a3-3e08-48a4-b350-6bde4ac38d23] Accept-Encoding:[gzip, deflate] Connection:[keep-alive]] {} <nil> 0 [] false localhost:8080 map[] map[] <nil> map[] [::1]:35884 /path/provide <nil> <nil> <nil> 0xc000138240}

在设置cookie时,响应的结构如下所示。
response Headers: map[Location:[/] Set-Cookie:[access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9ncmFtVmVyc2lvbiI6IjEuMC4wIiwidXNlckFnZW50IjoiR28taHR0cC1jbGllbnQvMS4xIiwiZXhwIjoxNTU2MjI4ODIyLCJqdGkiOiJlY2Q2NWRkZi1jZjViLTQ4N2YtYTNkYy00NmM3N2IyMmUzMWUiLCJpYXQiOjE1NTYyMjgyMjIsImlzcyI6ImdrLmp3dC5wcm9maWxlU2VydmljZS5hIn0.0sOvEzQS2gczjWSmtVSD_u0qMV2L7M4hKF1KUM08-bQ; Expires=Thu, 25 Apr 2019 21:47:02 GMT; HttpOnly] Date:[Thu, 25 Apr 2019 21:37:02 GMT] Content-Length:[0]]

我希望Authorize函数会返回nil。另外,如果我添加以下代码,我期望会有一些cookie存在。
for _, cookie := range r.Cookies() {
    fmt.Fprint(w, cookie.Name)
}

然而,Authorize函数返回标题中的错误,printf不会打印出任何cookie。

@mkopriva,还要感谢您的快速回复,我已经添加了设置cookie的代码。 - Abe Brandsma
@AbeBrandsma 邮递员的请求是无关紧要的,重要的是具有 Set-Cookie 头的 http 响应,这告诉浏览器存储 cookie 并在后续请求中将其发送到同一域。 - mkopriva
@AbeBrandsma,重定向代码也在3xx范围内。使用http.Redirect(w, r, redirectURL, 200)是错误的。如果您查看文档,您会发现它说:“提供的代码应该在3xx范围内,并且通常为StatusMovedPermanently、StatusFound或StatusSeeOther。”。 - mkopriva
@NoamHacker,您指的是哪个请求?据我所知,在请求时不必在标头中包含cookie的名称,但我可能是错的。 - Abe Brandsma
1
例如,@AbeBrandsma曾经遇到过一个问题,即Postman在后续请求中未发送cookie(请参见链接)。因此,请确保您使用的Postman版本没有此问题。同时,请确保任何与cookie相关的Postman设置已正确设置。此外,在您的本地主机上进行测试时,请确保将“Secure”标志设置为“false”,值为“true”仅会通过“https”发送cookie,而在大多数情况下,localhost不使用该协议。 - mkopriva
显示剩余11条评论
3个回答

2

请记得设置 cookie 的路径:

    c := &http.Cookie{Name: name, Value: encode(value), Path: "/"}
    http.SetCookie(w, c)

如果您不指定路径,那么该Cookie的路径将是当前请求的路径,因此该Cookie仅可在此路径下使用。

0
当我在Postman中发送/login的POST请求时,后端成功设置了cookie和token cookie,并随后的请求一起发送。但是当我尝试使用React应用程序的端点时,cookie没有正确设置并在随后的请求中发送。问题在于启用cors时,我们需要在/login端点(设置cookie的端点)和随后的请求期间传递credentials: include以设置cookie并将其发送回服务器。我正在使用需要显式提及credentials: include的fetch api,如果启用cors
// my login request looks like
export async function login(credentials: Credentials) {
  const userRes = await fetch(
    `${process.env.REACT_APP_SERVER_ENDPOINT}/login`,
    {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(credentials),
    }
  )
  return await userRes.json()
}
// subsequent requests
export async function getCurrentUser() {
  const userRes = await fetch(
    `${process.env.REACT_APP_SERVER_ENDPOINT}/userinfo`,
    {
      method: 'GET',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
      },
    }
  )

  return await userRes.json()
}

在后端,我们需要指定来源,因为使用credentials: 'include'时不接受允许所有来源。一些解决此问题的解决方案建议禁用HttpOnly和Secure。这是我从阅读与HttpOnly和Secure相关的文档中得出的笔记。对于我的问题,它们都没有对问题产生影响。
HttpOnly-确保cookie无法被JavaScript访问。
Secure-仅通过https或本地主机发送cookie。

-1

您正在使用错误的名称查找 cookie。 config.AccessTokenName == "access_token" 吗?(我认为不是)。但是您正在尝试使用 cookie 名称 access_token 获取 cookie。

这是一个工作示例代码,在 POSTMAN 和 Web 浏览器上都经过测试。

package main

import (
    "log"
    "net/http"
    "time"
)

func main() {

    http.HandleFunc("/set", SetTokenAndRedirect)

    http.HandleFunc("/get", getToken)

    http.ListenAndServe(":8090", nil)

}

func SetTokenAndRedirect(w http.ResponseWriter, r 
*http.Request) {

    expirationTime := time.Now().Add(time.Hour)
// Set the cookie with the JWT
    http.SetCookie(w, &http.Cookie{
        Name:     "access_token", // you have to search 
the cookie by this name
        Value:    "12378",
        Expires:  expirationTime,
        HttpOnly: true,
        Secure:   false,
    })

    redirectURL := "/"

    http.Redirect(w, r, redirectURL, 200)

}

func getToken(w http.ResponseWriter, r *http.Request) {
    c, err := r.Cookie("access_token")

    if err != nil {
        if err == http.ErrNoCookie {
            log.Println("Error finding cookie: ", err)
        }
        //handle the error gracefully
        log.Fatal(err)
    }

    tokenString := c.Value

    log.Println("Cookie found: ", tokenString)

}

如需进一步实验或需要:在编写任何其他内容之前设置标题(包括 cookie)。

可以在这里找到一个很好的讨论。


感谢您抽出时间回复我的问题。很抱歉我造成了一些混淆。实际上,我正在搜索“access_token”并设置“access_token”,我只是为了自己的健全性检查而暂时更改了它。您发送的讨论确实有所帮助,我可能会在设置Cookie之前编写一些响应内容。 - Abe Brandsma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接