Securing a Laravel API in 20 minutes with JWTs

JWT相比Passport来说,轻便,功能一样完备,性价比高。

参考JSON Web Token Authentication for Laravel & Lumen

先来安装jwt扩展包:

执行:

composer require tymon/jwt-auth

如果使用的Laravel是5.4及以下版本的;添加下面的代码 到config/app.php文件的providers配置数组中:

'providers' => [

    ...

    Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]

安装完成,然后【Publish the config】发布配置:

执行:

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

批注 2020-04-16 152427

现在生成了一个jwt.php文件在config文件夹下,可以用来对此扩展包进行基础配置。

打开文件,可以看到配置的值很多都是引用的.env文件中的配置。

批注 2020-04-16 152803

所以如果有需要的话,我们在env文件中键入值即可。

比如:

批注 2020-04-16 152919

最后,生成密钥:

执行:

php artisan jwt:secret

批注 2020-04-16 153050

会在.env文件中更新一个值:

批注 2020-04-16 153135

这个密钥将用于签名未来会用到的tokens。

参考:Quick start

接下来是User模型类:

模型类必须实现 use Tymon\JWTAuth\Contracts\JWTSubject;

批注 2020-04-16 153654

实现这个接口的两个方法:

批注 2020-04-16 153733

然后我们配置一下api访问的中间件设置

打开:config/auth.php 文件

修改为:

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

...

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

批注 2020-04-16 154105

接下来我们新建一个LoginController,默认的LoginController待会就不用了,用这个新的来测试一下;

执行:

php artisan make:controller Api\Auth\LoginController

批注 2020-04-16 155326

由于config/auth.php文件里有一个default配置是web:

批注 2020-04-16 154731

就是说,默认情况下,走的是web配置里的,虽然可以用auth:api的方式使用api的配置,不过还有一种方法,就是复制一份Controller类到Api文件夹下然后修改这个复制的Controller类的构造函数:

批注 2020-04-16 210448

构造函数中就是调用AuthManager中的setDefaultDriver方法:

批注 2020-04-16 210701

接下来配置新建的这个LoginController:

首先这个控制器继承的不是默认的Controller,而应该是我们刚才复制重构的那个Controller:

批注 2020-04-16 210827批注 2020-04-16 210904

新建一个login方法,我们暂时只测试处理登录后生成token这个操作:

public function login(Request $request)
{
    $credentials = $request->only(['email', 'password']);

    if (!$token = auth()->attempt($credentials)) {
        return response(['error' => 'Wrong Credentials!'], 401);
    }

    return response()->json(['data'=>$token])->setStatusCode(200);

}

接下来修改一下api.php文件:

Route::post('login', 'Api\Auth\LoginController@login')->name('login.api');

用postman测试结果:

失败时:

批注 2020-04-16 211710

成功时:

批注 2020-04-16 211746

参考之前教程 我们已经有task的model controller 数据库中也已经有数据了,

当然修改一下之前的TaskController:让TaskController继承新的Controller基类:

批注 2020-04-16 215705

并且也已经有phpunit,所以执行

.\vendor\bin\phpunit.bat
结果:

批注 2020-04-16 215126

同时使用postman用登录成功返回的token 发出请求:

批注 2020-04-16 215228

OK!

但是假如用户的token过期了怎么办? 要么要求用户登录 要么刷新一下token,但是刷新token之前,应该检查用户当前的token是不是最近的一个。如果不是最近的一个,提示blacklisted,前端视情况要求登录,如果是,那么刷新token就行。现在实现这个刷新token的方法:

LoginController中添加一个refresh方法:

public function refresh(Request $request)
{
    try {
        $token = auth()->refresh();
    } catch (TokenInvalidException $e) {
        return response()->json(['error' => $e->getMessage()])->setStatusCode(401);
    }
    return response()->json(['data' => $token])->setStatusCode(200);

}

注意这个auth()->refresh(); 其实是JWTGuard提供的:参考:Auth guard

批注 2020-04-16 221733

为了测试效果,api.php中添加一个route:

Route::post('refresh', 'Api\Auth\LoginController@refresh')->name('refresh.api');

打开postman:

批注 2020-04-16 222008

生成了新的token,如果继续用旧的token发送同样的请求,结果如下:

批注 2020-04-16 222118

TaskController.php:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Api\Controller;
use App\Http\Resources\TaskResource;
use App\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Validator;

/**
 * @group Tasks management
 *
 * APIs for managing tasks
 */
