laravel passport加密jwt格式的access_token中的sub(user_id)字段

在很多需求我們不希望別人知道用戶在我們表中的 user_id ;
但是又想用數據庫的自增 id 功能;
一般時候在取出用戶后加密 user_id 加密即可;
但是總有那么幾個不經意間就可能把我們的 user_id 暴露了;
比如說 laravel 的 passport ;

創建一個項目用于測試;

laravel new passport

安裝 passport;

composer require laravel/passport
php artisan migrate
php artisan passport:install

現在我們有了用于測試的 Clint;

Laravel\Passport\HasApiTokens Trait 添加到 App\User 模型中;

<?php

namespace App;

+ use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
+    use HasApiTokens, Notifiable;
    // ...
}

在 AuthServiceProvider 的 boot 方法中增加 Passport::routes() ;

<?php

namespace App\Providers;

+ use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * 注冊任何認證/授權服務。
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

+        Passport::routes();
    }
}

將 config/auth.php 中的 driver 改為 passport;

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

    'api' => [
-        'driver' => token',
+       'driver' => 'passport',
         'provider' => 'users',
    ],
],

創建測試用戶

php artisan make:seeder UsersTableSeeder
<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(App\User::class, 5)->create();
    }
}
php artisan db:seed --class=UsersTableSeeder

測試用戶也有了;

獲取下 access_token ;

我們拿著 access_token 去 jwt.io 解開看下;

可以看到 PAYLOAD 中的 sub 就是我們的未加密的用戶id;
下面就是將 user_id 加密的過程了;

既然是加密id;
那還需要安裝一個擴展包;
示例中我們使用laravel-hashids

composer require vinkla/hashids
php artisan vendor:publish --provider='Vinkla\Hashids\HashidsServiceProvider'

隨便配置下;
/config/hashids.php

      'main' => [
-            'salt' => 'your-salt-string',
-            'length' => 'your-length-integer',
+            'salt' => 'alsd2987vnvczx&^$%Tpweqfhkjn',
+            'length' => 20,
        ],

加密用戶id;
這里主要用到了 laravel 留的一個鉤子;
vendor/laravel/passport/src/Bridge/UserRepository.php

getUserEntityByUserCredentials 中會判斷 User 模型是否有 findForPassport 方法;
我們可以在此處加密;
app/User.php

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
use Hashids;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * 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',
    ];

    /**
     * 此處定義 getIdAttribute 是為了跳過模型把主鍵 id 轉成數字的操作
     *
     * @see \Illuminate\Database\Eloquent\Concerns\HasAttributes::getAttributeValue()
     *
     * @param $value
     * @return mixed
     */
    public function getIdAttribute($value)
    {
        return $value;
    }

    /**
     * 當獲取用戶的時候加密 user_id
     *
     * 下面這些方法是為了加密和解密 jwt 中的 user_id
     * @see \App\Extensions\Illuminate\Auth\ExtendedUserProvider::retrieveById()
     * @see \App\Models\User::findForPassport()
     * @see \App\Models\OauthAccessToken::setUserIdAttribute()
     *
     * @param string $email
     *
     * @return User
     */
    public function findForPassport($email): self
    {
        $user     = $this->where('email', $email)->first();
        $user->id = Hashids::encode($user->id);

        return $user;
    }
}

因為上面把 user_id 加密了;
而 oauth_access_tokens 表中的 user_id 是 int 類型;
所以我們需要在向 oauth_access_tokens 存儲數據的時候自動解密 user_id ;

php artisan make:model OauthAccessToken
<?php

namespace App;

use Laravel\Passport\Token;
use Vinkla\Hashids\Facades\Hashids;

class OauthAccessToken extends Token
{
    /**
     * 當向 oauth_access_tokens 表中存儲數據的時候解密 user_id
     *
     * 下面這些方法是為了加密和解密 jwt 中的 user_id
     * @see \App\Extensions\Illuminate\Auth\ExtendedUserProvider::retrieveById()
     * @see \App\Models\User::findForPassport()
     * @see \App\Models\OauthAccessToken::setUserIdAttribute()
     *
     * @param int|string $value
     */
    public function setUserIdAttribute($value): void
    {
        if (is_numeric($value)) {
            $this->attributes['user_id'] = $value;
        } else {
            $this->attributes['user_id'] = current(Hashids::decode($value));
        }
    }
}

覆蓋 passport 的 OauthAccessToken ;
app/Providers/AuthServiceProvider.php

<?php

namespace App\Providers;

use App\OauthAccessToken;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();

+        Passport::useTokenModel(OauthAccessToken::class);
    }
}

再獲取下 access_token ;

如我們所愿;
user_id 已經被加密了;
接著還需要處理的是當我們拿著這個token去訪問應用的時候能成功驗證;
并且可以正確的獲取到用戶;
定義ExtendedUserProvider 用于覆蓋 retrieveById 方法;
app/Extensions/Illuminate/Auth/ExtendedUserProvider.php

<?php

namespace App\Extensions\Illuminate\Auth;

use App\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\EloquentUserProvider;
use Vinkla\Hashids\Facades\Hashids;

class ExtendedUserProvider extends EloquentUserProvider
{
    /**
     * 當獲取用戶的時候解密 user_id
     *
     * 下面這些方法是為了加密和解密 jwt 中的 user_id
     * @see \App\Extensions\Illuminate\Auth\ExtendedUserProvider::retrieveById()
     * @see \App\User::findForPassport()
     * @see \App\OauthAccessToken::setUserIdAttribute()
     *
     * @param int|string $identifier
     * @return User
     * @throws AuthenticationException
     */
    public function retrieveById($identifier)
    {
        $model = $this->createModel();

        /**
         * If Id is a string, then we need to decrypt $identifier.
         *
         * @see \App\Models\User::findForPassport()
         */
        if (!is_numeric($identifier)) {
            $identifier = current(Hashids::decode($identifier));
        }

        return $model->newQuery()
            ->where($model->getAuthIdentifierName(), $identifier)
            ->first();
    }
}

