基于 Laravel (5.1) & Ember.js (1.13.0) 的用户授权系统

北战南征 提交于 2019-12-01 02:44:16

Laravel 本身提供了完整的用户授权解决方案,对于由 PHP 驱动的多页面应用,Laravel 能够完美解决用户授权问题。但是在 SPA 中,laravel 退化成一个 API server,页面路由和表单提交完全由前端框架控制,此时面临2个问题:

  1. 如何在前端实现页面访问权限控制?

  2. 如何对 ajax 请求做授权?


如何在前端实现页面访问权限控制?

Ember.js 1.13.0 没有提供 authentication 功能,我使用了一个名为 ember-simple-auth 的第三方扩展。这是它的 Github 主页:

https://github.com/simplabs/ember-simple-auth

首先在你的 ember-cli 项目根目录下安装该扩展:

ember install ember-cli-simple-auth

然后在 ember/config/environment.js 文件中对其进行配置,具体的配置选项在文档中有详细说明,我的配置如下:

// ember/config/environment.js

ENV['simple-auth'] = {
    authorizer: 'authorizer:custom'    //我使用了一个自定义的授权模块
};

Ember-simple-auth 定义了一系列 mixin 类,只要你的 route 继承了某个 mixin, 就获得了它预定义的某些行为或功能。例如,我的 ember/app/routes/application.js 内容如下:

// 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 权限错误,如果是,则重定向到登录页要求用户取得授权。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!