0

我的 Phoenix API 中的受保护路由正在向请求发送 403 响应。调试日志显示:

(Plug.CSRFProtection.InvalidCSRFTokenError) invalid CSRF (Cross Site Request Forgery) token, please make sure that:

  * The session cookie is being sent and session is loaded
  * The request include a valid '_csrf_token' param or 'x-csrf-token' header

我的路由器是这样的:

defmodule MyWeb.Router do
  use MyWeb, :router
  alias MyWeb.Auth.GuardianPipeline

  pipeline :api do
    plug :accepts, ["json"]
    plug :fetch_session
    plug GuardianPipeline
  end

  pipeline :protected do
    plug Guardian.Plug.EnsureAuthenticated
    plug :protect_from_forgery
  end

  scope "/api/v1", MyWeb.API.V1, as: :api_v1 do
    pipe_through :api

    post "/login", SessionController, :login
  end

  scope "/api/v1", MyWeb.API.V1, as: :api_v1 do
    pipe_through [:api, :protected]

    delete "/logout", SessionController, :logout
  end
end

我的会话控制器登录操作如下所示:

defp login_reply({:ok, user}, conn) do
  conn
  |> Guardian.Plug.sign_in(user)
  |> put_session(:_csrf_token, csrf_token)
  |> json(%{csrf_token: get_csrf_token()})
end

在前端,我对受保护的“注销”路径的请求如下所示:

const logout = async () => {
  const method = 'DELETE'
  const headers = {
    'Content-Type': 'application/json',
    'x-csrf-token': csrfToken.value
  }
  await fetch('http://localhost:4000/api/v1/logout', { method, headers, credentials: 'include' })
  1. CSRF 错误说我应该确保会话已发送并加载我看到会话 cookie 加载到我的浏览器中,所以我知道它已发送。会话由:fetch_session插件加载。所以第一个条件应该满足。

  2. x-csrf_token头正在获取中设置,并且我已验证csrfToken.value匹配由get_csrf_token(). 所以第二个条件也应该满足。

我究竟做错了什么?为什么没有验证 csrf 令牌?

--

这是整个 JS 组件

<script lang="ts">
import { defineComponent, ref, onMounted } from '@nuxtjs/composition-api'

export default defineComponent({
  setup (_props) {
    const csrfToken = ref('')
    const email = 'free@city.17'
    const error = ref('')
    const submitting = false

    const isAuthenticated = () => !!csrfToken.value

    const setAuthenticated = () => {
      return isAuthenticated()
    }

    const logout = async () => {
      const method = 'DELETE'
      const headers = {
        'Content-Type': 'application/json',
        'x-csrf-token': csrfToken.value
      }
      await fetch('http://localhost:4000/api/v1/logout', { method, headers, credentials: 'include' })
        .then((response) => {
          if (response.status === 401) {
            error.value = 'No account found with that email address'
          } else if (!response.ok) {
            error.value = 'Something went wrong'
          }
          return response
        })
        .then(response => response.json())
        .catch((_err) => {
          error.value = 'network error'
        })

      if (error.value) {
        throw new Error(error.value)
      }

      csrfToken.value = ''
      return true
    }

    const onLogout = () => {
      error.value = ''
      return logout().catch(_e => '')
    }

    const login = async () => {
      const method = 'POST'
      const body = JSON.stringify({ user: { email } })
      const headers = {
        'Content-Type': 'application/json'
      }
      const resp = await fetch('http://localhost:4000/api/v1/login', { method, headers, body, credentials: 'include' })
        .then((response) => {
          if (response.status === 401) {
            error.value = 'No account found with that email address'
          } else if (!response.ok) {
            error.value = 'Something went wrong'
          }
          return response
        })
        .then(response => response.json())
        .catch((_err) => {
          error.value = 'network error'
        })

      if (error.value) {
        throw new Error(error.value)
      }

      console.log(resp)
      csrfToken.value = resp.csrf_token
      return true
    }

    const onSubmit = () => {
      error.value = ''
      return login().catch(_e => '')
    }

    onMounted(setAuthenticated)

    return {
      csrfToken,
      email,
      error,
      submitting,
      isAuthenticated,
      setAuthenticated,
      onLogout,
      onSubmit
    }
  }
})
</script>

整个 Phoenix Session Controller

defmodule MyWeb.API.V1.SessionController do
  use MyWeb, :controller
  alias MyWeb.Auth.Guardian
  alias MyWeb.API.V1.Auth

  def login(conn, %{"user" => %{"email" => email}}) do
    Auth.authenticate_user(%{email: email})
    |> login_reply(conn)
  end

  def logout(conn, _params) do
    conn
    |> Guardian.Plug.sign_out(clear_remember_me: true)
    |> configure_session(drop: true)
    |> send_resp(:ok, "")
  end

  defp login_reply({:ok, user}, conn) do
    conn
    |> Guardian.Plug.sign_in(user)
    # |> Guardian.Plug.remember_me(user)
    |> put_session(:_csrf_token, csrf_token)
    |> json(%{csrf_token: csrf_token})
  end

  defp login_reply({:error, reason}, conn) do
    conn
    |> put_status(:unauthorized)
    |> json(%{reason: reason})
  end
end

我为路由器的protected管道创建了这个自定义插件:

  defp inspect_plug(conn, _opts) do
    IO.inspect(Plug.Conn.get_session(conn), label: :session)

    session_token = Plug.Conn.get_session(conn, "_csrf_token")
    IO.inspect(session_token, label: :session_token)
    IO.inspect(byte_size(session_token), label: :session_token_size)
    csrf_token = Plug.CSRFProtection.dump_state_from_session(session_token)

    IO.inspect(csrf_token, label: :csrf_token)

    conn
  end

这是它记录“注销”请求的示例:

session: %{
  "_csrf_token" => "HSwrOF5AOQ5nKyIWHH9RFFg0HVMWDRA_dFIVlulMQipLw39PmPzkPXIf",
  "guardian_default_token" => "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRvbWVldHVwIiwiZXhwIjoxNjI2NDU1MDQ4LCJpYXQiOjE2MjQwMzU4NDgsImlzcyI6ImF1dG9tZWV0dXAiLCJqdGkiOiJkMDczYTMwZi00ZTMzLTRkZjItODBhMi04YzRkZDNiYzAzMzAiLCJuYmYiOjE2MjQwMzU4NDcsInN1YiI6IjEiLCJ0eXAiOiJhY2Nlc3MifQ.KF-rv-65G_0NUMsFa9tYedWKE0gRTd9hvx_pOyBHFV5DTGFPtsfJnaz3WOk63ARKu8-bEbjAZGXRRPAZ0E3LMw"
}
session_token: "HSwrOF5AOQ5nKyIWHH9RFFg0HVMWDRA_dFIVlulMQipLw39PmPzkPXIf"
session_token_size: 56
csrf_token: nil

我想知道为什么从我的会话 cookiePlug.CSRFProtection.dump_state_from_session中返回 nil 。_csrf_token

https://github.com/elixir-plug/plug/blob/fa579592ebf53306e3a4b13e2414a40997add1f7/lib/plug/csrf_protection.ex#L195

4

0 回答 0