You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel 9 Sanctum登出接口返回401未授权错误求助

Laravel 9 + Sanctum SPA 登出接口返回未授权问题

我基于Laravel 9和Sanctum搭建了带身份验证的SPA应用,登录流程正常,/api/user接口能通过Bearer Token正常访问,但调用AuthController的logout方法时一直返回未授权提示。


后端代码及配置

AuthController 代码

App\Http\Controllers\Api\AuthController

<?php

namespace App\Http\Controllers\Api;

use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    public function login(Request $request){
        $request->validate([
            'email'=>['required','email'],
            'password'=>['required']
        ]);
        $user = User::where('email',$request->email)->first();
        Auth::guard('web')->login($user);
        if(!$user || !Hash::check($request->password, $user->password)){
            return response(['message'=>'账号或密码错误']);
        } else {
            $token = $user->createToken('bisa_js-token')->plainTextToken;
            $response = [
                'user'=>$user,
                'token'=>$token
            ];
            return response($response);
        }
    }
    public function logout(Request $request){
        Auth::attempt([
            'email'=>$request->email,
            'password'=>$request->password
        ]);
        Auth::user()->token()->delete();
        return response(['message'=>'Successfully Logging out']);
    }
}

Sanctum 配置

app\config\sanctum.php

<?php

use Laravel\Sanctum\Sanctum;

return [
    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
        Sanctum::currentApplicationUrlWithPort()
    ))),
    'guard' => ['web'],
    'expiration' => null,
    'middleware' => [
        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
    ],
];    

CORS 配置

app\config\cors.php

<?php

