资讯专栏INFORMATION COLUMN

Laravel 5.5 使用 Jwt-Auth 实现 API 用户认证以及无痛刷新访问令牌

xavier / 926人阅读

摘要:默认的时间为周。大概意思就是如果用户有一个,那么他可以带着他的过来领取新的,直到周的时间后,他便无法继续刷新了,需要重新登录。指定在刷新令牌时要保留的声明密钥。为了使令牌无效,您必须启用黑名单。指定用于对用户进行身份验证的提供程序。

最近在做一个公司的项目,前端使用 Vue.js,后端使用 Laravel 构建 Api 服务,用户认证的包本来是想用 Laravel Passport 的,但是感觉有点麻烦,于是使用了 jwt-auth 。

安装

jwt-auth 最新版本是 1.0.0 rc.1 版本,已经支持了 Laravel 5.5。如果你使用的是 Laravel 5.5 版本,可以使用如下命令安装。根据评论区 @tradzero 兄弟的建议,如果你是 Laravel 5.5 以下版本,也推荐使用最新版本,RC.1 前的版本都存在多用户token认证的安全问题。

$ composer require tymon/jwt-auth 1.0.0-rc.1
配置 添加服务提供商

将下面这行添加至 config/app.php 文件 providers 数组中:

app.php

"providers" => [

    ...

    TymonJWTAuthProvidersLaravelServiceProvider::class,
]
发布配置文件

在你的 shell 中运行如下命令发布 jwt-auth 的配置文件:

shell

$ php artisan vendor:publish --provider="TymonJWTAuthProvidersLaravelServiceProvider"

此命令会在 config 目录下生成一个 jwt.php 配置文件,你可以在此进行自定义配置。

生成密钥

jwt-auth 已经预先定义好了一个 Artisan 命令方便你生成 Secret,你只需要在你的 shell 中运行如下命令即可:

shell

$ php artisan jwt:secret

此命令会在你的 .env 文件中新增一行 JWT_SECRET=secret

配置 Auth guard

config/auth.php 文件中,你需要将 guards/driver 更新为 jwt

auth.php

"defaults" => [
    "guard" => "api",
    "passwords" => "users",
],

...

"guards" => [
    "api" => [
        "driver" => "jwt",
        "provider" => "users",
    ],
],
只有在使用 Laravel 5.2 及以上版本的情况下才能使用。
更改 Model

如果需要使用 jwt-auth 作为用户认证,我们需要对我们的 User 模型进行一点小小的改变,实现一个接口,变更后的 User 模型如下:

User.php

getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}
配置项详解

jwt.php

 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" => TymonJWTAuthProvidersJWTNamshi::class,

        /*
        |--------------------------------------------------------------------------
        | Authentication Provider
        |--------------------------------------------------------------------------
        |
        | 指定用于对用户进行身份验证的提供程序。
        |
        */

        "auth" => TymonJWTAuthProvidersAuthIlluminate::class,

        /*
        |--------------------------------------------------------------------------
        | Storage Provider
        |--------------------------------------------------------------------------
        |
        | 指定用于在黑名单中存储标记的提供程序。
        |
        */

        "storage" => TymonJWTAuthProvidersStorageIlluminate::class,

    ],

];
自定义认证中间件

先来说明一下我想要达成的效果,我希望用户提供账号密码前来登录。如果登录成功,那么我会给前端颁发一个 access _token ,设置在 header 中以请求需要用户认证的路由。

同时我希望如果用户的令牌如果过期了,可以暂时通过此次请求,并在此次请求中刷新该用户的 access _token,最后在响应头中将新的 access _token 返回给前端,这样子可以无痛的刷新 access _token ,用户可以获得一个很良好的体验,所以开始动手写代码。

执行如下命令以新建一个中间件:

php artisan make:middleware RefreshToken

中间件代码如下:

RefreshToken.php

checkForToken($request);

       // 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException  异常
        try {
            // 检测用户的登录状态,如果正常则通过
            if ($this->auth->parseToken()->authenticate()) {
                return $next($request);
            }
            throw new UnauthorizedHttpException("jwt-auth", "未登录");
        } catch (TokenExpiredException $exception) {
          // 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中
            try {
                // 刷新用户的 token
                $token = $this->auth->refresh();
               // 使用一次性登录以保证此次请求的成功
                Auth::guard("api")->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()["sub"]);
            } catch (JWTException $exception) {
               // 如果捕获到此异常,即代表 refresh 也过期了,用户无法刷新令牌,需要重新登录。
                throw new UnauthorizedHttpException("jwt-auth", $exception->getMessage());
            }
        }
        
        // 在响应头中返回新的 token
        return $this->setAuthenticationHeader($next($request), $token);
    }
}
设置 Axios 拦截器

我选用的 HTTP 请求套件是 axios。为了达到无痛刷新 token 的效果,我们需要对 axios 定义一个拦截器,用以接收我们刷新的 Token,代码如下:

app.js

import Vue from "vue"
import router from "./router"
import store from "./store"
import iView from "iview"
import "iview/dist/styles/iview.css"