修改配置項把 auth.providers.users.driver 改成
config/auth.php

    'providers' => [
        'users' => [
-            'driver' => 'extended',
+            'model' => App\User::class,
        ]
    ],

app/Providers/AuthServiceProvider.php

<?php

namespace App\Providers;

use App\Extensions\Illuminate\Auth\ExtendedUserProvider;
use App\OauthAccessToken;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
use Illuminate\Foundation\Application;
use Auth;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
        Passport::useTokenModel(OauthAccessToken::class);

        /**
         * Register @see \App\Extensions\Illuminate\Auth\ExtendedUserProvider
         */
        Auth::provider('extended', function ($app, $config) {
            $model = $config['model'];
            return new ExtendedUserProvider($app['hash'], $model);
        });
    }
}

使用 jwt 請求 /api/user 正確的通過的驗證

至此通過password模式的加密和解密已經完成了;
然鵝坑爹的是如果使用authorization code模式;
因為id是從 code 獲取的;
而不是從模型中獲取的;
因此我們需要在獲取 code 的步驟中加密 user_id ;
先來覆蓋原本的 AuthorizationController 中的approve方法;

php artisan make:controller Oauth/AuthorizationController
<?php

namespace App\Http\Controllers\Oauth;

use Illuminate\Http\Request;
use Laravel\Passport\Http\Controllers\ApproveAuthorizationController as Controller;
use Zend\Diactoros\Response as Psr7Response;
use Hashids;

class ApproveAuthorizationController extends Controller
{
    public function approve(Request $request)
    {
        return $this->withErrorHandling(function () use ($request) {
            $authRequest = $this->getAuthRequestFromSession($request);

            /**
             * Encrypt user_id
             *
             * @see \App\OauthAuthCode::setRawAttributes()
             */
            $user = $authRequest->getUser();
            $user->setIdentifier(Hashids::encode($user->getIdentifier()));

            return $this->convertResponse(
                $this->server->completeAuthorizationRequest($authRequest, new Psr7Response)
            );
        });
    }
}

覆蓋路由;
routes/web.php

/*
|--------------------------------------------------------------------------
| oauth
|--------------------------------------------------------------------------
*/
Route::prefix('oauth')->namespace('Oauth')->group(function () {
    Route::post('authorize', 'ApproveAuthorizationController@approve')->name('passport.authorizations.approve');
});

此處有一個坑爹的地方;
passport 在向 oauth_auth_code 表中存儲數據的時候使用了 setRawAttributes

以至于我們不能使用 setUserIdAttribute 來解密;
因此需要覆蓋 OauthAuthCode 模型的 setRawAttributes 方法用于解密;

php artisan make:model OauthAuthCode

app/OauthAuthCode.php

<?php

namespace App;

use Hashids;
use Laravel\Passport\AuthCode;

class OauthAuthCode extends AuthCode
{
    /**
     * 因為 laravel passport 在 @see \Laravel\Passport\Bridge\AuthCodeRepository@persistNewAuthCode() 中使用的 setRawAttributes ; 而 setRawAttributes 不能觸發 setUserIdAttribute ;所以不能使用 setUserIdAttribute 解密;需要 覆蓋 setRawAttributes 方法;
     *
     * @param array $attributes
     * @param bool  $sync
     *
     * @return $this
     */
    public function setRawAttributes($attributes, $sync = false)
    {
        /**
         * Decrypt user_id
         *
         * Encrypt user_id in @see \App\Http\Controllers\Oauth\AuthorizationController::authorize()
         */
        if (isset($attributes['user_id'])) {
            $attributes['user_id'] = current(Hashids::decode($attributes['user_id']));
        }

        return parent::setRawAttributes($attributes, $sync);
    }
}

app/Providers/AuthServiceProvider.php

<?php

namespace App\Providers;

use App\Extensions\Illuminate\Auth\ExtendedUserProvider;
use App\OauthAccessToken;
+ use App\OauthAuthCode;
use Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
        Passport::useTokenModel(OauthAccessToken::class);
+      Passport::useAuthCodeModel(OauthAuthCode::class);
        /**
         * Register @see \App\Extensions\Illuminate\Auth\ExtendedUserProvider
         */
        Auth::provider('extended', function ($app, $config) {
            $model = $config['model'];
            return new ExtendedUserProvider($app['hash'], $model);
        });
    }
}

authorization code模式需要網頁登錄;

php artisan make:auth

訪問 /login 登錄;

生成client;

php artisan passport:client


手動組一個獲取 code 的 url 并訪問;
http://passport.test/oauth/authorize?client_id=3&redirect_uri=http://passport.test/auth/callback&response_type=code&scope=

在回調地址 http://passport.test/auth/callback 頁面地址欄獲取 code ;
拿著 code 去換取 token ;

檢查 user_id 是否被加密;

檢測 token 是否可用;

一切按著劇本走的沒有什么問題;
我把示例代碼上傳到 github 上了;
如果自己按文章測試的過程中出現問題;
可用參考 https://github.com/baijunyao/laravel-passport-encrypt-user-id-demo

白俊遙博客
請先登錄后發表評論
  • latest comments
  • 總共2條評論
白俊遙博客

時間是一種解藥:大佬

2019-08-24 14:48:38 回復

白俊遙博客

亦森:怎么在這上面寫文章呀

2019-03-12 18:24:56 回復

欢乐时时彩官网-首页