在JavaScript前端技术大行其道的今天,我们通常只需在后台构建API提供给前端调用,并且后端仅仅设计为给前端移动App调用。用户认证是Web应用的重要组成部分,基于API的用户认证有两个最佳解决方案 —— OAuth 2.0 和 JWT(JSON Web Token)。
第一章 概念普及
1.1 JWT定义及其组成
JWT(JSON Web Token)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
1.1.1 载荷(Payload)
我们先将用户认证的操作描述成一个JSON对象。其中添加了一些其他的信息,帮助今后收到这个JWT的服务器理解这个JWT。
{
"sub": "1",
"iss": "http://localhost:8000/auth/login",
"iat": 1451888119,
"exp": 1454516119,
"nbf": 1451888119,
"jti": "37c107e4609ddbcc9c096ea5ee76c667"
}
这里面的前6个字段都是由JWT的标准所定义的。
- sub: 该JWT所面向的用户
- iss: 该JWT的签发者
- iat(issued at): 在什么时候签发的token
- exp(expires): token什么时候过期
- nbf(not before):token在此时间之前不能被接收处理
- jti:JWT ID为web token提供唯一标识
这些定义都可以在标准中找到。
将上面的JSON对象进行base64编码可以得到下面的字符串:
eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4ODExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ 这个字符串我们将它称作JWT的Payload(载荷)。
如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串:
var base64url = require('base64url')
var header = {
"from_user": "B",
"target_user": "A"
}
console.log(base64url(JSON.stringify(header)))
注:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
1.1.2 头部(Header)
JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象:
{
"typ": "JWT",
"alg": "HS256"
}
在这里,我们说明了这是一个JWT,并且我们所用的签名算法(后面会提到)是HS256算法。
对它也要进行Base64编码,之后的字符串就成了JWT的Header(头部): eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
1.1.3 签名(签名)
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4ODExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ 最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret):
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
这样就可以得到我们加密后的内容:
wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4 这一部分又叫做签名。
最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4ODExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ.wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4
1.2 通过JWT 进行认证
JWT 是一个令牌(Token),客户端得到这个服务器返回的令牌后,可以将其存储到 Cookie 或 localStorage 中,此后,每次与服务器通信都要带上这个令牌,你可以把它放到 Cookie 中自动发送,但这样做不能跨域,所以更好的做法是将其放到 HTTP 请求头 Authorization 字段里面: Authorization: Bearer 服务端收到这个 JWT 令牌后,就可以根据令牌值认定用户身份。
第二章 集成JWT到Laravel 5.8
2.1 项目安装依赖
composer require tymon/jwt-auth
 安装完成后可以在项目根目录中的 composer.json 中看到如下内容
...
"require": {
"php": "^7.1.3",
"fideloper/proxy": "^4.0",
"laravel/framework": "5.8.*",
"laravel/tinker": "^1.0",
"tymon/jwt-auth": "^1.0"
},
...
2.2 项目配置
本文环境使用的是laravel 5.8 无需注册服务提供者,Laravel 5.4 及以下版本的用户需要手动配置一下
2.2.1 发布配置文件
在项目的控制台中输入
php artisan vendor:publish
然后根据提示回应对应的配置项编号即可。  成功之后可以看到项目目录/config/jwt.php 的这个配置文件,你可以配置以下选项:
- ttl:token有效期(分钟)
- refresh_ttl:刷新token时间(分钟)
- algo:token签名算法
- user:指向User模型的命名空间路径
- identifier:用于从token的sub中获取用户
- require_claims:必须出现在token的payload中的选项,否则会抛出TokenInvalidException异常
- blacklist_enabled:如果该选项被设置为false,那么我们将不能废止token,即使我们刷新了token,前一个token仍然有效
- providers:完成各种任务的具体实现,如果需要的话你可以重写他们
- User —— providers.user:基于sub获取用户的实现
- JWT —— providers.jwt:加密/解密token
- Authentication —— providers.auth:通过证书/ID获取认证用户
- Storage —— providers.storage:存储token直到它们失效
2.2.2 生成密钥
通过如下命令
php artisan jwt:secret
 成功后可以在.env 中看到如下字段
JWT_SECRET=Wkgeflos6dLXPzt1f6fSB9qI4dmMpNgpoKezIsHxpzzKLSZjlXHSIvoKSsyCpL9H
2.3 快速开始
2.3.1 更新用户模型
首先,模型类需要在 User 模型实现Tymon\JWTAuth\Contracts\JWTSubject 接口和他的 2 个方法getJWTIdentifier() 和getJWTCustomClaims() .
下面的示例应该让您了解它的外观。显然,您应该根据自己的需要进行任何更改。
<?php
namespace App;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements JWTSubject
{
use Notifiable;
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}
以下是根据上面的模板构建我的模型类
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class UserModel extends Authenticatable implements JWTSubject
{
protected $table = 't_users';
protected $fillable = ['username','password'];
public $timestamps = false;
use Notifiable;
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}
2.3.2 配置身份验证保护
在config/auth.php 文件中,您需要进行一些更改以配置 Laravel 以使用jwt 防护来支持您的应用程序身份验证。 对文件进行以下更改:
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
...
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
...
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\UserModel::class,
],
],
2.3.3 添加一些基本的认证路由
在routes/api.php 中配置一些带有权限认证的路由
Route::group([
'middleware' => 'api',
'prefix' => 'auth'
], function ($router) {
Route::post('login', 'AuthController@login');
Route::post('logout', 'AuthController@logout');
Route::post('refresh', 'AuthController@refresh');
Route::post('me', 'AuthController@me');
});
2.3.4 创建AuthController
通过手动或者使用artisan 工具命令时创建:
php artisan make:controller AuthController
然后添加如下内容
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
class AuthController extends Controller
{
public function __construct()
{
$this->middleware('auth:api', ['except' => ['login']]);
}
public function login()
{
$credentials = request(['email', 'password']);
if (! $token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
public function me()
{
return response()->json(auth()->user());
}
public function logout()
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
public function refresh()
{
return $this->respondWithToken(auth()->refresh());
}
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
]);
}
}
第三章 测试
3.1测试登录接口

{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC90ZXN0MS5jb21cL2FwaVwvYXV0aFwvbG9naW4iLCJpYXQiOjE2MzMwMDc4MjIsImV4cCI6MTYzMzAxMTQyMiwibmJmIjoxNjMzMDA3ODIyLCJqdGkiOiIyMG8zczNHQkd4NnlnR2tuIiwic3ViIjoxLCJwcnYiOiI0MWRmODgzNGYxYjk4ZjcwZWZhNjBhYWVkZWY0MjM0MTM3MDA2OTBjIn0.josgI6pz8VLEjw2-etbjDL49Ju7xCnEMnJfd-zPAauc",
"token_type": "bearer",
"expires_in": 3600
}
成功拿到了。 有以下需要注意:
- 在
config/api.php 中定义的路由访问时 不能直接访问auth/** ,必须添加api 前缀,如api/auth/login ,是由app/Providers/RouteServiceProvider.php 决定的 - 图中可以看出我的数据库中密码为一串加密后的内容,但是提交的为
123456 ,原因是采用laravel的auth 机制来实现jwt 在登录的时候会默认将密码进行Bcrypt 加密,所以在注册时请直接将密码保存为该种加密方式
之后可以使用此令牌向您的应用程序发出经过身份验证的请求。
3.2 测试获取信息的接口
之前在api.php中定义了auth/me 现在来携带Token进行测试
 成功拿到了响应!~
|