zoukankan      html  css  js  c++  java
  • 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' => [
    
        ...
    
        TymonJWTAuthProvidersLaravelServiceProvider::class,
    ]

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

    执行:

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

    批注 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 TymonJWTAuthContractsJWTSubject;

    批注 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 ApiAuthLoginController

    批注 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', 'ApiAuthLoginController@login')->name('login.api');
    
    

    用postman测试结果:

    失败时:

    批注 2020-04-16 211710

    成功时:

    批注 2020-04-16 211746

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

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

    批注 2020-04-16 215705

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

    .vendorinphpunit.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', 'ApiAuthLoginController@refresh')->name('refresh.api');
    
    

    打开postman:

    批注 2020-04-16 222008

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

    批注 2020-04-16 222118

    TaskController.php:

    <?php
    
    namespace AppHttpControllers;
    
    use AppHttpControllersApiController;
    use AppHttpResourcesTaskResource;
    use AppTask;
    use IlluminateHttpRequest;
    use IlluminateSupportCarbon;
    use IlluminateSupportFacadesValidator;
    
    /**
     * @group Tasks management
     *
     * APIs for managing tasks
     */
    class TaskController extends Controller
    {
        /**
         * Display a listing of the tasks.
         *
         * @return IlluminateHttpResponse
         */
        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 IlluminateHttpRequest $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 IlluminateHttpRequest $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 IlluminateHttpResponse
         */
        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 IlluminateHttpRequest;
    use IlluminateSupportFacadesRoute;
    
    /*
    |--------------------------------------------------------------------------
    | 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', 'ApiAuthController@login')->name('login.api');
    
    Route::post('register', 'ApiAuthController@register')->name('register.api');
    
    Route::middleware('auth:api')->get('logout', 'ApiAuthController@logout')->name('logout.api');
    
    Route::post('/password/email', 'ApiForgotPasswordController@sendResetLinkEmail');
    Route::post('/password/reset', 'ApiResetPasswordController@reset');
    
    Route::apiResource('tasks', 'TaskController')->middleware('auth:api');
    
    Route::get('email/resend', 'ApiVerificationController@resend')->name('verification.resend');
    Route::get('email/verify/{id}/{hash}', 'ApiVerificationController@verify')->name('verification.verify');
    
    
    Route::post('login', 'ApiAuthLoginController@login')->name('login.api');
    
    

    User.php:

    <?php
    
    namespace App;
    
    use AppNotifications	estPasswordResetEmailNotification;
    use IlluminateContractsAuthMustVerifyEmail;
    use IlluminateDatabaseEloquentSoftDeletes;
    use IlluminateFoundationAuthUser as Authenticatable;
    use IlluminateNotificationsNotifiable;
    use LaravelPassportHasApiTokens;
    use TymonJWTAuthContractsJWTSubject;
    
    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' => AppUser::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 AppHttpControllersApiAuth;

    use AppHttpControllersApiController;
    use IlluminateHttpRequest;
    use TymonJWTAuthExceptionsJWTException;
    use TymonJWTAuthExceptionsTokenInvalidException;

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

        }
    }

    AppHttpControllersApiController.php:

    <?php
    
    namespace AppHttpControllersApi;
    
    use IlluminateFoundationAuthAccessAuthorizesRequests;
    use IlluminateFoundationBusDispatchesJobs;
    use IlluminateFoundationValidationValidatesRequests;
    use IlluminateRoutingController 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 AppHttpControllersApiAuth;
    
    use AppHttpControllersApiController;
    use IlluminateHttpRequest;
    use TymonJWTAuthExceptionsTokenInvalidException;
    
    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 IlluminateHttpJsonResponse
         */
        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

  • 相关阅读:
    Django使用Mysql已存在数据表的方法
    ajax加载验证码这样不断刷新的文件无法刷新问题
    flask ajax发送请求返回400
    flask启动找不到路由问题
    Python开发之路
    文件读写的简单应用
    sql 简单查询修改
    kafka 查询 SQL Query
    kafka条件查询excel拼接
    shelve模块
  • 原文地址:https://www.cnblogs.com/dzkjz/p/12713427.html
Copyright © 2011-2022 走看看