基於 Laravel (5.1) & Ember.js (1.13.0) 的用戶受權系統

Laravel 自己提供了完整的用戶受權解決方案,對於由 PHP 驅動的多頁面應用,Laravel 可以完美解決用戶受權問題。可是在 SPA 中,laravel 退化成一個 API server,頁面路由和表單提交徹底由前端框架控制,此時面臨2個問題:php

  1. 如何在前端實現頁面訪問權限控制?
    前端

  2. 如何對 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 函數作了兩件事:

  1. 爲每個 ajax 請求添加 'X-XSRF-TOKEN' header

  2. 檢查服務器返回的受權狀態,並作處理

具體來說:

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 權限錯誤,若是是,則重定向到登陸頁要求用戶取得受權。

相關文章
相關標籤/搜索