class TaskController extends Controller
{
    /**
     * Display a listing of the tasks.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
        $tasks = TaskResource::collection(auth()->user()->tasks()->with('user')->latest()->paginate(3));
        return response($tasks, 200);
    }

    /**
     * Store a newly created task in storage.
     * @bodyParam title string required 任务的标题. Example: 处理剩余的什麽
     * @bodyParam description text 任務的描述. Example:描述這個任務是做什麽
     * @bodyParam due string 任務的截至日.Example: next monday
     * @param \Illuminate\Http\Request $request
     * @response {
     *  "data": {
     *      "title": "due task",
     *      "description": "task with due date",
     *      "due": "2020-04-19 00:00:00",
     *      "user_id": 1,
     *      "updated_at": "2020-04-14T07:20:02.000000Z",
     *      "created_at": "2020-04-14T07:20:02.000000Z",
     *      "id": 11,
     *      "user": {
     *          "id": 1,
     *          "name": "user",
     *          "email": "user@user.com",
     *          "email_verified_at": null,
     *          "created_at": "2020-04-14T05:42:55.000000Z",
     *          "updated_at": "2020-04-14T05:42:55.000000Z",
     *          "deleted_at": null
     *    }
     *  }
     * }
     * @response 401 {
     *  "message":"The field is required."
     * }
     */
    public function store(Request $request)
    {
        $validated = Validator::make($request->all(), [
            'title' => 'required|max:255',
        ]);
        if ($validated->fails()) {
            return response($validated->errors()->all(), 401);
        }
        $input = $request->all();
        if ($request->has('due')) {
            $input['due'] = Carbon::parse($request->get('due'))->toDateTimeString();
        }
        $task = auth()->user()->tasks()->create($input);

        return new TaskResource($task->load('user'));
    }

    /**
     * Display the specified resource.
     * @urlParam task int required The ID of the task.
     * @response {
     *  "data": {
     *      "title": "due task",
     *      "description": "task with due date",
     *      "due": "2020-04-19 00:00:00",
     *      "user_id": 1,
     *      "updated_at": "2020-04-14T07:20:02.000000Z",
     *      "created_at": "2020-04-14T07:20:02.000000Z",
     *      "id": 11,
     *      "user": {
     *          "id": 1,
     *          "name": "user",
     *          "email": "user@user.com",
     *          "email_verified_at": null,
     *          "created_at": "2020-04-14T05:42:55.000000Z",
     *          "updated_at": "2020-04-14T05:42:55.000000Z",
     *          "deleted_at": null
     *    }
     *  }
     * }
     *
     * @response 404 {
     *  "message":"Not Found"
     * }
     */
    public function show(Task $task)
    {
        //
        return new TaskResource($task->load('user'));
    }

    /**
     * Update the specified resource in storage.
     * @urlParam task int required The ID of the task.
     * @bodyParam title string required 任务的标题. Example: 处理剩余的什麽
     * @bodyParam description text 任務的描述. Example:描述這個任務是做什麽
     * @bodyParam due string 任務的截至日.Example: next monday
     * @response {
     *  "data": {
     *      "title": "due task",
     *      "description": "task with due date",
     *      "due": "2020-04-19 00:00:00",
     *      "user_id": 1,
     *      "updated_at": "2020-04-14T07:20:02.000000Z",
     *      "created_at": "2020-04-14T07:20:02.000000Z",
     *      "id": 11,
     *      "user": {
     *          "id": 1,
     *          "name": "user",
     *          "email": "user@user.com",
     *          "email_verified_at": null,
     *          "created_at": "2020-04-14T05:42:55.000000Z",
     *          "updated_at": "2020-04-14T05:42:55.000000Z",
     *          "deleted_at": null
     *    }
     *  }
     * }
     *
     * @response 404 {
     *  "message":"Not Found"
     * }
     * @response 401 {
     *  "message":"The field is required."
     * }
     *
     * @response 422 {
     *  "message": "No permission!"
     * }
     * @param \Illuminate\Http\Request $request
     *
     *
     */
    public function update(Request $request, Task $task)
    {
        //
        $validated = Validator::make($request->all(), [
            'title' => 'required|max:255',
        ]);
        if ($validated->fails()) {
            return response($validated->errors()->all());
        }

        if (!auth()->user()->tasks->contains($task)) {
            return response('No permission!', 422);
        }
        $input = $request->all();

        if ($request->has('due')) {
            $input['due'] = Carbon::parse($request->get('due'))->toDateTimeString();
        }
        $task->update($input);

        return new TaskResource($task->load('user'));

    }

