Laravel 5 中使用 JWT(Json Web Token) 实现基于API的用户认证

Laravel使用JWT实现API认证

在JavaScript前端技术大行其道的今天,我们通常只需在后台构建API提供给前端调用,并且后端仅仅设计为给前端移动App调用。用户认证是Web应用的重要组成部分,基于API的用户认证有两个最佳解决方案 —— OAuth 2.0 和 JWT(JSON Web Token)。

JWT定义及其组成

JWT(JSON Web Token)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

载荷(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编码可以得到下面的字符串:

eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx
ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD
ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ

这个字符串我们将它称作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是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

头部(Header)

JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象:

{
  "typ": "JWT",
  "alg": "HS256"
}

在这里,我们说明了这是一个JWT,并且我们所用的签名算法(后面会提到)是HS256算法。

对它也要进行Base64编码,之后的字符串就成了JWT的Header(头部):

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

签名(签名)

将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx
ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD
ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ

最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret):

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
)

这样就可以得到我们加密后的内容:

wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4

这一部分又叫做签名。

最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx
ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD
ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ.wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4

通过JWT 进行认证

JWT 是一个令牌(Token),客户端得到这个服务器返回的令牌后,可以将其存储到 Cookie 或 localStorage 中,此后,每次与服务器通信都要带上这个令牌,你可以把它放到 Cookie 中自动发送,但这样做不能跨域,所以更好的做法是将其放到 HTTP 请求头 Authorization 字段里面:

Authorization: Bearer <token>

服务端收到这个 JWT 令牌后,就可以根据令牌值认定用户身份。

集成JWT到Laravel 5

安装

我们使用Composer安装jwt扩展包:

composer require tymon/jwt-auth

配置

安装完成后,需要在config/app.php中注册相应的服务提供者(Laravel 5.4 及以下版本,Laravel 5.5 + 版本会自动发现该扩展包):

Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class

然后注册需要用到的对应门面:

'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class

然后发布相应配置文件:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"

最后生成密钥:

php artisan jwt:secret

注:如果你是用的 tymon/jwt-auth 是 0.5.* 版本的,请使用php artisan jwt:generate命令生成密钥。

如果你想要将其添加到.env文件中,在.env中创建JWT_SECRET字段并再次执行生成密钥的命令。

在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直到它们失效

创建Token

在开始之前,还需要让 User 模型类实现 JWTSubject 接口:

<?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;

    // 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 [];
    }
}

创建用户token最常用的方式就是通过登录实现用户认证,如果成功则返回相应用户的token。这里假设我们有一个AuthenticateController

use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;

class AuthenticateController extends Controller
{
    public function authenticate(Request $request)
    {
        // grab credentials from the request
        $credentials = $request->only('email', 'password');

        try {
            // attempt to verify the credentials and create a token for the user
            if (! $token = JWTAuth::attempt($credentials)) {
                return response()->json(['error' => 'invalid_credentials'], 401);
            }
        } catch (JWTException $e) {
            // something went wrong whilst attempting to encode the token
            return response()->json(['error' => 'could_not_create_token'], 500);
        }

        // all good so return the token
        return response()->json(compact('token'));
    }
}

有时候我们还可以直接通过用户对象实例创建token:

// grab some user
$user = User::first();
$token = JWTAuth::fromUser($user);

此外,还可以使用Tymon\JWTAuth\PayloadFactory实例(或者JWTFactory门面)基于任意数据创建token:

$customClaims = ['foo' => 'bar', 'baz' => 'bob'];
$payload = JWTFactory::make($customClaims);
$token = JWTAuth::encode($payload);

还可以使用方法链的方式:

// add a custom claim with a key of `foo` and a value of ['bar' => 'baz']
$payload = JWTFactory::sub(123)->aud('foo')->foo(['bar' => 'baz'])->make();
$token = JWTAuth::encode($payload);

用户认证

用户登录成功之后,下一步就是发送一个包含token的请求来获取用户信息。

要通过http发送一个需要认证通过的请求,需要设置Authorization头:

Authorization: Bearer {yourtokenhere}

