使用Laravel作为后端,为Vue.js单页应用程序刷新身份验证令牌

9
我正在使用Vue(2.5)构建单页应用,并使用Laravel(5.5)作为后端。一切都很好,除了在注销登录后直接再次登录。在这种情况下,调用/api/user(以检索用户的帐户信息并再次验证用户身份)会失败,并显示401未经授权的错误(即使登录成功)。作为响应,用户被直接弹回到登录页面(我自己编写了此措施以应对401响应)。
有效的方法是注销,使用ctrl/cmd + R刷新页面,然后再次登录。页面刷新解决了我的问题,这让我相信我没有正确处理X-CSRF-TOKEN的刷新,或者可能忘记了Laravel使用的某些cookies(如这里所述)。
这是执行用户单击登录按钮后执行的登录表单代码片段。
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月07日

在所有有用的建议之后,我想添加一些信息。我正在使用 Passport 在 Laravel 上进行身份验证,并且已经安装了 CreateFreshApiToken 中间件。

我一直在查看我的应用程序设置的 cookie,特别是 laravel_token,据说它保存了加密的 JWT,Passport 将使用它来验证来自 JavaScript 应用程序的 API 请求。当注销时,laravel_token cookie 将被删除。然后立即再次登录(使用 axios 发送 AJAX post 请求),没有设置新的 laravel_token,因此无法验证用户。我知道 Laravel 不会在登录 POST 请求上设置 cookie,但是 GET 请求到 /refreshTokens(不受保护)之后直接设置 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();
    }
}

“logout”功能是如何实现的? - Bragolgirith
它使用POST请求调用Laravel的注销函数,因此只需axios.post('/logout')即可。 - Daniel Schreij
当您观察注销/登录的Ajax请求来回传递时,是否看到/refreshtokens生成并返回了一个新的令牌,并且该令牌用于随后(失败的)对服务器的调用? - Chris Phillips
是的,我看到正在生成一个新的令牌。我进行了更多的研究,并发现了一些新信息:问题在于refreshToken返回的令牌是用于防止跨站请求伪造的CSRF令牌。它不是用于验证用户的JWT令牌。这个令牌被设置在名为laravel_token的cookie中,这是一个httpOnly cookie,因此在JS中无法访问。许多来源都说浏览器应该负责处理这个令牌,即使它是通过ajax响应返回的,但显然在这里并非如此... - Daniel Schreij
这个问题的更多信息在这里:https://github.com/laravel/passport/issues/293 - Daniel Schreij
3个回答

4

考虑到您正在使用API进行身份验证,我建议使用PassportJWT身份验证来处理身份验证令牌。


谢谢。我正在使用Passport。根据https://laravel.com/docs/5.5/passport#consuming-your-api-with-javascript,它应该自动处理JWT身份验证。这很有效,除了注销登录问题。我认为我没有正确处理每个响应返回的cookie数据,根据https://laravel.com/docs/5.5/csrf#csrf-x-csrf-token。 - Daniel Schreij
啊,对不起,看来没注意到。我会检查您的配置和设置,并确保一切都正确。 - CUGreen
我已经查过了,但是没有发现任何异常。我认为我的步骤是正确的,但我似乎缺少了一部分拼图(例如处理 cookie 数据)。我怀疑我不是第一个遇到这个问题的人,因此希望有人能找出错误所在。 - Daniel Schreij
显然,我混淆了用于防止跨站点请求伪造的CSRF令牌和用于身份验证的“laravel_token” cookie。该cookie是httpOnly的,因此无法被JS访问。它应该由浏览器自动处理,但在我的情况下,这种情况并没有发生(即使我执行额外的GET请求来设置cookie,通过调用/refreshTokens)。 - Daniel Schreij
你的配置中显然缺少或存在错误。一些来自开发工具的请求/响应转储将会很有帮助。同时,请确保你已经按照这里所述将\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class添加到了你的web中间件组中,以便使用JavaScript消费API。 - CUGreen
中间件已经就位并设置了laravel_token cookie用于身份验证。一旦我弄清楚正在发生什么,我会深入探讨并在这里发布我的发现或转储。 - Daniel Schreij

4

终于解决了!

在LoginController的authenticated方法中直接返回UserResource并不是一个有效的Laravel Response(但我猜它是原始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() 的调用也没有起作用,可能是因为它受到了 guest middleware 的保护。
最终我成功的方法是在登录函数返回后(或承诺实现后)执行一个对 '/' 的虚拟调用。
最终,我的组件中的登录函数如下:
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 store中的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();
}

1

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

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

这会将正确的csrftoken()附加到您所有请求头作为请求cookie。

谢谢您的建议,但这已经实施了,我正在使用CreateFreshApiToken中间件。 - Daniel Schreij

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接