本文主要介绍JWT([JSON Web Token])授权机制在前后端分离中的应用与实践,包括以下三部分:
- JWT原理介绍
- JWT的安全性
- JWT与Vue的使用
用户授权往往还是采用Cookie+Session的方式,也就是需要原生应用中模拟浏览器对Cookie的操作
Cookie+Session的存在主要是为了解决HTTP这一无状态协议下服务器如何识别用户的问题,其原理就是在用户登录通过验证后,服务端将数据加密后保存到客户端浏览器的Cookie中,同时服务器保留相对应的Session(文件或DB)。用户之后发起的请求都会携带Cookie信息,服务端需要根据Cookie寻回对应的Session,从而完成验证,确认这是之前登陆过的用户。其工作原理如下图所示:
JWT是Auth0提出的通过对JSON进行加密签名来实现授权验证的方案,编码之后的JWT看起来是这样的一串字符:可以参考jwt.io eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
由.
分为三段,通过解码可以得到:
// 1. Headers
// 包括类别(typ)、加密算法(alg);
{
"alg": "HS256",
"typ": "JWT"
}
// 2. Claims
// 包括需要传递的用户信息;
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
// 3. Signature
// 根据alg算法与私有秘钥进行加密得到的签名字串;
// 这一段是最重要的敏感信息,只能在服务端解密;
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECREATE_KEY
)
在使用过程中,服务端通过用户登录验证之后,将Header+Claim信息加密后得到第三段签名,然后将签名返回给客户端,在后续请求中,服务端只需要对用户请求中包含的JWT进行解码,即可验证是否可以授权用户获取相应信息,其原理如下图所示:
通过比较可以看出,使用JWT可以省去服务端读取Session的步骤,这样更符合RESTful的规范。但是对于客户端(或App端)来说,为了保存用户授权信息,仍然需要通过Cookie或类似的机制进行本地保存。因此JWT是用来取代服务端的Session而非客户端Cookie的方案,当然对于客户端本地存储,HTML5提供了Cookie之外更多的解决方案(localStorage/sessionStorage),究竟采用哪种存储方式,其实从Js操作上来看没有本质上的差异,不同的选择更多是出于安全性的考虑。
用户授权这样敏感的信息,安全性当然是首先需要考虑的因素。这里主要讨论在使用JWT时如何防止XSS和XSRF两种攻击。
XSS是Web中最常见的一种漏洞(我们的**学报官网就存在这个漏洞这件事我就不说了=.=),其主要原因是对用户输入信息不加过滤,导致用户(被误导)恶意输入的Js代码在访问该网页时被执行,而Js可以读取当前网站域名下保存的Cookie信息。针对这种攻击,无论是Cookie还是localStorage中的信息都有可能被窃取,但防止XSS也相对简单一些,对用户输入的所有信息进行过滤即可。另外,现在越来越多的CDN服务,让我们可以节省服务器流量,但同时也有可能引入不安全的Js脚本,例如前段时间Github被Great Cannon轰击的案例,则需要提高对某度之类服务的警惕。
另外一种更加棘手的XSRF漏洞主要利用Cookie是按照域名存储,同时访问某域名时浏览器会自动携带该域名所保存的Cookie信息这一特征。如果执意要将JWT存储在Cookie中,服务端则需要额外验证请求来源,或者在提交表单中加入随机签名并在处理表单时进行验证。
我在后面的实例中采用将JWT保存在localStorage中的方案,请求时将JWT放入Request Header中的Authorization位。对JWT安全性问题想要了解更多可以参考下面几篇文章:
- Where to Store Your JWTs - Cookies vs HTML5 Web Storage
- Use JWT the Right Way!
- 10 Things You Should Know about Tokens
- Where to store JWT in browser? How to protect against CSRF?
jwt_auth 当前使用版本已经2.0.*了,但是这里 使用的是1.0的最新版,同样的 如果你是 Laravel 5.4 以下版本,也推荐使用最新版本,RC.1 前的版本都存在多用户token认证的安全问题。
$ composer require tymon/jwt-auth 1.0.*@dev
将下面这行添加至 config/app.php 文件 providers 数组中:
'providers' => [
...
Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
jwt-auth 已经预先定义好了一个 Artisan 命令方便你生成 Secret $ php artisan jwt:secret
此命令会在你的 .env 文件中新增一行 JWT_SECRET=secret。
在 config/auth.php 文件中,你需要将 guards/driver 更新为 jwt:
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
...
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
只有在使用 Laravel 5.2 及以上版本的情况下才能使用。
如果需要使用 jwt-auth 作为用户认证,我们需要对我们的 User 模型进行一点小小的改变,实现一个接口,变更后的 User 模型如下:
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;
// Rest omitted for brevity
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
return [
/*
|--------------------------------------------------------------------------
| JWT Authentication Secret
|--------------------------------------------------------------------------
|
| 用于加密生成 token 的 secret
|
*/
'secret' => env('JWT_SECRET'),
/*
|--------------------------------------------------------------------------
| JWT Authentication Keys
|--------------------------------------------------------------------------
|
| 如果你在 .env 文件中定义了 JWT_SECRET 的随机字符串
| 那么 jwt 将会使用 对称算法 来生成 token
| 如果你没有定有,那么jwt 将会使用如下配置的公钥和私钥来生成 token
|
*/
'keys' => [
/*
|--------------------------------------------------------------------------
| Public Key
|--------------------------------------------------------------------------
|
| 公钥
|
*/
'public' => env('JWT_PUBLIC_KEY'),
/*
|--------------------------------------------------------------------------
| Private Key
|--------------------------------------------------------------------------
|
| 私钥
|
*/
'private' => env('JWT_PRIVATE_KEY'),
/*
|--------------------------------------------------------------------------
| Passphrase
|--------------------------------------------------------------------------
|
| 私钥的密码。 如果没有设置,可以为 null。
|
*/
'passphrase' => env('JWT_PASSPHRASE'),
],
/*
|--------------------------------------------------------------------------
| JWT time to live
|--------------------------------------------------------------------------
|
| 指定 access_token 有效的时间长度(以分钟为单位),默认为1小时,您也可以将其设置为空,以产生永不过期的标记
|
*/
'ttl' => env('JWT_TTL', 60),
/*
|--------------------------------------------------------------------------
| Refresh time to live
|--------------------------------------------------------------------------
|
| 指定 access_token 可刷新的时间长度(以分钟为单位)。默认的时间为 2 周。
| 大概意思就是如果用户有一个 access_token,那么他可以带着他的 access_token
| 过来领取新的 access_token,直到 2 周的时间后,他便无法继续刷新了,需要重新登录。
|
*/
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
/*
|--------------------------------------------------------------------------
| JWT hashing algorithm
|--------------------------------------------------------------------------
|
| 指定将用于对令牌进行签名的散列算法。
|
*/
'algo' => env('JWT_ALGO', 'HS256'),
/*
|--------------------------------------------------------------------------
| Required Claims
|--------------------------------------------------------------------------
|
| 指定必须存在于任何令牌中的声明。
|
|
*/
'required_claims' => [
'iss',
'iat',
'exp',
'nbf',
'sub',
'jti',
],
/*
|--------------------------------------------------------------------------
| Persistent Claims
|--------------------------------------------------------------------------
|
| 指定在刷新令牌时要保留的声明密钥。
|
*/
'persistent_claims' => [
// 'foo',
// 'bar',
],
/*
|--------------------------------------------------------------------------
| Blacklist Enabled
|--------------------------------------------------------------------------
|
| 为了使令牌无效,您必须启用黑名单。
| 如果您不想或不需要此功能,请将其设置为 false。
|
*/
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
/*
| -------------------------------------------------------------------------
| Blacklist Grace Period
| -------------------------------------------------------------------------
|
| 当多个并发请求使用相同的JWT进行时,
| 由于 access_token 的刷新 ,其中一些可能会失败
| 以秒为单位设置请求时间以防止并发的请求失败。
|
*/
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
/*
|--------------------------------------------------------------------------
| Providers
|--------------------------------------------------------------------------
|
| 指定整个包中使用的各种提供程序。
|
*/
'providers' => [
/*
|--------------------------------------------------------------------------
| JWT Provider
|--------------------------------------------------------------------------
|
| 指定用于创建和解码令牌的提供程序。
|
*/
'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class,
/*
|--------------------------------------------------------------------------
| Authentication Provider
|--------------------------------------------------------------------------
|
| 指定用于对用户进行身份验证的提供程序。
|
*/
'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
/*
|--------------------------------------------------------------------------
| Storage Provider
|--------------------------------------------------------------------------
|
| 指定用于在黑名单中存储标记的提供程序。
|
*/
'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,
],
];
参考链接:
Laravel 5.5 使用 Jwt-Auth 实现 API 用户认证以及无痛刷新访问令牌
jwt.io
JWT 在前后端分离中的应用与实践