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
相关产品推荐
相关产品推荐

