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"
现在生成了一个jwt.php文件在config文件夹下,可以用来对此扩展包进行基础配置。
打开文件,可以看到配置的值很多都是引用的.env文件中的配置。
所以如果有需要的话,我们在env文件中键入值即可。
比如:
最后,生成密钥:
执行:
php artisan jwt:secret
会在.env文件中更新一个值:
这个密钥将用于签名未来会用到的tokens。
参考:Quick start
接下来是User模型类:
模型类必须实现 use Tymon\JWTAuth\Contracts\JWTSubject;
实现这个接口的两个方法:
然后我们配置一下api访问的中间件设置
打开:config/auth.php 文件
修改为:
'defaults' => [ 'guard' => 'api', 'passwords' => 'users', ], ... 'guards' => [ 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], ],
接下来我们新建一个LoginController,默认的LoginController待会就不用了,用这个新的来测试一下;
执行:
php artisan make:controller Api\Auth\LoginController
由于config/auth.php文件里有一个default配置是web:
就是说,默认情况下,走的是web配置里的,虽然可以用auth:api的方式使用api的配置,不过还有一种方法,就是复制一份Controller类到Api文件夹下然后修改这个复制的Controller类的构造函数:
构造函数中就是调用AuthManager中的setDefaultDriver方法:
接下来配置新建的这个LoginController:
首先这个控制器继承的不是默认的Controller,而应该是我们刚才复制重构的那个Controller:
新建一个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测试结果:
失败时:
成功时:
参考之前教程 我们已经有task的model controller 数据库中也已经有数据了,
当然修改一下之前的TaskController:让TaskController继承新的Controller基类:
并且也已经有phpunit,所以执行
.\vendor\bin\phpunit.bat
同时使用postman用登录成功返回的token 发出请求:
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
为了测试效果,api.php中添加一个route:
Route::post('refresh', 'Api\Auth\LoginController@refresh')->name('refresh.api');
打开postman:
生成了新的token,如果继续用旧的token发送同样的请求,结果如下:
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 ]); } }
测试登录结果如下:
失败结果: