Эх сурвалжийг харах

自定义 User Provider 和 Guard 组件实现基于微服务接口的用户认证

chenlong 4 жил өмнө
parent
commit
49298fc452

+ 4 - 12
app/Http/Controllers/Auth/LoginController.php

@@ -54,26 +54,18 @@ class LoginController extends Controller
     {
         $this->validateLogin($request);
 
-        // If the class is using the ThrottlesLogins trait, we can automatically throttle
-        // the login attempts for this application. We'll key this by the username and
-        // the IP address of the client making these requests into this application.
         if ($this->hasTooManyLoginAttempts($request)) {
             $this->fireLockoutEvent($request);
-
             return $this->sendLockoutResponse($request);
         }
 
-        $details = $request->only('email', 'password');
-        $details['status'] = 1;
-        if (auth()->attempt($details)) {
-            return $this->sendLoginResponse($request);
+        $credentials = $request->only('email', 'password');
+        if ($token = auth()->login($credentials)) {
+            $this->clearLoginAttempts($request);
+            return redirect()->route('user.profile')->cookie('jwt_token', $token);
         }
 
-        // If the login attempt was unsuccessful we will increment the number of attempts
-        // to login and redirect the user back to the login form. Of course, when this
-        // user surpasses their maximum number of attempts they will get locked out.
         $this->incrementLoginAttempts($request);
-
         return $this->sendFailedLoginResponse($request);
     }
 }

+ 7 - 5
app/Http/Controllers/Auth/RegisterController.php

@@ -8,6 +8,7 @@ use App\MicroApi\Services\UserService;
 use App\Http\Controllers\Controller;
 use App\Shop\Customers\Requests\RegisterCustomerRequest;
 use Illuminate\Foundation\Auth\RegistersUsers;
+use Illuminate\Support\Facades\Auth;
 
 class RegisterController extends Controller
 {
@@ -47,10 +48,11 @@ class RegisterController extends Controller
     public function register(RegisterCustomerRequest $request)
     {
         $data = $request->except('_method', '_token');
-        $user = $this->create($data);
-        $token = $this->userService->auth($data);  // 获取 Token
-        session([md5($token) => $user]);  // 存储用户信息
-
-        return redirect()->route('user.profile')->cookie('jwt-token', $token);
+        if ($user = $this->create($data)) {
+            $token = Auth::login($data);
+            return redirect()->route('user.profile')->cookie('jwt_token', $token);
+        } else {
+            throw new AuthenticationException('注册失败,请重试');
+        }
     }
 }

+ 1 - 0
app/Http/Controllers/Front/AccountsController.php

