Laravel 自己提供了完整的用戶受權解決方案,對於由 PHP 驅動的多頁面應用,Laravel 可以完美解決用戶受權問題。可是在 SPA 中,laravel 退化成一個 API server,頁面路由和表單提交徹底由前端框架控制,此時面臨2個問題:php
如何在前端實現頁面訪問權限控制?
前端如何對 ajax 請求作受權?laravel
如何在前端實現頁面訪問權限控制?git
Ember.js 1.13.0 沒有提供 authentication 功能,我使用了一個名爲 ember-simple-auth 的第三方擴展。這是它的 Github 主頁:github
https://github.com/simplabs/ember-simple-auth
ajax
首先在你的 ember-cli 項目根目錄下安裝該擴展:
shell
ember install ember-cli-simple-auth
而後在 ember/config/environment.js 文件中對其進行配置,具體的配置選項在文檔中有詳細說明,個人配置以下:json
// ember/config/environment.js ENV['simple-auth'] = { authorizer: 'authorizer:custom' //我使用了一個自定義的受權模塊 };
Ember-simple-auth 定義了一系列 mixin 類,只要你的 route 繼承了某個 mixin, 就得到了它預約義的某些行爲或功能。例如,個人 ember/app/routes/application.js 內容以下:
api
// ember/app/routes/application.js import ApplicationRouteMixin from 'simple-auth/mixins/application-route-mixin'; export default Ember.Route.extend(ApplicationRouteMixin, { actions: { invalidateSession: function() { this.get('session').invalidate(); } } });
application-route-mixin 已經預約義好了一系列 actions 方法。當 session 上的事件被觸發時,對應的 action 將被調用來處理該事件。你也能夠在 ember/app/routes/application.js 本身的 action 中覆蓋這些方法(ember-simple-auth 會在本地 localStorage 中維護一個 session 對象,它保存着前端產生的全部受權信息)。跨域
而後,在只能由已受權用戶訪問的頁面路由中添加 authenticated-route-mixin:
// ember/app/routes/user.js import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin'; export default Ember.Route.extend(AuthenticatedRouteMixin,{ model: function(params) { return this.store.find('user',params.user_id); } });
authenticated-route-mixin 保證了只有受權用戶才能訪問 /user。若是未受權,則默認重定向到 /login 。因此在 ember/app/routes/login.js 中須要添加 unauthenticated-route-mixin :
// ember/app/routes/login.js import UnauthenticatedRouteMixin from 'simple-auth/mixins/unauthenticated-route-mixin'; export default Ember.Route.extend(UnauthenticatedRouteMixin);
unauthenticated-route-mixin 保證該路徑不須要受權也能訪問,這對於 /login 是合理的。
如何對 ajax 請求作受權?
自定義 authenticator : ember/app/authenticators/custom.js
// ember/app/authenticators/custom.js import Base from 'simple-auth/authenticators/base'; export default Base.extend({ /** * Check auth state of frontend * * @param data (傳入session包含的數據) * @returns {ES6Promise.Promise} */ restore: function(data) { return new Ember.RSVP.Promise(function(resolve, reject) { if ( data.is_login ){ resolve(data); } else{ reject(); } }); }, /** * Permission to login by frontend * * @param obj credentials * @returns {ES6Promise.Promise} */ authenticate: function(credentials) { var authUrl = credentials.isLogin ? '/auth/login' : '/auth/register' return new Ember.RSVP.Promise(function(resolve, reject) { Ember.$.ajax({ url: authUrl, type: 'POST', data: { email: credentials.identification, password: credentials.password } }).then(function(response) { if(response.login === 'success'){ resolve({ is_login : true }); } }, function(xhr, status, error) { reject(xhr.responseText); }); }); }, /** * Permission to logout by frontend * * @returns {ES6Promise.Promise} */ invalidate: function() { return new Ember.RSVP.Promise(function(resolve) { Ember.$.ajax({ url: '/auth/logout', type: 'GET' }).then(function(response) { if(response.logout === 'success'){ resolve(); } }); }); } });
restore, authenticate, invalidate 3個函數分別用來獲取受權,進行受權,取消受權。
自定義 authorizer : ember/app/authorizers/custom.js
// ember/app/authorizers/custom.js import Base from 'simple-auth/authorizers/base'; export default Base.extend({ authorize: function(jqXHR, requestOptions) { var _this = this; Ember.$.ajaxSetup({ headers: { 'X-XSRF-TOKEN': Ember.$.cookie('XSRF-TOKEN') // 防止跨域攻擊 }, complete : function(response, state) { // 檢查服務器的受權狀態 if(response.status===403 && _this.get('session').isAuthenticated) { _this.get('session').invalidate(); } } }); } });
authorize 函數作了兩件事:
爲每個 ajax 請求添加 'X-XSRF-TOKEN' header
檢查服務器返回的受權狀態,並作處理
具體來說:
header 內容是 laravel 所設置的 'XSRF-TOKEN' cookie 的值,laravel 會嘗試從每個請求中讀取 header('X-XSRF-TOKEN'), 並檢驗 token 的值是否合法,若是檢驗經過,則認爲這是一個安全的請求(該功能在 laravel/app/Http/Middleware/VerifyCsrfToken.php 中實現)。
而後,在 laravel 新建一箇中間件(Middleware) ,我把它命名爲 VerifyAuth:
<?php // laravel/app/Http/Middleware/VerifyAuth.php namespace App\Http\Middleware; use Closure; use Illuminate\Contracts\Auth\Guard; class VerifyAuth { protected $include = ['api/*']; // 須要作權限驗證的 URL protected $auth; public function __construct(Guard $auth) { $this->auth = $auth; } /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @abort 403 * @return mixed */ public function handle($request, Closure $next) { if( $this->shouldPassThrough($request) || $this->auth->check() ) { return $next($request); } abort(403, 'Unauthorized action.'); //拋出異常,由前端捕捉並處理 } /** * Determine if the request has a URI that should pass through auth verification. * * @param \Illuminate\Http\Request $request * @return bool */ protected function shouldPassThrough($request) { foreach ($this->include as $include) { if ($request->is($include)) { return false; } } return true; } }
它只對 API 請求作權限驗證,由於 AUTH 請求是對權限的操做,而除此以外的其餘請求都會做爲無效請求從新路由給前端,或者拋出錯誤。若是一個請求是未被受權的,服務器拋出 403 錯誤提醒前端須要用戶登陸或者註冊。
最後,在 laravel\app\Http\Controllers\Auth\AuthController.php 中實現全部的受權邏輯:
<?php namespace App\Http\Controllers\Auth; use App\User; use Validator; use Response; use Auth; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ThrottlesLogins; use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers; class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins; protected $remember = true; // 是否長期記住已登陸的用戶 public function __construct() { $this->middleware('guest', ['except' => 'getLogout']); } public function postLogin(Request $credentials) // 登陸 { return $this->logUserIn($credentials); } public function getLogout() // 登出 { Auth::logout(); return Response::json(['logout'=>'success']); } public function postRegister(Request $credentials) // 建立並註冊新用戶 { $newUser = new User; $newUser->email = $credentials['email']; $newUser->password = bcrypt($credentials['password']); $newUser->save(); return $this->logUserIn($credentials); } protected function logUserIn(Request $credentials) // 實現用戶登陸 { $loginData = ['email' => $credentials['email'], 'password' => $credentials['password']]; if ( Auth::attempt($loginData, $this->remember) ) { return Response::json(['login'=>'success']); } else { return Response::json(['login'=>'failed']); } } }
總結
設置頁面訪問權限能防止未受權用戶訪問不屬於他的頁面,但總歸前端是徹底暴露給用戶的,因此用戶的受權狀態必須由服務器維護。前端一方面爲每一個 ajax 請求添加防止跨域攻擊的 token, 另外一方面當每一個請求返回後檢查 http status code 是否爲 403 權限錯誤,若是是,則重定向到登陸頁要求用戶取得受權。