    /**
     * Remove the specified resource from storage.
     * @urlParam task int required The ID of the task.
     * @response {
     *  "message": "Success deleted!"
     *  }
     * @response 404 {
     *  "message":"Not Found"
     * }
     * @return \Illuminate\Http\Response
     */
    public function destroy(Task $task)
    {
        //
        if (!auth()->user()->tasks->contains($task)) {
            return response('No permission!', 422);
        }
        $task->delete();

        return response(['message' => 'Success deleted!']);
    }
}

Api.php:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

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

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

//Route::get('verified-only', function (Request $request) {
//    dd('You Are Verified!', $request->user()->name);
//})->middleware('auth:api', 'verified');

//Route::post('login', 'Api\AuthController@login')->name('login.api');

Route::post('register', 'Api\AuthController@register')->name('register.api');

Route::middleware('auth:api')->get('logout', 'Api\AuthController@logout')->name('logout.api');

Route::post('/password/email', 'Api\ForgotPasswordController@sendResetLinkEmail');
Route::post('/password/reset', 'Api\ResetPasswordController@reset');

Route::apiResource('tasks', 'TaskController')->middleware('auth:api');

Route::get('email/resend', 'Api\VerificationController@resend')->name('verification.resend');
Route::get('email/verify/{id}/{hash}', 'Api\VerificationController@verify')->name('verification.verify');


Route::post('login', 'Api\Auth\LoginController@login')->name('login.api');

User.php:

<?php

namespace App;

use App\Notifications\testPasswordResetEmailNotification;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements MustVerifyEmail, JWTSubject
{
    use Notifiable, HasApiTokens, SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function sendPasswordResetNotification($token)
    {
        $this->notify(new testPasswordResetEmailNotification($token));
    }

    public function tasks()
    {
        return $this->hasMany(Task::class);
    }

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

auth.php:

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session", "token"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
//            'driver' => 'token',
//            'driver' => 'passport',
            'driver' => 'jwt',
            'provider' => 'users',
            'hash' => false,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | You may specify multiple password reset configurations if you have more
    | than one user table or model in the application and you want to have
    | separate password reset settings based on the specific user types.
    |
    | The expire time is the number of minutes that the reset token should be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Password Confirmation Timeout
    |--------------------------------------------------------------------------
    |
    | Here you may define the amount of seconds before a password confirmation
    | times out and the user is prompted to re-enter their password via the
    | confirmation screen. By default, the timeout lasts for three hours.
    |
    */

    'password_timeout' => 10800,

];

LoginController:

<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Api\Controller;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;

class LoginController extends Controller
{
     //
     public function login(Request $request)
     {
         $credentials = $request->only(['email', 'password']);

        if (!$token = auth()->attempt($credentials)) {
             return response(['error' => 'Wrong Credentials!'], 401);
         }

        return response()->json(['data' => $token])->setStatusCode(200);
     }

    public function refresh(Request $request)
     {
         try {
             $token = auth()->refresh();
         } catch (TokenInvalidException $e) {
             return response()->json(['error' => $e->getMessage()])->setStatusCode(401);
         }
         return response()->json(['data' => $token])->setStatusCode(200);

    }
}

App\Http\Controllers\Api\Controller.php:

<?php

namespace App\Http\Controllers\Api;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

    public function __construct()
    {
        auth()->setDefaultDriver('api');
    }
}


20200417 update:

参照:https://jwt-auth.readthedocs.io/en/docs/quick-start/ Create the AuthController部分

更新:

<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Api\Controller;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;

class LoginController extends Controller
{
    //
    public function login(Request $request)
    {
        $credentials = $request->only(['email', 'password']);

        if (!$token = auth()->attempt($credentials)) {
            return response(['error' => 'Wrong Credentials!'], 401);
        }
        return $this->respondWithToken($token);
//        return response()->json(['data' => $token])->setStatusCode(200);
    }

    public function refresh(Request $request)
    {
        try {
            $token = auth()->refresh();
        } catch (TokenInvalidException $e) {
            return response()->json(['error' => $e->getMessage()])->setStatusCode(401);
        }
        return $this->respondWithToken($token);
//        return response()->json(['data' => $token])->setStatusCode(200);

    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}

测试登录结果如下:

批注 2020-04-17 010627

失败结果:

批注 2020-04-17 010658

posted @ 2020-04-16 15:54  dzkjz  阅读(220)  评论(0编辑  收藏  举报