return [
    'paths' => ['/api/*', 'sanctum/csrf-cookie', '/login', '/logout'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://backend.bisa_js.test','http://localhost:3000'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

Kernel 配置

App\Http\Kernel.php

protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

API 路由

app\routes\api.php

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\AuthController;

Route::post('/login',[AuthController::class, 'login']);
Route::middleware('auth:sanctum')->post('/logout',[AuthController::class, 'logout']);
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

前端代码

登录组件

Login.js

import React, {useState, useRef, useEffect} from "react"
import axios from "axios"
import {useCookies} from 'react-cookie'
import {useNavigate} from 'react-router-dom'
const Login = () => {
    const navigate = useNavigate()
    const [email, setEmail] = useState('')
    const errorEmailRef = useRef(null)
    const [errorEmail, setErrorEmail] = useState([])
    const [password, setPassword] = useState('')
    const errorPasswordRef = useRef(null)
    const [errorPassword, setErrorPassword] = useState([])
    const [errorMessage, setErrorMessage] = useState('')
    const errorMessageRef = useRef(null)
    const [cookie, setCookie, removeCookie] = useCookies(['login'])
    useEffect(()=>{
        if(cookie.token !== undefined && cookie.user !== undefined){
            document.body.className = ''
            navigate('/',{replace: true})
        }
    },[])
    const submitForm = (e) => {
        e.preventDefault()
        axios.post('http://backend.bisa_js.test/api/login', {email, password}, {
            headers: {
                'Accept':'application/json'
            }
        })
        .then((response)=>{
            console.log(response.data)
            setCookie('token',response.data.token, {path:'/'})
            setCookie('user',response.data.user, {path:'/'})
            navigate('/')
        })
        .catch((reject)=>{
            if(reject.response.data.errors !== undefined){
                const errors = reject.response.data.errors
                errors.email !== undefined ? setErrorEmail(errors.email) : setErrorEmail([])
                errors.password !== undefined ? setErrorPassword(errors.password) : setErrorPassword([])
                errorEmailRef.current.className = 'block'
                errorPasswordRef.current.className = 'block'
                setErrorMessage(reject.response.data.message)
            } else {
                if(errorMessageRef.current.classList.contains('hidden')){
                    errorMessageRef.current.classList.toggle('hidden')
                    errorMessageRef.current.classList.toggle('block')
                }
                setErrorMessage(reject.response.data.message)
            }
            console.log(reject.response)
        })
    }
    return (
        <div className="w-full h-screen bg-slate-800 flex flex-col justify-center">
            <div className="mb-3 p-2 w-[300px] mx-auto bg-red-800 rounded-lg shadow shadow-white text-white hidden" ref={errorMessageRef}>{errorMessage}</div>
            <div className="w-[300px] mx-auto p-5 h-max rounded bg-slate-900 text-white shadow shadow-white">
                
                <form className="bisa-js__form-login" onSubmit={submitForm}>
                    <div className="bisa-js__group-form mb-5">
                        <label className="block mb-3">Email</label>
                        <input type='email' value={email} onChange={(event)=>{
                            setEmail(event.target.value)
                        }} placeholder="type here your email..." 
                        className="block p-2 text-base rounded outline-none mb-2 bg-slate-900 shadow shadow-white text-white w-full"/>
                        <div ref={errorEmailRef} className="hidden">
                            {errorEmail.map((val, ind)=>(
                                <p className="text-red-500 font-medium" key={ind}>{val}</p>
                            ))}
                        </div>
                    </div>
                    <div className="bisa-js__group-form mb-5">
                        <label className="block mb-3">Password</label>
                        <input type='password' value={password} onChange={(event)=>{
                            setPassword(event.target.value)
                        }} placeholder="type here your password..."
                        className="block p-2 text-base rounded outline-none mb-2 bg-slate-900 shadow shadow-white text-white w-full"/>
                        <div ref={errorPasswordRef} className="hidden">
                        {errorPassword.map((val, ind)=>(
                                <p className="text-red-500 font-medium" key={ind}>{val}</p>
                            ))}
                        </div>
                    </div>
                    <button type="submit" className="p-2 rounded shadow shadow-white">Login</button>
                </form>
            </div>
        </div>
    )
}
export default Login

下拉菜单(含登出功能)

Dropdown.js

import React, { useRef, useState } from 'react'
import { useCookies } from 'react-cookie'
import axios from 'axios'
import {useNavigate} from 'react-router-dom'
const Dropdown = () => {
    const [cookie, setCookie, removeCookie] = useCookies(['login'])
    const dropdownRef = useRef('')
    const [toggleMenuBool, setToggleMenuBool] = useState(false)
    const navigate = useNavigate()
    function logout(){
        axios.defaults.withCredentials = true
        axios.get('http://backend.bisa_js.test/api/user', {
            headers: {
                'Accept': 'application/json',
                'Authorization': `Bearer ${cookie.token}`
            }
        }).then((response)=>{
            console.log(response)
        })

        axios.post('http://backend.bisa_js.test/api/logout', {}, {
            headers: {
                'Accept':'application/json',
                'Authorization':`Bearer ${cookie.token}`
            }
        }).then((response)=>{
            console.log(response)
            removeCookie('user',{path: '/'});
            removeCookie('token',{path: '/'});
            navigate('/login')
        }).catch(err => {
            console.error(err.response)
        })
    }
    return (
        <div className='bisa-js__dropdown w-max relative'>
            <button className={toggleMenuBool ? 'bisa-js__dropdown-toogle w-max py-2 px-4 border border-slate-800 text-white bg-slate-800' : 'bisa-js__dropdown-toogle w-max py-2 px-4 border border-slate-800 bg-transparent text-slate-800'} onClick={()=>{
                setToggleMenuBool(!toggleMenuBool)
                dropdownRef.current.classList.contains('hidden') ? dropdownRef.current.classList.remove('hidden') : dropdownRef.current.classList.add('hidden')
            }} >Account</button>
            <div className='bisa-js__dropdown-menu w-max absolute top-12 right-0 bg-white border border-slate-800 hidden' ref={dropdownRef}>
                <div className='bisa-js__dropdown-header p-3'>
                    <p>{cookie.user.name}</p>
                    <p className='text-sm font-medium'>{cookie.user.email}</p>
                </div>
                <hr/>
                <div className='bisa-js__logout'>
                    <button onClick={logout.bind(this)}>Logout</button>
                </div>
            </div>
        </div>
    )
}
export default Dropdown

问题原因及修复方案

1. 后端Logout方法逻辑错误

当前logout方法中调用Auth::attempt()尝试重新验证用户,但前端并未传递邮箱和密码到登出接口,导致Auth::user()返回null,触发未授权。且路由已添加auth:sanctum中间件,用户已通过Bearer Token验证,无需二次登录。

修改后的logout方法:

public function logout(Request $request){
    // 删除当前请求使用的令牌
    $request->user()->currentAccessToken()->delete();
    // 若需删除该用户所有令牌,替换为下面一行:
    // $request->user()->tokens()->delete();
    return response(['message'=>'成功登出']);
}

2. 前端Logout请求参数格式错误

原代码中axios.post的参数顺序错误,将headers放在了请求体位置,导致请求格式异常。正确写法是第二个参数为请求体(登出无需传递内容,传空对象),第三个参数为配置对象。

修复后的请求代码:

axios.post('http://backend.bisa_js.test/api/logout', {}, {
    headers: {
        'Accept':'application/json',
        'Authorization':`Bearer ${cookie.token}`
    }
})

3. 登录接口逻辑优化(可选)

原登录方法先执行Auth::guard('web')->login($user)再验证密码,存在安全隐患,应先验证密码再登录用户:

public function login(Request $request){
    $request->validate([
        'email'=>['required','email'],
        'password'=>['required']
    ]);
    
    $user = User::where('email',$request->email)->first();
    
    if(!$user || !Hash::check($request->password, $user->password)){
        return response(['message'=>'账号或密码错误'], 401);
    } 
    
    // 验证通过后再登录
    Auth::guard('web')->login($user);
    $token = $user->createToken('bisa_js-token')->plainTextToken;
    
    return response([
        'user'=>$user,
        'token'=>$token
    ]);
}

完成以上修改后,登出接口即可正常工作,不再返回未授权错误。

内容的提问来源于stack exchange,提问作者humanrizki123

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 20:19:00