使用 Laravel 作为后端刷新 Vue.js SPA 的身份验证令牌

Posted

技术标签:

【中文标题】使用 Laravel 作为后端刷新 Vue.js SPA 的身份验证令牌【英文标题】:Refreshing authentication tokens for a Vue.js SPA using Laravel for the backend 【发布时间】:2018-04-12 11:20:50 【问题描述】:

我正在使用 Laravel (5.5) 作为后端构建一个带有 Vue (2.5) 的单页应用程序。一切正常,除了注销后直接再次登录。在这种情况下,对 /api/user 的调用(以检索用户的帐户信息并再次验证用户的身份)失败并出现 401 未授权(即使登录成功)。作为响应,用户被直接弹回登录屏幕(我自己编写了这个度量作为对 401 响应的响应)。

有效的方法是注销,使用 ctrl/cmd+R 刷新页面,然后再次登录。页面刷新解决了我的问题,让我有理由相信我没有正确处理 X-CSRF-TOKEN 的刷新,或者可能忘记了 Laravel 使用的某些 cookie(如 here 所述)。

这是用户单击登录按钮后执行的登录表单代码的 sn-p。

login()
    // Copy the form data
    const data = ...this.user;
    // If remember is false, don't send the parameter to the server
    if(data.remember === false)
        delete data.remember;
    

    this.authenticating = true;

    this.authenticate(data)
        .then( this.refreshTokens )
        .catch( error => 
            this.authenticating = false;
            if(error.response && [422, 423].includes(error.response.status) )
                this.validationErrors = error.response.data.errors;
                this.showErrorMessage(error.response.data.message);
            else
                this.showErrorMessage(error.message);  
            
        );
,
refreshTokens()
    return new Promise((resolve, reject) => 
        axios.get('/refreshtokens')
            .then( response => 
                window.Laravel.csrfToken = response.data.csrfToken;
                window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.csrfToken;
                this.authenticating = false;
                this.$router.replace(this.$route.query.redirect || '/');
                return resolve(response);
            )
            .catch( error => 
                this.showErrorMessage(error.message);
                reject(error);
            );
    );
,  

authenticate() 方法是一个 vuex 动作,在 laravel 端调用登录端点。

/refreshTokens 端点只是调用这个 Laravel 控制器函数,它返回当前登录用户的 CSRF 令牌:

public function getCsrfToken()
    return ['csrfToken' => csrf_token()];

重新获取令牌后,用户将被重定向到主页(或其他页面,如果提供的话) 使用this.$router.replace(this.$route.query.redirect || '/'); 并在那里调用api/user 函数来检查当前登录用户的数据。

我是否应该采取任何其他措施来完成这项工作,但我忽略了?

感谢您的帮助!


编辑:2017 年 11 月 7 日

在所有有用的建议之后,我想补充一些信息。我在 Laravel 端使用 Passport 进行身份验证,并且 CreateFreshApiToken 中间件就位。

我一直在查看我的应用程序设置的 cookie,特别是 laravel_token,据说它保存了 Passport 将用来验证来自 javascript 应用程序的 API 请求的加密 JWT。注销时,laravel_token cookie 被删除。之后直接再次登录时(使用 axios 发送 AJAX 发布请求)没有设置新的laravel_token,所以这就是它不对用户进行身份验证的原因。我知道 Laravel 没有在登录 POST 请求上设置 cookie,但是之后直接对 /refreshTokens (不受保护)的 GET 请求应该设置 cookie。但是,这似乎没有发生。

我已尝试增加对/refreshTokens 的请求和对/api/user 的请求之间的延迟,以可能给服务器一些时间来整理东西,但无济于事。

为了完整起见,这里是我的 Auth\LoginController 正在处理登录请求服务器端:

class LoginController extends Controller

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    
        // $this->middleware('guest')->except('logout');
    

    /**
     * Get the needed authorization credentials from the request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    protected function credentials(\Illuminate\Http\Request $request)
    
        //return $request->only($this->username(), 'password');
        return ['email' => $request->$this->username(), 'password' => $request->password, 'active' => 1];
    

    /**
     * The user has been authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  mixed  $user
     * @return mixed
     */
    protected function authenticated(\Illuminate\Http\Request $request, $user)
    
        $user->last_login = \Carbon\Carbon::now();
        $user->timestamps = false;
        $user->save();
        $user->timestamps = true;

        return (new UserResource($user))->additional(
            ['permissions' => $user->getUIPermissions()]
        );
    


    /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(\Illuminate\Http\Request $request)
    
        $this->guard()->logout();
        $request->session()->invalidate();
    

【问题讨论】:

“注销”功能是如何实现的? 它通过post请求调用laravel的注销功能。所以很简单axios.post('/logout'); 当您观察 Ajax 的注销/登录请求来回切换时,您是否看到 /refreshtokens 生成并返回了一个新令牌,并且该令牌用于后续(和失败)调用到服务器? 是的,我看到正在生成一个新令牌。我做了更多研究,发现了一些新信息:问题是 refreshToken 返回的令牌是用于防止跨站点请求伪造的 CSRF 令牌。它不是用于对用户进行身份验证的 JWT 令牌。这是在一个名为 laravel_token 的 cookie 中设置的,它是一个 httpOnly cookie,因此在 JS 中无法访问。许多消息来源说浏览器应该处理这个令牌,即使它是通过 ajax 响应返回的,但显然这里不是这种情况...... 这里有更多关于这个问题的信息:github.com/laravel/passport/issues/293 【参考方案1】:

