我的 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' })
CSRF 错误说我应该确保会话已发送并加载我看到会话 cookie 加载到我的浏览器中,所以我知道它已发送。会话由
:fetch_session
插件加载。所以第一个条件应该满足。标
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