Laravel 自带的 API 守卫驱动 token 使用详解

目录
  1. 1. 寻找 TokenGuard
  2. 2. 解读 TokenGuard
  3. 3. 开始使用 token 进行 api 认证
    1. 3.1. 添加数据表字段
    2. 3.2. 创建登录控制器
  4. 4. 添加登录路由
  5. 5. 调试
  6. 6. 总结

Laravel 框架中,默认的用户认证守卫有两个,webapiweb 守卫默认的驱动是 session,而 api 守卫默认的驱动是 token。那么,该如何使用这个 token 驱动?

寻找 TokenGuard

通过 Auth::guard() 这个方法,可以追溯到 token 驱动对应的类。来看 Illuminate\Auth\AuthManager 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Attempt to get the guard from the local cache.
*
* @param string $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*/
public function guard($name = null)
{
$name = $name ?: $this->getDefaultDriver();

return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

/**
* Resolve the given guard.
*
* @param string $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*
* @throws \InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->getConfig($name);

if (is_null($config)) {
throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
}

if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($name, $config);
}

$driverMethod = 'create'.ucfirst($config['driver']).'Driver';

if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($name, $config);
}

throw new InvalidArgumentException("Auth driver [{$config['driver']}] for guard [{$name}] is not defined.");
}

可以看到,默认情况下就会调用到 createTokenDriver 。来看看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function createTokenDriver($name, $config)
{
// The token guard implements a basic API token based guard implementation
// that takes an API token field from the request and matches it to the
// user in the database or another persistence layer where users are.
$guard = new TokenGuard(
$this->createUserProvider($config['provider'] ?? null),
$this->app['request']
);

$this->app->refresh('request', $guard, 'setRequest');

return $guard;
}

显然,api守卫默认的驱动就是 TokenGuard

解读 TokenGuard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Create a new authentication guard.
*
* @param \Illuminate\Contracts\Auth\UserProvider $provider
* @param \Illuminate\Http\Request $request
* @param string $inputKey
* @param string $storageKey
* @return void
*/
public function __construct(UserProvider $provider, Request $request, $inputKey = 'api_token', $storageKey = 'api_token')
{
$this->request = $request;
$this->provider = $provider;
$this->inputKey = $inputKey;
$this->storageKey = $storageKey;
}

/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}

$user = null;

$token = $this->getTokenForRequest();

if (! empty($token)) {
$user = $this->provider->retrieveByCredentials(
[$this->storageKey => $token]
);
}

return $this->user = $user;
}

从构造函数和 user() 方法中可以看出,默认使用

1
['api_token' => $token]

这个数组去获取用户,也就是说,在用户表中我们需要一个字段(默认 api_token)去存储标识用户的 token。

开始使用 token 进行 api 认证

添加数据表字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddUsersApiTokenField extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('api_token', 60)->unique()->nullable()->after('password');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('api_token');
});
}
}

创建登录控制器

这里不演示注册之类的,假设我们的 users 表中已经存在用户,先创建一个用于 api 登录的控制器。在每次登录的时候,更新一次用户的 api_token 。这里使用了 ThrottlesLogins ,用来控制登录的尝试次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?php

namespace App\Http\Controllers\Api;

use Hash;
use App\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
use ThrottlesLogins;

/**
* @param Request $request
* @return \Illuminate\Http\Response|void
* @throws ValidationException
*/
public function login(Request $request)
{
$this->validateLogin($request);

if ($this->hasTooManyLoginAttempts($request)) {
return $this->sendLockoutResponse($request);
}

return $this->attempLogin($request);

}

/**
* @param Request $request
*/
public function validateLogin(Request $request)
{
$this->validate($request, [
$this->username() => 'required|string',
'password' => 'required|string'
]);
}

/**
* @param Request $request
* @return \Illuminate\Http\Response|void
*/
protected function attempLogin(Request $request)
{
$this->incrementLoginAttempts($request);

$user = User::where('email', $request->email)->first();

if (!$user || !Hash::check($request->password, $user->password)) {
return $this->sendFailedLoginResponse($request);
}

// 更新 api_key
$api_token = uniqid($user->id);
$user->api_token = $api_token;
$user->save();

return $this->sendLoginResponse($request, $user);
}

/**
* @param Request $request
*/
protected function sendFailedLoginResponse(Request $request)
{
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
]);
}

/**
* @param Request $request
* @param User $user
* @return \Illuminate\Http\Response
*/
protected function sendLoginResponse(Request $request, User $user)
{
$this->clearLoginAttempts($request);

return \Response::make([
'user' => $user,
'token' => $user->api_token
]);
}

public function username()
{
return 'email';
}
}

添加登录路由

routes\api.php 修改如下:

1
2
3
4
5
6
7
8
Route::namespace('Api')->group(function () {
Route::post('login', 'LoginController@login');
}); # 登录路由

Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});

调试

测试之前先往 users 表中添加几个用户,以下是我的测试数据。

图片丢失了,没办法

可以看到登录成功并且返回了 token 。
接下去我们使用获取到的 token 请求需要登录的接口,默认有一个,就是 /user.

图片丢失了,没办法

ok~ 已经成功返回了数据,说明登录成功了!

例子中,api_token 是通过 OAuth 2.0 Access Token 的形式传进去的,但这不是唯一的方法,可以查看 TokenGuard 中的 getTokenForRequest 这个方法,它告诉我们可以用四种不同的形式传入 api_token

总结

默认的 api token 认证虽然在文档中没有提及如何使用,但是通过查看代码,也很容易使用。但是,在我们不重写或者扩展 tokenGUard 的情况下,api_token 简直就是裸奔,显然不可能就这样应用到项目中去。个人猜测,框架中提供这个功能是为了让我们更好的理解 api 认证的工作原理,方便我们开发自己需要的 guard ,而且官方文档也推荐我们使用 passport 或者 jwt 进行 api 认证。