如果用户名/密码没有进行base64编码那么Apache似乎会摒弃Authorization头,要修复这一问题你可以添加如下代码到Apache配置文件:

RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]

或者将token信息包含到URL中:

http://api.mysite.com/me?token={yourtokenhere}

要从请求中获取token,你可以这么做:

// this will set the token on the object
JWTAuth::parseToken();// and you can continue to chain methods
$user = JWTAuth::parseToken()->authenticate();

要获取该token值,你可以这么调用:

$token = JWTAuth::getToken();

如果token被设置则会返回,否则会尝试使用方法从请求中解析token,如果token未被设置或不能解析最终返回false。

当然如果需要的话你还可以手动设置token:

JWTAuth::setToken('foo.bar.baz');

从Token中获取认证用户:

// somewhere in your controller
public function getAuthenticatedUser()
{
    try {
        if (! $user = JWTAuth::parseToken()->authenticate()) {
            return response()->json(['user_not_found'], 404);
        }
    } catch (Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
        return response()->json(['token_expired'], $e->getStatusCode());
    } catch (Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
        return response()->json(['token_invalid'], $e->getStatusCode());
    } catch (Tymon\JWTAuth\Exceptions\JWTException $e) {
        return response()->json(['token_absent'], $e->getStatusCode());
    }

    // the token is valid and we have found the user via the sub claim
    return response()->json(compact('user'));
}

jwt-auth扩展还提供了两个中间件GetUserFromTokenRefreshToken,前者用于在请求头和参数中检查是否包含token,并尝试对其解码,后者会再次从请求中解析token,并顺序刷新token(同时废弃老的token)并将其作为下一个响应的一部分。要使用这两个中间件,需要到app/Http/Kernel.php下的$routeMiddleware属性中注册它们:

protected $routeMiddleware = [
    ...
    'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
    'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',
];

JWT让用户认证变得简单和安全,token会被保存到本地的storage/web或Cookie中,使用JWT,基于API的用户认证将不再困难。

学院君 has written 1243 articles

Laravel学院院长,终身学习者

积分:167842 等级:P12 职业:手艺人 城市:杭州

