解決上一節當中如果api路由改成:
1 Route::middleware('auth:api')->post('/questions/follow', 'QuestionController@followThroughApi');
之后 axios ajax post請求報 401 unauthorized
異常的問題。
原理
:
教程
:
Building SPAs with Laravel 5 and Vue.js 2 - Finishing Up
修改指導:簡單來說,auth要求的token我們沒有提供:
通過api訪問走的是token認證,這里沒有提供token所以就認證失敗返回401了
這個tokendriver對應的其實就是:
這個TokenGuard.php文件,這里面需要一個api_token。需要在request里提供api_token參數,為了區別是哪個用戶,需要在user表添加api_token字段。認證過程調用的是TokenGuard.php中的getTokenForRequest方法:
這個bearerToken實際找header中是否存在Authorization
我們需要來提供這個token:
原理參考:
BearerToken:
本質上給用戶表添加api_token,后台根據這個字段判斷是否是有效的用戶,無效返回401,有效返回查詢結果。
優點是容易理解,缺點太簡單,安全也不夠。
為了安全,可以實現下面的功能:每次登錄成功后刷新api_token為新值
其實 Laravel 官方提供了一個 Laravel Passport 的包。Laravel Passport is an OAuth2 server and API authentication package 。搞一搞laravel里api路由的 auth:api 和 api_token
JWT(Json Web Token):
【但是並不建議真實生產環境下用下面的方法,建議用官方的passport或者jwt
】
步驟:
執行命令:
1 php artisan make:migration add_api_token_to_users_table --table=users
編輯****_add_api_token_to_users_table.php文件:
_add_api_token_to_users_table.php1 <?php 2 3 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Schema\Blueprint; 5 use Illuminate\Support\Facades\Schema; 6 7 class AddApiTokenToUsersTable extends Migration 8 { 9 /** 10 * Run the migrations. 11 * 12 * @return void 13 */ 14 public function up() 15 { 16 Schema::table('users', function (Blueprint $table) { 17 // 18 $table->string('api_token', 64)->unique()->comment("api驗證token"); 19 }); 20 } 21 22 /** 23 * Reverse the migrations. 24 * 25 * @return void 26 */ 27 public function down() 28 { 29 Schema::table('users', function (Blueprint $table) { 30 // 31 $table->dropColumn('api_token'); 32 }); 33 } 34 } 35 36執行命令:
1 php artisan migrate
其余的參考Laravel Vue 前后端分離 使用token認證 按照此鏈接教程,完成后打開網頁刷新,再點擊關注按鈕,axios提交的時候不會報401異常了,記得修改 api.php中:
1 Route::middleware('auth:api')->post('/questions/follow', 'QuestionController@followThroughApi');
修改一下之前的兩個錯誤:
1.因為之前采用的是
一個路由 處理刷新取關注狀態和按下按鈕關注的邏輯,這樣一刷新頁面就又相當於執行了一次關注/取關操作,必須分割開
1 //加載頁面時取關注狀態 2 Route::middleware('auth:api')->post('/questions/follow/stats', 'QuestionController@getFollowStats'); 3 //執行關注/取關操作 4 Route::middleware('auth:api')->post('/questions/follow', 'QuestionController@followThroughApi'); 5然后,對應需要在QuestionController添加一個方法取關注狀態,
2.之前判斷用戶可否關注的邏輯錯誤
1 public function getFollowStats(Request $request) 2 { 3 $user = auth()->guard('api')->user(); 4 $question = Question::find($request->get('question')); 5 6 $followable = $user->can('follow', $question); 7 8 return response()->json([ 9 'followable' => $followable, 10 ]); 11 } 12 13 public function followThroughApi(Request $request) 14 { 15 $user = auth()->guard('api')->user(); 16 $question = Question::find($request->get('question')); 17 18 //同步記錄 19 $user->followQuestions()->toggle($question->id); 20 $question->followers_count = $question->followUsers()->count(); 21 $question->update(); 22 //判斷用戶關注狀態 23 $followable = $user->can('follow', $question); 24 25 return response()->json([ 26 'followable' => $followable, 27 ]); 28 } 29
再然后,優化一下,用戶的數據不由props傳遞,也不由axios提交,因為有bearer token了。
接着QuestionController中followThroughApi和getFollowStats方法內,
$user獲取: 直接通過 auth()->user() 或 auth()->guard('api')->user(); 獲取即可。
QuestionController.php

1 <?php 2 3 namespace App\Http\Controllers; 4 5 use App\Http\Requests\QuestionStoreRequest; 6 use App\Models\Question; 7 use App\Repositories\QuestionRepository; 8 use App\User; 9 use Illuminate\Http\Request; 10 11 class QuestionController extends Controller 12 { 13 14 /** 15 * @var QuestionRepository 16 */ 17 private $questionRepository; 18 19 public function __construct(QuestionRepository $questionRepository) 20 { 21 $this->middleware( 22 'auth', 23 [ 24 'except' => 25 [ 26 'index', 27 'show', 28 'followThroughApi' 29 ]//非注冊用戶只能查看不能編輯添加更改刪除 30 ] 31 ); 32 33 $this->questionRepository = $questionRepository; 34 } 35 36 37 /** Display a listing of the resource. 38 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 39 */ 40 public function index() 41 { 42 // 43 $questions = $this->questionRepository->getQuestionPublished(); 44 return view('questions.index', compact('questions')); 45 } 46 47 48 /** 49 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 50 */ 51 public function create() 52 { 53 // 54 return view('questions.create'); 55 } 56 57 58 /** 59 * @param QuestionStoreRequest $request 60 * @return \Illuminate\Http\RedirectResponse 61 */ 62 public function store(QuestionStoreRequest $request)//依賴注入QuestionStoreRequest實例 63 { 64 // 65 // $data = $request->validate([ 66 // 'title' => 'required|min:8', 67 // 'content' => 'required|min:28', 68 // ]); 69 //存儲topics 70 $topics = $this->questionRepository->normalizeTopics($request->get('topics')); 71 //初始化question要用到的數據 72 $data = $request->all(); 73 $data['user_id'] = auth()->user()->id; 74 75 // $question=Question::create($data); 被下方代碼取代 76 $question = $this->questionRepository->create($data); 77 78 //使用我們再question model里面添加的topics方法獲得 topics關聯,再使用attach方法 79 $question->topics()->attach($topics); 80 81 return redirect()->route('questions.show', $question); 82 } 83 84 85 /** 86 * @param Question $question 87 * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 88 */ 89 public function show(Question $question) 90 { 91 //使用關系關聯加載,with方法會將分類之下的主題一起查詢出來,而且不會出現N+1影響性能的問題 92 $question->with('topics')->get(); 93 //使用關系關聯加載,with方法會將分類之下的回答一起查詢出來,而且不會出現N+1影響性能的問題 94 $question->with('answers')->get(); 95 96 return view('questions.show', compact('question')); 97 } 98 99 100 /**判斷權限 返回視圖 101 * @param Question $question 102 * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View 103 */ 104 public function edit(Question $question) 105 { 106 if (auth()->user()->can('update', $question)) //判斷當前用戶是否有權編輯更新該question實例 107 { 108 //返回編輯視圖 109 return view('questions.edit', compact('question')); 110 } else { 111 //返回警告 沒有權限 112 return redirect()->back()->with('warning', '你不能編輯不屬於你的問題!'); 113 } 114 } 115 116 117 /** Update the specified resource in storage. 118 * @param QuestionStoreRequest $questionStoreRequest 119 * @param Question $question 120 * @return \Illuminate\Http\RedirectResponse 121 */ 122 public function update(QuestionStoreRequest $questionStoreRequest, Question $question) 123 { 124 //更新前 判斷下權限 125 if (!(auth()->user()->can('update', $question))) { 126 //返回警告 沒有權限 127 return redirect()->back()->with('warning', '你不能編輯不屬於你的問題!'); 128 } 129 //取得更新的字段 使用Eloquent提供的update方法執行問題更新 130 $question->update([ 131 'title' => $questionStoreRequest->get('title'), 132 'content' => $questionStoreRequest->get('content'), 133 ]); 134 135 136 //topics的操作這時候看起來有點臃腫 可以使用TopicController來管理,暫時省略 137 //存儲topics 138 $topics = $this->questionRepository->normalizeTopics($questionStoreRequest->get('topics')); 139 //使用我們再question model里面添加的topics方法獲得 topics關聯, 140 //再使用sync方法同步tag 【刪除的會被刪除掉,沒刪除的就保留,新的就增加】 141 $question->topics()->sync($topics); 142 143 //更新完成,跳轉回去 144 return redirect()->back(); 145 } 146 147 148 /**Remove the specified resource from storage. 149 * @param Question $question 150 * @return \Illuminate\Http\RedirectResponse 151 * @throws \Exception 152 */ 153 public function destroy(Question $question) 154 { 155 // 156 if (auth()->user()->can('destroy', $question)) { 157 $question->delete(); 158 return redirect()->route('questions.index')->with('success', "刪除成功!"); 159 } 160 return redirect()->back()->with('danger', "你不能刪除不屬於你的問題!"); 161 } 162 163 164 public function follow(Question $question) 165 { 166 if (auth()->user()->can('follow', $question)) //通過QuestionPolicy的follow方法判斷用戶是否可以關注問題 167 { 168 $message = "關注"; 169 } else { 170 $message = "取關"; 171 } 172 //同步記錄 173 auth()->user()->followQuestions()->toggle($question); 174 $question->followers_count = $question->followUsers()->count(); 175 $question->save(); 176 return redirect()->back()->with('success', $message . '成功!'); 177 } 178 179 public function getFollowStats(Request $request) 180 { 181 $user = auth()->guard('api')->user(); 182 $question = Question::find($request->get('question')); 183 184 $followable = $user->can('follow', $question); 185 186 return response()->json([ 187 'followable' => $followable, 188 ]); 189 } 190 191 public function followThroughApi(Request $request) 192 { 193 $user = auth()->guard('api')->user(); 194 $question = Question::find($request->get('question')); 195 196 //同步記錄 197 $user->followQuestions()->toggle($question->id); 198 $question->followers_count = $question->followUsers()->count(); 199 $question->update(); 200 //判斷用戶關注狀態 201 $followable = $user->can('follow', $question); 202 203 return response()->json([ 204 'followable' => $followable, 205 ]); 206 } 207 } 208 209
QuestionPolicy.php

1 <?php 2 3 namespace App\Policies; 4 5 use App\Models\Question; 6 use App\User; 7 use Illuminate\Auth\Access\HandlesAuthorization; 8 9 class QuestionPolicy 10 { 11 use HandlesAuthorization; 12 13 /** 14 * Create a new policy instance. 15 * 16 * @return void 17 */ 18 public function __construct() 19 { 20 // 21 22 } 23 24 25 /** 26 * 判斷用戶是否有權編輯更新問題 27 * @param User $user 28 * @param Question $question 29 * @return bool 30 */ 31 public function update(User $user, Question $question) 32 { 33 return $user->id === $question->user_id; 34 } 35 36 37 /** 38 * 判斷用戶是否有權刪除問題 39 * @param User $user 40 * @param Question $question 41 * @return bool 42 */ 43 public function destroy(User $user, Question $question) 44 { 45 return $user->id === $question->user_id; 46 } 47 48 49 /** 用戶是否可以關注問題,未登錄不行,關注了不行 50 * @param User $user 51 * @param Question $question 52 * @return bool 53 */ 54 public function follow(User $user, Question $question) 55 { 56 //axiox api 需要auth:api 先不實現,注釋掉 57 if (auth()->check()) { 58 return !($user->followQuestions->contains('id', $question->id)); 59 } else { 60 61 } 62 } 63 } 64 65
app.blade.php

1 <!doctype html> 2 <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 7 {{-- CSRF Token--}} 8 <meta name="csrf-token" content="{{ csrf_token() }}"> 9 {{-- api bearer Token--}} 10 <meta name="api-token" content="{{ Auth::check() ? 'Bearer '.auth()->user()->api_token : 'Bearer ' }}"> 11 12 <title>{{ config('app.name', 'Laravel') }}</title> 13 14 15 {{-- Fonts--}} 16 <link rel="dns-prefetch" href="//fonts.gstatic.com"> 17 <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> 18 19 {{-- Styles--}} 20 <link href="{{ mix('css/app.css') }}" rel="stylesheet"> 21 22 </head> 23 <body> 24 <div id="app"> 25 <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm"> 26 <div class="container"> 27 <a class="navbar-brand" href="{{ url('/') }}"> 28 {{ config('app.name', 'Laravel') }} 29 </a> 30 <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" 31 aria-controls="navbarSupportedContent" aria-expanded="false" 32 aria-label="{{ __('Toggle navigation') }}"> 33 <span class="navbar-toggler-icon"></span> 34 </button> 35 36 <div class="collapse navbar-collapse" id="navbarSupportedContent"> 37 {{-- Left Side Of Navbar--}} 38 <ul class="navbar-nav mr-auto"> 39 40 </ul> 41 42 {{-- Right Side Of Navbar--}} 43 <ul class="navbar-nav ml-auto"> 44 {{-- Authentication Links--}} 45 @guest 46 <li class="nav-item"> 47 <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a> 48 </li> 49 @if (Route::has('register')) 50 <li class="nav-item"> 51 <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a> 52 </li> 53 @endif 54 @else 55 <li class="nav-item dropdown"> 56 <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" 57 data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre> 58 {{ Auth::user()->name }} <span class="caret"></span> 59 </a> 60 61 <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> 62 <a class="dropdown-item" href="{{ route('logout') }}" 63 onclick="event.preventDefault(); 64 document.getElementById('logout-form').submit();"> 65 {{ __('Logout') }} 66 </a> 67 68 <form id="logout-form" action="{{ route('logout') }}" method="POST" 69 style="display: none;"> 70 @csrf 71 </form> 72 </div> 73 </li> 74 @endguest 75 </ul> 76 </div> 77 </div> 78 </nav> 79 80 <main class="py-4"> 81 @include('flash::message') 82 @yield('content') 83 </main> 84 </div> 85 {{--Scripts--}} 86 <script src="{{ mix('js/app.js') }}"></script> 87 88 <script> 89 $('#flash-overlay-modal').modal(); 90 window.UEDITOR_CONFIG.serverUrl = "{{ config('ueditor.route.name') }}"; 91 </script> 92 @yield('footer-js') 93 </body> 94 </html> 95 96
api.php

1 <?php 2 3 use Illuminate\Http\Request; 4 5 /* 6 |-------------------------------------------------------------------------- 7 | API Routes 8 |-------------------------------------------------------------------------- 9 | 10 | Here is where you can register API routes for your application. These 11 | routes are loaded by the RouteServiceProvider within a group which 12 | is assigned the "api" middleware group. Enjoy building your API! 13 | 14 */ 15 16 Route::middleware('auth:api')->get('/user', function (Request $request) { 17 return $request->user(); 18 }); 19 20 Route::middleware('api')->get('/topics', function (Request $request) { 21 $query = $request->query('q'); 22 return \App\Topic::query()->where('name', 'like', '%' . $query . '%')->get(); 23 }); 24 //加載頁面時取關注狀態 25 Route::middleware('auth:api')->post('/questions/follow/stats', 'QuestionController@getFollowStats'); 26 //執行關注/取關操作 27 Route::middleware('auth:api')->post('/questions/follow', 'QuestionController@followThroughApi'); 28 29 30 31
bootstrap.js

1 window._ = require('lodash'); 2 3 /** 4 * We'll load jQuery and the Bootstrap jQuery plugin which provides support 5 * for JavaScript based Bootstrap features such as modals and tabs. This 6 * code may be modified to fit the specific needs of your application. 7 */ 8 9 try { 10 window.Popper = require('popper.js').default; 11 window.$ = window.jQuery = require('jquery'); 12 13 require('bootstrap'); 14 } catch (e) { 15 } 16 17 /** 18 * We'll load the axios HTTP library which allows us to easily issue requests 19 * to our Laravel back-end. This library automatically handles sending the 20 * CSRF token as a header based on the value of the "XSRF" token cookie. 21 */ 22 23 window.axios = require('axios'); 24 25 window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 26 27 let api_token = document.head.querySelector('meta[name="api-token"]'); 28 29 if (api_token) { 30 window.axios.defaults.headers.common['Authorization'] = api_token.content; 31 } else { 32 console.error('Authorization token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); 33 } 34 35 /** 36 * Echo exposes an expressive API for subscribing to channels and listening 37 * for events that are broadcast by Laravel. Echo and event broadcasting 38 * allows your team to easily build robust real-time web applications. 39 */ 40 41 // import Echo from 'laravel-echo'; 42 43 // window.Pusher = require('pusher-js'); 44 45 // window.Echo = new Echo({ 46 // broadcaster: 'pusher', 47 // key: process.env.MIX_PUSHER_APP_KEY, 48 // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 49 // encrypted: true 50 // }); 51 52
QuestionFollowButton.vue

1 <template> 2 <button :class="classObject" 3 @click="follow" 4 v-text="text"> 5 </button> 6 </template> 7 8 <script> 9 export default { 10 props: ['question'], 11 name: "QuestionFollowButton", 12 data() { 13 return { 14 followable: true, 15 } 16 }, 17 computed: { 18 text() { 19 return this.followable ? "關注用戶" : "取消關注"; 20 }, 21 classObject() { 22 return this.followable ? "btn btn-block btn-primary" : "btn btn-block btn-danger"; 23 }, 24 }, 25 mounted: function () { 26 let currentObj = this; 27 axios.post('/api/questions/follow/stats', {'question': this.question}) 28 .then(function (response) { 29 currentObj.followable = response.data.followable; 30 }) 31 .catch(function (e) { 32 console.log(e); 33 }); 34 }, 35 methods: { 36 follow() { 37 let currentObj = this; 38 axios.post('/api/questions/follow', {'question': this.question}) 39 .then(function (response) { 40 currentObj.followable = response.data.followable; 41 } 42 ) 43 .catch(function (e) { 44 console.log(e); 45 }); 46 }, 47 } 48 } 49 </script> 50 51 <style scoped> 52 53 </style> 54 55
show.blade.php

1 @extends('layouts.app') 2 @section('content') 3 <div class="container"> 4 <div class="row"> 5 <div class="col-md-8 col-md offset-1"> 6 {{--問題--}} 7 <div class="card"> 8 <div class="card-header"> 9 {{ $question->title }} 10 11 @foreach(['success','warning','danger'] as $info) 12 @if(session()->has($info)) 13 <div class="alert alert-{{$info}}">{{ session()->get($info) }}</div> 14 @endif 15 @endforeach 16 17 @can('update',$question) 18 <a href="{{ route('questions.edit',$question) }}" class="btn btn-warning">編輯</a> 19 @endcan 20 21 @can('destroy',$question) 22 <form action="{{ route('questions.destroy',$question) }}" method="post"> 23 @csrf 24 @method('DELETE') 25 <button type="submit" class="btn btn-danger">刪除</button> 26 </form> 27 @endcan 28 29 @forelse($question->topics as $topic) 30 <button class="btn btn-secondary float-md-right m-1">{{ $topic->name }}</button> 31 @empty 32 <p class="text text-warning float-md-right"> "No Topics"</p> 33 @endforelse 34 35 <p class="text text-info float-md-right"> 已有{{ count($question->answers) }}個回答</p> 36 37 </div> 38 <div class="card-body"> 39 {!! $question->content !!} 40 </div> 41 </div> 42 43 44 {{--回答提交form--}} 45 {{--只有登錄用戶可以提交回答--}} 46 @if(auth()->check()) 47 <div class="card mt-2"> 48 <div class="card-header"> 49 提交回答 50 </div> 51 <div class="card-body"> 52 <form action="{{ route('answers.store',$question) }}" method="post"> 53 @csrf 54 <!-- 回答編輯器容器 --> 55 <script id="container" name="content" type="text/plain" 56 style="width: 100%;height: 200px">{!! old('content') !!}</script> 57 <p class="text text-danger"> @error('content') {{ $message }} @enderror </p> 58 <!--提交按鈕--> 59 <button type="submit" class="btn btn-primary float-md-right mt-2">提交回答</button> 60 </form> 61 </div> 62 </div> 63 @else 64 {{--顯示請登錄--}} 65 <a href="{{ route('login') }}" class="btn btn-success btn-block mt-4">登錄提交答案</a> 66 @endif 67 {{--展示答案--}} 68 @forelse($question->answers as $answer) 69 <div class="card mt-4"> 70 <div class="card-header"> 71 <div class="float-left"> 72 <img src="{{ $answer->user->avatar }}" class="img-thumbnail imgWrap" 73 style="height: 50px" alt="{{ $answer->user->name }}"> 74 <span class="text text-info">{{ $answer->user->name }}</span> 75 </div> 76 <span class="float-right text text-info m-auto">{{ $answer->updated_at }}</span> 77 </div> 78 79 <div class="card-body"> 80 {!! $answer->content !!} 81 </div> 82 </div> 83 84 @empty 85 86 @endforelse 87 </div> 88 89 <div class="col-md-3"> 90 <div class="card"> 91 <div class="card-header"> 92 <h2> {{ $question->followers_count }}</h2> 93 <span>關注者</span> 94 </div> 95 96 <div class="card-body"> 97 <div id="app"> 98 <question-follow-button question="{{$question->id}}"> 99 </question-follow-button> 100 </div> 101 </div> 102 </div> 103 </div> 104 </div> 105 </div> 106 @endsection 107 @section('footer-js') 108 @include('questions._footer_js') 109 @endsection 110 111
補充:
截取
API requests with axios always unauthorized with Laravel API
I'm not using Passort or any library like that since it's an internal API serving only VueJS to obtain stuff from the database.
If the API is not stateless, meaning that the user is known to be logged in with a standard session cookie, then you can just use the default 'web' middleware for the API routes.
In the default RouteServiceProvider, change the mapApiRoutes function to use the web middleware instead:
protected function mapApiRoutes()
{
Route::prefix('api')
// ->middleware('api')
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
That being said, you should really put the API routes behind the default 'auth' middleware since they're not throttled by default.
In the routes/api.php file:
Route::group(['middleware' => 'auth'], function() { Route::get('/latest', 'InternalApiController@latest'); });
And if you want to ensure it's an AJAX request, you can create a simple middleware that checks that the request has the X-Requested-With header set to XMLHttpRequest.
class RequestIsAjax { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if (!$request->ajax()) { return redirect()->route('login.index'); } return $next($request); } }
And register it within the $routeMiddleware array inside the \App\Http\Kernel class.
protected $routeMiddleware = [ 'ajax' => \App\Http\Middleware\RequestIsAjax::class,