浏览器本地主机中未设置 HTTPOnly Cookie

3 cookies httponly go express svelte

问题

我有一个带有登录端点的 REST API。登录端点接受用户名和密码,服务器通过发送包含一些有效负载(如 JWT)的 HTTPOnly Cookie 进行响应。

我一直使用的方法已经工作了几年,直到上周Set-Cookie标题停止工作。在 REST API 失去功能之前,我没有接触过它的源代码,因为我当时正在开发基于 Svelte 的前端。

我怀疑这与本地主机中Secure设置的属性有关。false然而,根据使用HTTP cookies,只要是本地主机,不安全的连接就应该没问题。我以这种方式开发 REST API 一段时间了,很惊讶地发现 cookie 不再被设置。

使用 Postman 测试 API 会产生设置 cookie 的预期结果。

使用的方法

我尝试重新创建真实 API 的一般流程,并将其精简为核心要素。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/golang-jwt/jwt/v4"
)

const idleTimeout = 5 * time.Second

func main() {
    app := fiber.New(fiber.Config{
        IdleTimeout: idleTimeout,
    })

    app.Use(cors.New(cors.Config{
        AllowOrigins:     "*",
        AllowHeaders:     "Origin, Content-Type, Accept, Range",
        AllowCredentials: true,
        AllowMethods:     "GET,POST,HEAD,DELETE,PUT",
        ExposeHeaders:    "X-Total-Count, Content-Range",
    }))

    app.Get("/", hello)
    app.Post("/login", login)

    go func() {
        if err := app.Listen("0.0.0.0:8080"); err != nil {
            log.Panic(err)
        }
    }()

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)

    _ = <-c
    fmt.Println("\n\nShutting down server...")
    _ = app.Shutdown()
}

func hello(c *fiber.Ctx) error {
    return c.SendString("Hello, World!")
}

func login(c *fiber.Ctx) error {
    type LoginInput struct {
        Email string `json:"email"`
    }

    var input LoginInput

    if err := c.BodyParser(&input); err != nil {
        return c.Status(400).SendString(err.Error())
    }

    stringUrl := fmt.Sprintf("https://jsonplaceholder.typicode.com/users?email=%s", input.Email)

    resp, err := http.Get(stringUrl)
    if err != nil {
        return c.Status(500).SendString(err.Error())
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return c.Status(500).SendString(err.Error())
    }

    if len(body) > 0 {
        fmt.Println(string(body))
    } else {
        return c.Status(400).JSON(fiber.Map{
            "message": "Yeah, we couldn't find that user",
        })
    }

    token := jwt.New(jwt.SigningMethodHS256)
    cookie := new(fiber.Cookie)

    claims := token.Claims.(jwt.MapClaims)
    claims["purpose"] = "Just a test really"

    signedToken, err := token.SignedString([]byte("NiceSecret"))
    if err != nil {
        // Internal Server Error if anything goes wrong in getting the signed token
        fmt.Println(err)
        return c.SendStatus(500)
    }

    cookie.Name = "access"
    cookie.HTTPOnly = true
    cookie.Secure = false
    cookie.Domain = "localhost"
    cookie.SameSite = "Lax"
    cookie.Path = "/"
    cookie.Value = signedToken
    cookie.Expires = time.Now().Add(time.Hour * 24)

    c.Cookie(cookie)

    return c.Status(200).JSON(fiber.Map{
        "message": "You have logged in",
    })
}
Run Code Online (Sandbox Code Playgroud)

这基本上是通过 JSON 占位符的用户进行查找,如果找到具有匹配电子邮件地址的用户,则会发送 HTTPOnly Cookie 并附加一些数据。

考虑到这可能是我使用的库的问题,我决定用 Express 编写一个 Node 版本。

import axios from 'axios'
import express from 'express'
import cookieParser from 'cookie-parser'
import jwt from 'jsonwebtoken'

const app = express()

app.use(express.json())
app.use(cookieParser())
app.use(express.urlencoded({ extended: true }))
app.disable('x-powered-by')