@@ -72,6 +72,7 @@ class AccountsController extends Controller
 
     public function profile(Request $request)
     {
+        dd($request->user());
         $token = $request->cookie('jwt-token');
         if (empty($token) || !$this->userService->isAuth($token)) {
             return '尚未登录';

+ 98 - 1
app/MicroApi/Items/UserItem.php

@@ -2,11 +2,108 @@
 # app/MicroApi/Services/ResponseHandler.php
 namespace App\MicroApi\Items;
 
-class UserItem
+use Illuminate\Contracts\Auth\Authenticatable;
+
+class UserItem implements Authenticatable
 {
     public $id;
     public $name;
     public $email;
     public $password;
     public $status;
+
+    protected $hidden = ['password'];
+
+    /**
+     * 将 JSON 序列化对象数据填充到 UserItem
+     */
+    public function fillAttributes($data)
+    {
+        if (is_object($data)) {
+            $data = get_object_vars($data);
+        }
+        foreach ($data as $key => $value) {
+            if (in_array($key, $this->hidden)) {
+                continue;
+            }
+            switch ($key) {
+                case 'id':
+                    $this->id = $value;
+                    break;
+                case 'name':
+                    $this->name = $value;
+                    break;
+                case 'email':
+                    $this->email = $value;
+                    break;
+                case 'status':
+                    $this->status = $value;
+                    break;
+                default:
+                    break;
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * Get the name of the unique identifier for the user.
+     *
+     * @return string
+     */
+    public function getAuthIdentifierName()
+    {
+        return 'id';
+    }
+
+    /**
+     * Get the unique identifier for the user.
+     *
+     * @return mixed
+     */
+    public function getAuthIdentifier()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Get the password for the user.
+     *
+     * @return string
+     */
+    public function getAuthPassword()
+    {
+        // TODO: Implement getAuthPassword() method.
+    }
+
+    /**
+     * Get the token value for the "remember me" session.
+     *
+     * @return string
+     */
+    public function getRememberToken()
+    {
+        // TODO: Implement getRememberToken() method.
+    }
+
+    /**
+     * Set the token value for the "remember me" session.
+     *
+     * @param  string $value
+     * @return void
+     */
+    public function setRememberToken($value)
+    {
+        // TODO: Implement setRememberToken() method.
+    }
+
+    /**
+     * Get the column name for the "remember me" token.
+     *
+     * @return string
+     */
+    public function getRememberTokenName()
+    {
+        // TODO: Implement getRememberTokenName() method.
+    }
 }

+ 14 - 0
app/Providers/AppServiceProvider.php

@@ -6,6 +6,9 @@ use Illuminate\Support\ServiceProvider;
 use Laravel\Cashier\Cashier;
 use GuzzleHttp\Client as HttpClient;
 use App\MicroApi\Services\UserService;
+use Illuminate\Support\Facades\Auth;
+use App\Services\Auth\MicroUserProvider;
+use App\Services\Auth\JwtGuard;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -17,6 +20,17 @@ class AppServiceProvider extends ServiceProvider
     public function boot()
     {
         Cashier::useCurrency(config('cart.currency'), config('cart.currency_symbol'));
+        // 扩展 User Provider
+        Auth::provider('micro', function($app, array $config) {
+            // 返回一个Illuminate\Contracts\Auth\UserProvider实例...
+            return new MicroUserProvider($config['model']);
+        });
+
+        // 扩展 Auth Guard
+        Auth::extend('jwt', function($app, $name, array $config) {
+            // 返回一个Illuminate\Contracts\Auth\Guard实例...
+            return new JwtGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
+        });
     }
 
     /**

+ 156 - 0
app/Services/Auth/JwtGuard.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace App\Services\Auth;
+
+use App\MicroApi\Items\UserItem;
+use Illuminate\Auth\GuardHelpers;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Illuminate\Http\Request;
+use Illuminate\Contracts\Auth\Guard;
+use Illuminate\Contracts\Auth\UserProvider;
+
+class JwtGuard implements Guard
+{
+    use GuardHelpers;
+
+    /**
+     * The request instance.
+     *
+     * @var \Illuminate\Http\Request
+     */
+    protected $request;
+
+    /**
+     * The name of the query string item from the request containing the API token.
+     *
+     * @var string
+     */
+    protected $inputKey;
+
+    /**
+     * The name of the token "column" in persistent storage.
+     *
+     * @var string
+     */
+    protected $storageKey;
+
+    /**
+     * Create a new authentication guard.
+     *
+     * @param  \Illuminate\Contracts\Auth\UserProvider  $provider
+     * @param  \Illuminate\Http\Request  $request
+     * @param  string  $inputKey
+     * @param  string  $storageKey
+     * @return void
+     */
+    public function __construct(UserProvider $provider, Request $request, $inputKey = 'jwt_token', $storageKey = 'jwt_token')
+    {
+        $this->request = $request;
+        $this->provider = $provider;
+        $this->inputKey = $inputKey;
+        $this->storageKey = $storageKey;
+    }
+
+    /**
+     * Get the currently authenticated user.
+     *
+     * @return \Illuminate\Contracts\Auth\Authenticatable|null
+     */
+    public function user()
+    {
+        // If we've already retrieved the user for the current request we can just
+        // return it back immediately. We do not want to fetch the user data on
+        // every call to this method because that would be tremendously slow.
+        if (!is_null($this->user)) {
+            return $this->user;
+        }
+
+        $user = null;
+        $token = $this->getTokenForRequest();
+
+        if (!empty($token)) {
+            $user = $this->provider->retrieveByToken(null, $token);
+        }
+
+        return $this->user = $user;
+    }
+
+    /**
+     * Attempt to authenticate a user using the given credentials.
+     *
+     * @param  array  $credentials
+     * @return Authenticatable|null
+     */
+    public function login(array $credentials)
+    {
+        $token = $this->provider->retrieveByCredentials($credentials);
+
+        // If an implementation of UserInterface was returned, we'll ask the provider
+        // to validate the user against the given credentials, and if they are in
+        // fact valid we'll log the users into the application and return true.
+        if ($token) {
+            $user = $this->provider->retrieveByToken(null, $token);
+            $this->setUser($user);
+        }
+
+        return $token;
+    }
+
+    /**
+     * Get the token for the current request.
+     *
+     * @return string
+     */
+    public function getTokenForRequest()
+    {
+        $token = $this->request->query($this->inputKey);
+
+        if (empty($token)) {
+            $token = $this->request->input($this->inputKey);
+        }
+
+        if (empty($token)) {
+            $token = $this->request->bearerToken();
+        }
+
+        if (empty($token)) {
+            $token = $this->request->cookie($this->inputKey);
+        }
+
+        return $token;
+    }
+
+    /**
+     * Validate a user's credentials.
+     *
+     * @param  array  $credentials
+     * @return bool
+     */
+    public function validate(array $credentials = [])
+    {
+        if (empty($credentials[$this->inputKey])) {
+            return false;
+        }
+
+        $credentials = [$this->storageKey => $credentials[$this->inputKey]];
+
+        if ($this->provider->validateCredentials(new UserItem, $credentials)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Set the current request instance.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return $this
+     */
+    public function setRequest(Request $request)
+    {
+        $this->request = $request;
+
+        return $this;
+    }
+}

+ 167 - 0
app/Services/Auth/MicroUserProvider.php

@@ -0,0 +1,167 @@
+<?php
+
+namespace App\Services\Auth;
+
+use App\MicroApi\Exceptions\RpcException;
+use App\MicroApi\Items\UserItem;
+use App\MicroApi\Services\UserService;
+use Firebase\JWT\JWT;
+use Illuminate\Auth\AuthenticationException;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Illuminate\Contracts\Auth\UserProvider;
+
+class MicroUserProvider implements UserProvider
+{
+    /**
+     * @var UserService
+     */
+    protected $userService;
+
+    /**
+     * The auth user model.
+     *
+     * @var string
+     */
+    protected $model;
+
+    /**
+     * Create a new auth user provider.
+     *
+     * @param  string  $model
+     * @return void
+     */
+    public function __construct($model)
+    {
+        $this->model = $model;
+        $this->userService = resolve('microUserService');
+    }
+
+    /**
+     * Retrieve a user by their unique identifier.
+     *
+     * @param  mixed $identifier
+     * @return \Illuminate\Contracts\Auth\Authenticatable|null
+     * @throws RpcException
+     */
+    public function retrieveById($identifier)
+    {
+        $user = $this->userService->getById($identifier);
+        if ($user) {
+            $model = $this->createModel();
+            $model->fillAttributes($user);
+        } else {
+            $model = null;
+        }
+        return $model;
+    }
+
+    /**
+     * Retrieve a user by their unique identifier and "remember me" token.
+     *
+     * @param  mixed $identifier
+     * @param  string $token
+     * @return \Illuminate\Contracts\Auth\Authenticatable|null
+     */
+    public function retrieveByToken($identifier, $token)
+    {
+        $model = $this->createModel();
+        $data = JWT::decode($token, config('services.micro.jwt_key'), [config('services.micro.jwt_algorithms')]);
+        if ($data->exp <= time()) {
+            return null;  // Token 过期
+        }
+        $model->fillAttributes($data->User);
+        return $model;
+    }
+
+    /**
+     * Update the "remember me" token for the given user in storage.
+     *
+     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
+     * @param  string $token
+     * @return void
+     */
+    public function updateRememberToken(Authenticatable $user, $token)
+    {
+        // TODO: Implement updateRememberToken() method.
+    }
+
+    /**
+     * Retrieve a user by the given credentials.
+     *
+     * @param  array $credentials
+     * @return string
+     */
+    public function retrieveByCredentials(array $credentials)
+    {
+        if (empty($credentials) ||
+            (count($credentials) === 1 &&
+                array_key_exists('password', $credentials))) {
+            return;
+        }
+
+        try {
+            $token = $this->userService->auth($credentials);
+        } catch (RpcException $exception) {
+            throw new AuthenticationException("认证失败:邮箱和密码不匹配");
+        }
+
+        return $token;
+    }
+
+    /**
+     * Validate a user against the given credentials.
+     *
+     * @param  \Illuminate\Contracts\Auth\Authenticatable $user
+     * @param  array $credentials
+     * @return bool
+     */
+    public function validateCredentials(Authenticatable $user, array $credentials)
+    {
+        if (empty($credentials['token'])) {
+            return false;
+        }
+
+        try {
+            $valid = $this->userService->isAuth($credentials['token']);
+        } catch (RpcException $exception) {
+            throw new AuthenticationException("认证失败:令牌失效,请重新认证");
+        }
+
+        return $valid;
+    }
+
+    /**
+     * Create a new instance of the model.
+     *
+     * @return UserItem
+     */
+    public function createModel()
+    {
+        $class = '\\'.ltrim($this->model, '\\');
+
+        return new $class;
+    }
+
+    /**
+     * Gets the name of the Eloquent user model.
+     *
+     * @return string
+     */
+    public function getModel()
+    {
+        return $this->model;
+    }
+
+    /**
+     * Sets the name of the Eloquent user model.
+     *
+     * @param  string  $model
+     * @return $this
+     */
+    public function setModel($model)
+    {
+        $this->model = $model;
+
+        return $this;
+    }
+}

+ 4 - 3
composer.json

@@ -11,19 +11,20 @@
         "binarytorch/larecipe": "^1.2",
         "doctrine/dbal": "^2.5",
         "fideloper/proxy": "~4.0",
-        "kalnoy/nestedset": "^4.3",
+        "firebase/php-jwt": "^5.2",
+        "gloudemans/shoppingcart": "dev-master",
         "guzzlehttp/guzzle": "^6.3",
         "jsdecena/baserepo": "^1.0",
         "jsdecena/mailchimp": "~7.0",
         "jsdecena/mcpro": "1.1.*",
+        "kalnoy/nestedset": "^4.3",
         "laravel/cashier": "~7.0",
         "laravel/framework": "5.7.*|^6.0|^7.0|^8.0",
         "laravel/tinker": "~1.0",
         "nicolaslopezj/searchable": "^1.10",
         "paypal/rest-api-sdk-php": "*",
         "santigarcor/laratrust": "5.0.*",
-        "shippo/shippo-php": "^1.4",
-        "gloudemans/shoppingcart": "dev-master"
+        "shippo/shippo-php": "^1.4"
     },
     "require-dev": {
         "fzaninotto/faker": "~1.4",

+ 55 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "82c07f5ea08028f7c8e45d20b9965c25",
+    "content-hash": "ce2fb9151a5b29afa4b1e2ed2f5c7579",
     "packages": [
         {
             "name": "barryvdh/laravel-dompdf",
@@ -1012,6 +1012,60 @@
             },
             "time": "2020-10-22T13:48:01+00:00"
         },
+        {
+            "name": "firebase/php-jwt",
+            "version": "v5.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/firebase/php-jwt.git",
+                "reference": "feb0e820b8436873675fd3aca04f3728eb2185cb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/firebase/php-jwt/zipball/feb0e820b8436873675fd3aca04f3728eb2185cb",
+                "reference": "feb0e820b8436873675fd3aca04f3728eb2185cb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": ">=4.8 <=9"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Firebase\\JWT\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Neuman Vong",
+                    "email": "neuman+pear@twilio.com",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Anant Narayanan",
+                    "email": "anant@php.net",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+            "homepage": "https://github.com/firebase/php-jwt",
+            "keywords": [
+                "jwt",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/firebase/php-jwt/issues",
+                "source": "https://github.com/firebase/php-jwt/tree/master"
+            },
+            "time": "2020-03-25T18:49:23+00:00"
+        },
         {
             "name": "gloudemans/shoppingcart",
             "version": "dev-master",

+ 11 - 1
config/auth.php

@@ -14,7 +14,7 @@ return [
     */
 
     'defaults' => [
-        'guard' => 'web',
+        'guard' => 'jwt',
         'passwords' => 'users',
     ],
 
@@ -56,6 +56,11 @@ return [
             'driver' => 'token',
             'provider' => 'users',
         ],
+
+        'jwt' => [
+            'driver' => 'jwt',
+            'provider' => 'micro_user'
+        ]
     ],
 
     /*
@@ -85,6 +90,11 @@ return [
              'driver' => 'eloquent',
              'model' => App\Shop\Employees\Employee::class,
          ],
+
+        'micro_user' => [
+            'driver' => 'micro',
+            'model' => App\MicroApi\Items\UserItem::class,
+        ],
     ],
 
     /*

+ 3 - 1
config/services.php

@@ -37,7 +37,9 @@ return [
 
     'micro' => [
         'api_gateway' => env('MICRO_API_GATEWAY', 'http://localhost:8080'),
-        'timeout' => env('MICRO_TIMEOUT', 3.0)   //网关超时时间
+        'timeout' => env('MICRO_TIMEOUT', 3.0),   //网关超时时间
+        'jwt_key' => env('MICRO_JWT_KEY', 'laracomUserTokenKeySecret'),
+        'jwt_algorithms' => env('MICRO_JWT_ALGORITHMS', 'HS256'),
     ]
 
 ];

+ 1 - 1
routes/web.php

@@ -76,7 +76,6 @@ Route::namespace('Auth')->group(function () {
 
 Route::namespace('Front')->group(function () {
     Route::get('/', 'HomeController@index')->name('home');
-    Route::get('/profile', 'AccountsController@profile')->name('user.profile');
     Route::group(['middleware' => ['auth', 'web']], function () {
 
         Route::namespace('Payments')->group(function () {
@@ -90,6 +89,7 @@ Route::namespace('Front')->group(function () {
         });
 
         Route::get('accounts', 'AccountsController@index')->name('accounts');
+        Route::get('profile', 'AccountsController@profile')->name('user.profile');
         Route::get('checkout', 'CheckoutController@index')->name('checkout.index');
         Route::post('checkout', 'CheckoutController@store')->name('checkout.store');
         Route::get('checkout/execute', 'CheckoutController@executePayPalPayment')->name('checkout.execute');