使用 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 代码运行工匠命令