app.get("/", (req, res) => {
    res.send("Hello there!")
})

app.post("/login", async (req, res, next) => {
    try {
        const { email } = req.body

        const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users?email=${email}`)

        if (data) {
            if (data.length > 0) {
                res.locals.user = data[0]
                next()
            } else {
                return res.status(404).json({
                    message: "No results found"
                })
            }
        }
    } catch (error) {
        return console.error(error)
    }
}, async (req, res) => {
    try {
        let { user } = res.locals

        const token = jwt.sign({
            user: user.name
        }, "mega ultra secret sauce 123")

        res
            .cookie(
                'access',
                token,
                {
                    httpOnly: true,
                    secure: false,
                    maxAge: 3600
                }
            )
            .status(200)
            .json({
                message: "You have logged in, check your cookies"
            })
    } catch (error) {
        return console.error(error)
    }
})

app.listen(8000, () => console.log(`Server is up at localhost:8000`))
Run Code Online (Sandbox Code Playgroud)

这两个都不适用于我测试过的浏览器。

结果

Go 对此做出回应。

HTTP/1.1 200 OK
Date: Mon, 21 Feb 2022 05:17:36 GMT
Content-Type: application/json
Content-Length: 32
Vary: Origin
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Total-Count,Content-Range
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoiSnVzdCBhIHRlc3QgcmVhbGx5In0.8YKepcvnMreP1gUoe_S3S7uYngsLFd9Rrd4Jto-6UPI; expires=Tue, 22 Feb 2022 05:17:36 GMT; domain=localhost; path=/; HttpOnly; SameSite=Lax
Run Code Online (Sandbox Code Playgroud)

对于 Node API,这是响应标头。

HTTP/1.1 200 OK
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiTGVhbm5lIEdyYWhhbSIsImlhdCI6MTY0NTQyMDM4N30.z1NQcYm5XN-L6Bge_ECsMGFDCgxJi2eNy9sg8GCnhIU; Max-Age=3; Path=/; Expires=Mon, 21 Feb 2022 05:13:11 GMT; HttpOnly
Content-Type: application/json; charset=utf-8
Content-Length: 52
ETag: W/"34-TsGOkRa49turdlOQSt5gB2H3nxw"
Date: Mon, 21 Feb 2022 05:13:07 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Run Code Online (Sandbox Code Playgroud)

客户来源

我用它作为发送和接收数据的测试表单。

HTTP/1.1 200 OK
Date: Mon, 21 Feb 2022 05:17:36 GMT
Content-Type: application/json
Content-Length: 32
Vary: Origin
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Total-Count,Content-Range
Set-Cookie: access=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoiSnVzdCBhIHRlc3QgcmVhbGx5In0.8YKepcvnMreP1gUoe_S3S7uYngsLFd9Rrd4Jto-6UPI; expires=Tue, 22 Feb 2022 05:17:36 GMT; domain=localhost; path=/; HttpOnly; SameSite=Lax
Run Code Online (Sandbox Code Playgroud)

附加信息

邮递员: 9.8.3

语言版本

去: 1.17.6

Node.js: v16.13.1

苗条: 3.44.0

使用的浏览器

火狐浏览器: 97.0.1

微软边缘: 98.0.1108.56

铬: 99.0.4781.0

小智 8

解决方案

事实证明问题出在前端,特别是 JavaScript 的fetch()方法。

let response = await fetch(`http://localhost:8000/login`, {
                method: "POST",
                credentials: "include", //--> send/receive cookies
                body: JSON.stringify({
                    email,
                }),
                headers: {
                    "Content-Type": "application/json",
                },
            });
Run Code Online (Sandbox Code Playgroud)

您的对象中需要credentials: include属性RequestInit,不仅用于发出需要 cookie 身份验证的请求,还用于接收所述 cookie。

Axios 通常会自动填写这部分(根据经验),但如果没有,您还需要添加请求的withCredentials: true第三个config参数以允许浏览器设置 cookie。