Vue.use(iView)


new Vue({
    el: "#app",
    router,
    store,
    created() {
        // 自定义的 axios 响应拦截器
        this.$axios.interceptors.response.use((response) => {
            // 判断一下响应中是否有 token,如果有就直接使用此 token 替换掉本地的 token。你可以根据你的业务需求自己编写更新 token 的逻辑
            var token = response.headers.authorization
            if (token) {
                // 如果 header 中存在 token,那么触发 refreshToken 方法,替换本地的 token
                this.$store.dispatch("refreshToken", token)
            }
            return response
        }, (error) => {
            switch (error.response.status) {
                
                // 如果响应中的 http code 为 401,那么则此用户可能 token 失效了之类的,我会触发 logout 方法,清除本地的数据并将用户重定向至登录页面
                case 401:
                    return this.$store.dispatch("logout")
                    break
                // 如果响应中的 http code 为 400,那么就弹出一条错误提示给用户
                case 400:
                    return this.$Message.error(error.response.data.error)
                    break
            }
            return Promise.reject(error)
        })
    }
})

Vuex 内的代码如下:

store/index.js

import Vue from "vue"
import Vuex from "vuex"
import axios from "axios"

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        name: null,
        avatar: null,
        mobile: null,
        token: null,
        remark: null,
        auth: false,
    },
    mutations: {
        // 用户登录成功,存储 token 并设置 header 头
        logined(state, token) {
            state.auth = true
            state.token = token
            localStorage.token = token
        },
        // 用户刷新 token 成功,使用新的 token 替换掉本地的token
        refreshToken(state, token) {
            state.token = token
            localStorage.token = token
            axios.defaults.headers.common["Authorization"] = state.token
        },
        // 登录成功后拉取用户的信息存储到本地
        profile(state, data) {
            state.name = data.name
            state.mobile = data.mobile
            state.avatar = data.avatar
            state.remark = data.remark
        },
        // 用户登出,清除本地数据
        logout(state){
            state.name = null
            state.mobile = null
            state.avatar = null
            state.remark = null
            state.auth = false
            state.token = null

            localStorage.removeItem("token")
        }
    },
    actions: {
         // 登录成功后保存用户信息
        logined({dispatch,commit}, token) {
            return new Promise(function (resolve, reject) {
                commit("logined", token)
                axios.defaults.headers.common["Authorization"] = token
                dispatch("profile").then(() => {
                    resolve()
                }).catch(() => {
                    reject()
                })
            })
        },
        // 登录成功后使用 token 拉取用户的信息
        profile({commit}) {
            return new Promise(function (resolve, reject) {
                axios.get("profile", {}).then(respond => {
                    if (respond.status == 200) {
                        commit("profile", respond.data)
                        resolve()
                    } else {
                        reject()
                    }
                })
            })
        },
        // 用户登出,清除本地数据并重定向至登录页面
        logout({commit}) {
            return new Promise(function (resolve, reject) {
                commit("logout")
                axios.post("auth/logout", {}).then(respond => {
                    Vue.$router.push({name:"login"})
                })
            })
        },
        // 将刷新的 token 保存至本地
        refreshToken({commit},token) {
            return new Promise(function (resolve, reject) {
                commit("refreshToken", token)
            })
        },
    }
})
更新异常处理的 Handler

由于我们构建的是 api 服务,所以我们需要更新一下 app/Exceptions/Handler.php 中的 render

方法,自定义处理一些异常。

Handler.php

 array_first(array_collapse($exception->errors()))], 400);
        }
        // 用户认证的异常,我们需要返回 401 的 http code 和错误信息
        if ($exception instanceof UnauthorizedHttpException) {
            return response($exception->getMessage(), 401);
        }

        return parent::render($request, $exception);
    }
}

更新完此方法后,我们上面自定义的中间件里抛出的异常和我们下面参数验证错误抛出的异常都会被转为指定的格式抛出。

使用

现在,我们可以在我们的 routes/api.php 路由文件中新增几条路由来测试一下了:

api.php

Route::prefix("auth")->group(function($router) {
    $router->post("login", "AuthController@login");
    $router->post("logout", "AuthController@logout");


});

Route::middleware("refresh.token")->group(function($router) {
    $router->get("profile","UserController@profile");
});

在你的 shell 中运行如下命令以新增一个控制器:

$ php artisan make:controller AuthController

打开此控制器,写入如下内容

 [
                "required",
                "exists:users",
            ],
            "password" => "required|string|min:6|max:20",
         ];
          
        // 验证参数,如果验证失败,则会抛出 ValidationException 的异常
        $params = $this->validate($request, $rules);

       // 使用 Auth 登录用户,如果登录成功,则返回 201 的 code 和 token,如果登录失败则返回
        return ($token = Auth::guard("api")->attempt($params))
            ? response(["token" => "bearer " . $token], 201)
            : response(["error" => "账号或密码错误"], 400);
    }

    /**
     * 处理用户登出逻辑
     *
     * @return IlluminateHttpJsonResponse
     */
    public function logout()
    {
        Auth::guard("api")->logout();

        return response(["message" => "退出成功"]);
    }
}

然后我们进入 tinker

$ php artisan tinker

执行以下命令来创建一个测试用户,我这里的用户名是用的是手机号码,你可以自行替换为邮箱。别忘了设置命名空间哟:

>>> namespace AppModels;
>>> User::create(["name" => "Test","mobile" => 17623239881,"password" => bcrypt("123456")]);

正确执行结果如下图:

然后打开 Postman 来进行 api 测试

正确的请求结果如下图:

可以看到我们已经成功的拿到了 token,接下来我们就去验证一下刷新 token 吧

如图可以看到我们已经拿到了新的 token,接下来的事情便会交由我们前面设置的 axios 拦截器处理,它会将本地的 token 替换为此 token。

版本科普

感觉蛮多人对版本没什么概念,所以在这里科普下常见的版本。

α(Alpha)版

​ 这个版本表示该 Package 仅仅是一个初步完成品,通常只在开发者内部交流,也有很少一部分发布给专业测试人员。一般而言,该版本软件的 Bug 较多,普通用户最好不要安装。

β(Beta)版

该版本相对于 α(Alpha)版已有了很大的改进,修复了严重的错误,但还是存在着一些缺陷,需要经过大规模的发布测试来进一步消除。通过一些专业爱好者的测试,将结果反馈给开发者,开发者们再进行有针对性的修改。该版本也不适合一般用户安装。

RC/ Preview版

RC 即 Release Candidate 的缩写,作为一个固定术语,意味着最终版本准备就绪。一般来说 RC 版本已经完成全部功能并清除大部分的 BUG。一般到了这个阶段 Package 的作者只会修复 Bug,不会对软件做任何大的更改。

普通发行版本

一般在经历了上面三个版本后,作者会推出此版本。此版本修复了绝大部分的 Bug,并且会维护一定的时间。(时间根据作者的意愿而决定,例如 Laravel 的一般发行版本会提供为期一年的维护支持。)

LTS(Long Term Support) 版

该版本是一个特殊的版本,和普通版本旨在支持比正常时间更长的时间。(例如 Laravel 的 LTS 版本会提供为期三年的 维护支持。)

结语

jwt-auth 确实是一个很棒的用户认证 Package,配置简单,使用方便。

文章结束,感谢阅读。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/26275.html

相关文章

  • PHP / Laravel API 开发推荐阅读清单

    showImg(https://segmentfault.com/img/bV6aHV?w=1280&h=800); 社区优秀文章 Laravel 5.5+passport 放弃 dingo 开发 API 实战,让 API 开发更省心 - 自造车轮。 API 文档神器 Swagger 介绍及在 PHP 项目中使用 - API 文档撰写方案 推荐 Laravel API 项目必须使用的 8 个...

    shmily 评论0 收藏0
  • Laravel 5.5 使用 Passport 实现 Auth 认证

    摘要:最近在写一个前后端分离项目,本来想用开发的,但是略感笨重,于是想到了的和新出的。更改看守器驱动将配置文件中授权看守器的的选项改为。然后在你的前端请求里需要加入一个参数然后在你需要使用到认证的路由里使用中间件,一切就大功告成啦 最近在写一个前后端分离项目,本来想用 Jwt-auth + Dingo 开发的,但是略感笨重,于是想到了 Laravel 的 Passport 和 5.5 新出的...

    miguel.jiang 评论0 收藏0
  • API 开发中可选择传递 token 接口遇到的一个坑

    摘要:此处做刷新处理具体代码可以参考必须需要登录验证的接口在响应头中返回新的问题解决。 在做 API 开发时,不可避免会涉及到登录验证,我使用的是jwt-auth 在登录中会经常遇到一个token过期的问题,在config/jwt.php默认设置中,这个过期时间是一个小时,不过为了安全也可以设置更小一点,我设置了为五分钟。 五分钟过期,如果就让用户去登录,这种体验会让用户直接抛弃你的网站...

    siberiawolf 评论0 收藏0
  • laravel使用JWT做API认证

    摘要:最近项目做认证,最终技术选型决定使用,项目框架使用的是,使用有比较方便使用的开源包。使用安装,使用的框架版本为,最新稳定版本为。 最近项目做API认证,最终技术选型决定使用JWT,项目框架使用的是laravel,laravel使用JWT有比较方便使用的开源包:jwt-auth。 使用composer安装jwt-auth,laravel使用的框架版本为5.0,jwt-auth最新稳定版本...

    SexySix 评论0 收藏0
  • Laravel Passport里的授权类型介绍

    摘要:模糊授权,跟上面的认证码授权类似,不同的是,我们的资源服务器,返回的直接就是准入令牌,而不是认证码。 本文来自pilishen.com----原文链接; 欢迎来和pilishen一起学习php&Laravel;学习群:109256050 OAuth2是一个安全框架,控制着程序受保护部分的准入,主要是控制不同的客户端如何来调取API,保证它们在请求相应资源的时候有相应的权限。 Larav...

    RobinTang 评论0 收藏0

发表评论

0条评论

xavier

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<