60 条回复

  1. hwhphp hwhphp says:

    为什么我laravel5.8直接安装jwt失败了。报错意思是让我安装nesbot/carbon 1.26.3版本以上吗?不是会自动安装的吗? Problem 1

    • Installation request for tymon/jwt-auth ^0.5.12 -> satisfiable by tymon/jwt-auth[0.5.12].
    • Conclusion: remove nesbot/carbon 2.20.0
    • Conclusion: don't install nesbot/carbon 2.20.0
    • tymon/jwt-auth 0.5.12 requires nesbot/carbon ~1.0 -> satisfiable by nesbot/carbon[1.0.0, 1.0.1, 1.1.0, 1.10.0, 1.11.0, 1.12.0, 1.13.0, 1.14.0, 1.15.0, 1.16.0, 1.17.0, 1.18.0, 1.19.0, 1.2.0, 1.20.0, 1.21.0, 1.22.0, 1.22.1, 1.23.0, 1.24.0, 1.24.1, 1.24.2, 1.25.0, 1.25.1, 1.25.3, 1.26.0, 1.26.1, 1.26.2, 1.26.3, 1.26.4, 1.26.5, 1.26.6, 1.27.0, 1.28.0, 1.29.0, 1.29.1, 1.29.2, 1.3.0, 1.30.0, 1.31.0, 1.31.1, 1.32.0, 1.33.0, 1.34.0, 1.34.1, 1.34.2, 1.34.3, 1.34.4, 1.35.0, 1.35.1, 1.36.0, 1.36.1, 1.36.2, 1.37.0, 1.37.1, 1.38.0, 1.38.1, 1.38.2, 1.38.3, 1.38.4, 1.39.0, 1.4.0, 1.5.0, 1.6.0, 1.7.0, 1.8.0, 1.9.0].
    • Can only install one of: nesbot/carbon[1.26.3, 2.20.0].
  2. MrCPF MrCPF says:
    @ 小王八

    这是因为在你的JWT配置文件config/jwt.php中有这个NamshiAdapter,但是很可能你安装的JWT版本是1.0,1.0版本的名字有些变化,把NamshiAdapter改成Namshi,相应的还有几个需要更改,如'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,总之是版本的原因,不想改的话,把原来的配置文件删了,重新生成一个php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider",总之根据自己的版本来

  3. jkloozx jkloozx says:
    你是不是改了jwt版本但是没有发布新的jwt.php文件,执行下发布配置文件的命令就好了,不同版本的配置文件是不同的。
  4. 董飞 董飞 says:
    问题: FatalThrowableError Class 'Tymon\JWTAuth\Providers\JWT\Namshi' not found 1 我这边情况是,用户注册的时候,能拿到token。 以下是我注册用户的时候,返回的数据。 {"name":"\u8463\u98de\u6d4b","password":"$2y$10$oZvFaPVeoSGZ8N\/TEyfUOeT2Ik1H7jk7RrT16ew3bBFV7MyB9LMWi","phone":"15600589534","updated_at":"2018-05-25 12:29:18","created_at":"2018-05-25 12:29:18","id":44,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImQ3MmJmYjZmNjM5NDI0Y2ZjNzU4ZjFhMGM0Nzg5NDA2MGE1OTRmZmI2ZjJkOTNkMWE3Yjg1YWQ4Y2RjMDlmMWQ0OWY2NDUxNWMwNWU1MDQ5In0.eyJhdWQiOiIxIiwianRpIjoiZDcyYmZiNmY2Mzk0MjRjZmM3NThmMWEwYzQ3ODk0MDYwYTU5NGZmYjZmMmQ5M2QxYTdiODVhZDhjZGMwOWYxZDQ5ZjY0NTE1YzA1ZTUwNDkiLCJpYXQiOjE1MjcyMjI1NTgsIm5iZiI6MTUyNzIyMjU1OCwiZXhwIjoxNTU4NzU4NTU4LCJzdWIiOiI0NCIsInNjb3BlcyI6W119.ZyXhNw2yf_KX2JxufsH664m3ealQb9HVVXZ51QE414hi3xX7qKf_w72uX40jOBVITTAJvFcaBbCKEYqibOsNyWzATEzdEgboWoRa7VpE8zhrz-iHHxMAOfYRNvltpwCAWeAfo6f-MBn_ejEhwGVXJhTpveH3xjD2XmEnBDIneoqgXVYeUIEtDbc4irU0cUY9UI8jXSTpfdcCIwkZvvSX4cYfFpGjzmOAvxJ-kscLH_XthOwvRkM1ojNfzSWZq20DwQJHSV2_MNGlmzv9Usjl7eVvSkU6XL-tBMj6t-kQ7DW0prfj7LifaHB1NGsEGfQ-QgkT2hOPkbxvlDTBk5C_Qxw8uKY-u1hXe-p3WpBKXE84PPwOwjQtHiPP0bCSmv8V_xiKhN0OIawdiRd3SwhYqYyHAqclirIxpWSpTo_o85nqT-Z_LpPj64BFhMFMpfRYwu85IExhJnhc3VeCT1RbtNXn-yTCYQmWwSUBGyxE3qouiJsxGHGaxeL9q0FO-kRnmEliduwE3trDaKBJ25_Keq1jTlqunBM2rPC19H1rQv5TVjtfLCRRqJwX31uetoMrAb3811EhRM8t6jP75Md8nheZX8kunUtGJNC_qaSpfISjqJwUpHQFmaALLVd939vVfxYawgrNUmo6FzKlcH47zjhEysMiVleUHw-SacwQTJ8"} 2 但当我试图用token去拿用户数据的时候,报了这个错误。 3 有没有人遇到或者了解这个情况的。求教。万分感谢!
  5. 学院君 学院君 says:
    最新版本 Laravel 参考这篇教程:http://laravelacademy.org/post/9178.html
  6. 小王八 小王八 says:
    ReflectionException thrown with message "Class Tymon\JWTAuth\Providers\JWT\NamshiAdapter does not exist“ 这报错为啥啊 Stack Overflow都搜不到

登录后才能进行评论,立即登录?