考虑到您使用 api 进行身份验证,我建议使用 Passport 或 JWT Authentication 来处理身份验证令牌。

【讨论】:

谢谢。我正在使用护照。它说它应该根据laravel.com/docs/5.5/… 自动处理 JWT 身份验证。这很好用,除了注销登录问题。我的想法是我没有正确处理根据laravel.com/docs/5.5/csrf#csrf-x-csrf-token 随每个响应返回的 cookie 数据 啊抱歉不明显。我会检查您的配置和设置,并确保一切正确 我已经这样做了,但找不到任何异常。我认为我的程序是正确的,但我错过了难题的某些部分(比如处理 cookie 数据)。我怀疑我是第一个遇到这个问题的人,所以希望有人发现错误。 显然我将用于防止跨站点请求伪造的 CSRF 令牌与用于身份验证的 laravel_token cookie 混淆了。此 cookie 是 httpOnly 的,因此 JS 无法访问。它应该由浏览器自动处理,但在我的情况下不会发生这种情况。 (即使我通过调用 /refreshTokens 执行了设置 cookie 所需的额外 GET 请求)。 您的配置中显然缺少某些内容或不正确。来自开发工具的一些请求/响应转储会很有帮助。此外,请确保您已将 \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class 添加到您的 web 中间件组中,如此处所述 laravel.com/docs/5.5/…【参考方案2】:

终于修好了!

通过直接在 LoginControllers authenticated 方法中返回 UserResource,它不是有效的 Laravel 响应(但我猜是原始 JSON 数据?)所以可能没有附加 cookie 之类的东西。我必须在资源上附加对 response() 的调用,现在一切似乎都运行良好(尽管我需要进行更广泛的测试)。

所以:

protected function authenticated(\Illuminate\Http\Request $request, $user)

    ...

    return (new UserResource($user))->additional(
        ['permissions' => $user->getUIPermissions()]
    );

变成

protected function authenticated(\Illuminate\Http\Request $request, $user)

    ...

    return (new UserResource($user))->additional(
        ['permissions' => $user->getUIPermissions()]
    )->response();  // Add response to Resource

Laravel 文档中关于将一个部分归因于此的万岁: https://laravel.com/docs/5.5/eloquent-resources#resource-responses

此外,登录的 POST 请求没有设置 laravel_token,并且调用 refreshCsrfToken() 也没有成功,可能是因为它受到了来宾中间件的保护。

最终对我有用的是在登录功能返回(或承诺已履行)后立即对“/”执行虚拟调用。

最后我在组件中的登录功能如下:

login()
    // Copy the user object
    const data = ...this.user;
    // If remember is false, don't send the parameter to the server
    if(data.remember === false)
        delete data.remember;
    

    this.authenticating = true;

    this.authenticate(data)
        .then( csrf_token => 
            window.Laravel.csrfToken = csrf_token;
            window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrf_token;

            // Perform a dummy GET request to the site root to obtain the larevel_token cookie
            // which is used for authentication. Strangely enough this cookie is not set with the
            // POST request to the login function.
            axios.get('/')
                .then( () => 
                    this.authenticating = false;
                    this.$router.replace(this.$route.query.redirect || '/');
                )
                .catch(e => this.showErrorMessage(e.message));
        )
        .catch( error => 
            this.authenticating = false;
            if(error.response && [422, 423].includes(error.response.status) )
                this.validationErrors = error.response.data.errors;
                this.showErrorMessage(error.response.data.message);
            else
                this.showErrorMessage(error.message);  
            
        );

而我的 vuex 商店中的 authenticate() 动作如下:

authenticate( dispatch , data)
    return new Promise( (resolve, reject) => 
        axios.post(LOGIN, data)
            .then( response => 
                const csrf_token, ...user = response.data;
                // Set Vuex state
                dispatch('setUser', user );
                // Store the user data in local storage
                Vue.ls.set('user', user );
                return resolve(csrf_token);
            )
            .catch( error => reject(error) );
    );
,

因为除了对/ 的虚拟调用之外,我不想对refreshTokens 进行额外调用,因此我将 csrf_token 附加到后端的 /login 路由的响应中:

protected function authenticated(\Illuminate\Http\Request $request, $user)

    $user->last_login = \Carbon\Carbon::now();
    $user->timestamps = false;
    $user->save();
    $user->timestamps = true;

    return (new UserResource($user))->additional([
        'permissions' => $user->getUIPermissions(),
        'csrf_token' => csrf_token()
    ])->response();

【讨论】:

【参考方案3】:

您应该在 Web 中间件中使用 Passports CreateFreshApiToken 中间件passport consuming-your-api

web => [...,
    \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],

此附件将正确的 csrftoken() 作为 request_cookies 附加到您的所有请求标头

【讨论】:

感谢您的建议,但建议已经到位,我正在使用 CreateFreshApiToken 中间件

以上是关于使用 Laravel 作为后端刷新 Vue.js SPA 的身份验证令牌的主要内容,如果未能解决你的问题,请参考以下文章

将 Vue js 前端和 laravel 后端(api 路由)托管到共享服务器?

vue.js的“Firebase + Validation Example”中如何使用mysql作为数据持久化后端?

我想从 laravel 中的 vue.js 代码运行工匠命令

使用 Laravel 和 Vue.js 分离前端和后端

如何使用 vue.js laravel 自动显示数据而不刷新

Laravel + Vue JS:从数据库中获取/存储 Eloquent 模型的值