環境
laradock php7.3laravel 6.6.0
mysql 8.0.16 ( on Windows 10 )
Passport
參考資料
https://learnku.com/docs/laravel/6.x/api-authentication/5429 API 认证官方強烈建議使用 Laravel Passport 來實現提供API身份驗證
https://learnku.com/laravel/t/22586 使用 Laravel Passport 处理 API 认证
https://medium.com/techcompose/create-rest-api-in-laravel-with-authentication-using-passport-133a1678a876 Create REST API in Laravel with authentication using Passport(原文)
安裝
$ composer require laravel/passport跑Migration
設定MySQL server用戶權限(以Navicat為例)
使用者 => 新增使用者在MySQL 8.0,php7.3 插件必須選擇 mysql_native_password ,否則跑migrate時會報錯:
SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client
https://stackoverflow.com/a/50027581 php mysqli_connect: authentication method unknown to the client [caching_sha2_password]
https://mysqlserverteam.com/upgrading-to-mysql-8-0-default-authentication-plugin-considerations/ Upgrading to MySQL 8.0 : Default Authentication Plugin Considerations
因為當下的 php mysqli extension 不支持新的 caching_sha2 authentication 功能,除非升級php 7.4
伺服器權限:全部授予 => 儲存
建立資料庫、設置.env MySQL連線
略運行數據庫遷移
$ php artisan migrateMigration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (1.05 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (1.2 seconds)
Migrating: 2016_06_01_000001_create_oauth_auth_codes_table
Migrated: 2016_06_01_000001_create_oauth_auth_codes_table (1.83 seconds)
Migrating: 2016_06_01_000002_create_oauth_access_tokens_table
Migrated: 2016_06_01_000002_create_oauth_access_tokens_table (1.59 seconds)
Migrating: 2016_06_01_000003_create_oauth_refresh_tokens_table
Migrated: 2016_06_01_000003_create_oauth_refresh_tokens_table (1.37 seconds)
Migrating: 2016_06_01_000004_create_oauth_clients_table
Migrated: 2016_06_01_000004_create_oauth_clients_table (1.15 seconds)
Migrating: 2016_06_01_000005_create_oauth_personal_access_clients_table
Migrated: 2016_06_01_000005_create_oauth_personal_access_clients_table (0.69 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (0.55 seconds)
新增了以下資料表:
failed_jobs
migrations
oauth_access_tokens
oauth_auth_codes
oauth_clients
oauth_personal_access_clients
oauth_refresh_tokens
password_resets
users
生成密鑰
$ php artisan passport:installEncryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: xLFj4jeEdfNJtxcwPUqHojrU0d6F6D0kO1x2yyxx
Password grant client created successfully.
Client ID: 2
Client secret: KUwHMd5XxmrikxqxBFzJDZnNTDcW9kXZY1SKK2T7
密鑰保存在 oauth_clients 表的 Laravel Personal Access Client 和 Laravel Password Grant Client 中
修改代碼
diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 30490683..90ce09b8 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; +use Laravel\Passport\Passport; class AuthServiceProvider extends ServiceProvider { @@ -25,6 +26,6 @@ class AuthServiceProvider extends ServiceProvider { $this->registerPolicies(); - // + Passport::routes(); } } diff --git a/app/User.php b/app/User.php index e79dab7f..45db5e3e 100644 --- a/app/User.php +++ b/app/User.php @@ -5,10 +5,11 @@ namespace App; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Passport\HasApiTokens; class User extends Authenticatable { - use Notifiable; + use Notifiable, HasApiTokens; /** * The attributes that are mass assignable. diff --git a/config/auth.php b/config/auth.php index aaf982bc..04c6eec2 100644 --- a/config/auth.php +++ b/config/auth.php @@ -42,7 +42,7 @@ return [ ], 'api' => [ - 'driver' => 'token', + 'driver' => 'passport', 'provider' => 'users', 'hash' => false, ], diff --git a/routes/api.php b/routes/api.php index c641ca5e..254958fe 100644 --- a/routes/api.php +++ b/routes/api.php @@ -16,3 +16,17 @@ use Illuminate\Http\Request; Route::middleware('auth:api')->get('/user', function (Request $request) { return $request->user(); }); + +Route::group([ + 'prefix' => 'auth' +], function () { + Route::post('login', 'AuthController@login'); + Route::post('signup', 'AuthController@signup'); + + Route::group([ + 'middleware' => 'auth:api' + ], function() { + Route::get('logout', 'AuthController@logout'); + Route::get('user', 'AuthController@user'); + }); +});s
- 將 Laravel\Passport\HasApiTokens 加入 App\User 模型
- AuthServiceProvider boot()中調用Passport::routes()
- config/auth.php guards.api.driver 設為 passport
- routes/api.php 中添加API路由
創建控制器
$ php artisan make:controller AuthControllerapp/Http/Controllers/AuthController.php 加入
namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Carbon\Carbon; use App\User; class AuthController extends Controller { /** * Create user * * @param [string] name * @param [string] email * @param [string] password * @param [string] password_confirmation * @return [string] message */ public function signup(Request $request) { $request->validate([ 'name' => 'required|string', 'email' => 'required|string|email|unique:users', 'password' => 'required|string|confirmed' ]); $user = new User([ 'name' => $request->name, 'email' => $request->email, 'password' => bcrypt($request->password) ]); $user->save(); return response()->json([ 'message' => 'Successfully created user!' ], 201); } /** * Login user and create token * * @param [string] email * @param [string] password * @param [boolean] remember_me * @return [string] access_token * @return [string] token_type * @return [string] expires_at */ public function login(Request $request) { $request->validate([ 'email' => 'required|string|email', 'password' => 'required|string', 'remember_me' => 'boolean' ]); $credentials = request(['email', 'password']); if(!Auth::attempt($credentials)) return response()->json([ 'message' => 'Unauthorized' ], 401); $user = $request->user(); $tokenResult = $user->createToken('Personal Access Token'); $token = $tokenResult->token; if ($request->remember_me) $token->expires_at = Carbon::now()->addWeeks(1); $token->save(); return response()->json([ 'access_token' => $tokenResult->accessToken, 'token_type' => 'Bearer', 'expires_at' => Carbon::parse( $tokenResult->token->expires_at )->toDateTimeString() ]); } /** * Logout user (Revoke the token) * * @return [string] message */ public function logout(Request $request) { $request->user()->token()->revoke(); return response()->json([ 'message' => 'Successfully logged out' ]); } /** * Get the authenticated User * * @return [json] user object */ public function user(Request $request) { return response()->json($request->user()); } }
s
測試(使用Postman)
Header加入Content-Type:application/json
X-Requested-With:XMLHttpRequest
註冊
http://app.test:5566/api/auth/signup登錄
http://app.test:5566/api/auth/login用戶
將登錄拿到的 access_token 放到 Bearer Tokenhttp://app.test:5566/api/auth/user
登出
http://app.test:5566/api/auth/logout底層如何運作?
登錄
Auth::attempt($credentials);
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
SessionGuard.php:349, Illuminate\Auth\SessionGuard->attempt()
return $query->first();
EloquentUserProvider.php:131, Illuminate\Auth\EloquentUserProvider->retrieveByCredentials()
=> 沒驗證密碼,直接取users對象
if ($this->hasValidCredentials($user, $credentials)) {
SessionGuard.php:354, Illuminate\Auth\SessionGuard->attempt()
return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
SessionGuard.php:377, Illuminate\Auth\SessionGuard->hasValidCredentials()
return $this->hasher->check($plain, $user->getAuthPassword());
EloquentUserProvider.php:145, Illuminate\Auth\EloquentUserProvider->validateCredentials()
=> 檢查密碼
$this->login($user, $remember);
SessionGuard.php:355, Illuminate\Auth\SessionGuard->attempt()
$this->setUser($user);
SessionGuard.php:423, Illuminate\Auth\SessionGuard->login()
=>設置用戶
$user = $request->user();
return $this->user;
SessionGuard.php:123, Illuminate\Auth\SessionGuard->user()
$tokenResult = $user->createToken('Personal Access Token');
return Container::getInstance()->make(PersonalAccessTokenFactory::class)->make(
$this->getKey(), $name, $scopes
);
HasApiTokens.php:67, App\User->createToken()
$this->createRequest($this->clients->personalAccessClient(), $userId, $scopes)
PersonalAccessTokenFactory.php:71, Laravel\Passport\PersonalAccessTokenFactory->make()
return json_decode($this->server->respondToAccessTokenRequest(
$request, new Response
)->getBody()->__toString(), true);
PersonalAccessTokenFactory.php:114, Laravel\Passport\PersonalAccessTokenFactory->dispatchRequestToAuthorizationServer()
$tokenResponse = $grantType->respondToAccessTokenRequest(
$request,
$this->getResponseType(),
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()]
);
AuthorizationServer.php:198, League\OAuth2\Server\AuthorizationServer->respondToAccessTokenRequest()
// Issue and persist access token
$accessToken = $this->issueAccessToken(
$accessTokenTTL, $client,
$this->getRequestParameter('user_id', $request), $scopes
);
PersonalAccessGrant.php:29, Laravel\Passport\Bridge\PersonalAccessGrant->respondToAccessTokenRequest()
$accessToken->setIdentifier($this->generateUniqueIdentifier());
AbstractGrant.php:440, Laravel\Passport\Bridge\PersonalAccessGrant->issueAccessToken()
return bin2hex(random_bytes($length));
AbstractGrant.php:550, Laravel\Passport\Bridge\PersonalAccessGrant->generateUniqueIdentifier()
=> 將存入 oauth_access_tokens.id
$this->accessTokenRepository->persistNewAccessToken($accessToken);
AbstractGrant.php:442, Laravel\Passport\Bridge\PersonalAccessGrant->issueAccessToken()
$this->tokenRepository->create([
'id' => $accessTokenEntity->getIdentifier(),
'user_id' => $accessTokenEntity->getUserIdentifier(),
'client_id' => $accessTokenEntity->getClient()->getIdentifier(),
'scopes' => $this->scopesToArray($accessTokenEntity->getScopes()),
'revoked' => false,
'created_at' => new DateTime,
'updated_at' => new DateTime,
'expires_at' => $accessTokenEntity->getExpiryDateTime(),
]);
AccessTokenRepository.php:57, Laravel\Passport\Bridge\AccessTokenRepository->persistNewAccessToken()
=> 存入 oauth_access_tokens 表
=> 存入 oauth_access_tokens 表
$token = tap($this->findAccessToken($response), function ($token) use ($userId, $name) {
$this->tokens->save($token->forceFill([
'user_id' => $userId,
'name' => $name,
]));
});
PersonalAccessTokenFactory.php:74, Laravel\Passport\PersonalAccessTokenFactory->make()
$response
array (
'token_type' => 'Bearer',
'expires_in' => 31535998,
'access_token' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZmE0OGEyNzYxN2FkNjlhZDlkMTBlZDkzZmExMzFkZTc4MDU2ODBjYzQ0YjYwMDEyNjIwM2E1MTE1ZmExYTFhZTM5ZmEyMDBhZjIzZTNlNTgiLCJpYXQiOjE2MDM3NjQxMDEsIm5iZiI6MTYwMzc2NDEwMSwiZXhwIjoxNjM1MzAwMDk5LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.fSBAzUlHwfwctJBjnM_kiOn48Imp7gcqrMkfJeVdXtE8tFmcNGiFeT3DvtK_0zYCF7qWJM5ZUsOcOEjBhttVgsMPEOjv9Cb7kVdipE2GUX4d4x6Bhw1xCrmYKbNFw4rY3G9J2tySuLZR85ZmIYGf0LvKNkwWvdeS3SZJeh00FAhN3g9ssVgw5NjxzV9T65G5_X-9yDg6GzRQhWNU_GoX1qYhet8dUM1SQTtaTorpjVF5Xk0bEFXyjqkQfiRHiQxBb5YKIuRZu_653Gw8gs1HEv2252CWh7YsGY2iM-5kH3PPz9DpE9GYelC0ULWcWWA7uYknJ7IjXfDhgQKjV_RYpmOIC-EpidazuFd4x1T4ZPIn-ZTjh0wEsPdbReoXagValXByCmykMqAEP9-qPQVG05w_qWoHW_OmV8spnOLz-xZnwmVirughgQiJihN3z1fmnYZTrrsuMZ5pzeG3LNeKDDx1KrYV6m-42JREcHeXVWfl0RgMyCh4d4CkjksNAVB_t756aA4eAzgldGIVtVMuAtkLroS6rtkMQqVVkSfzhjix9fsRN45-7ZKbLCbSi665rl0Bs_XZ0MDs-9LJXkQqSe5DEvvlVoSmLjxUaGkXWfnbkLck2NIAhR3OOYcDq2TEiS749qzaaPEf_nEL17M4mHuDdI4o24NsFBt-gZ5CcHM',
)
return $this->tokens->find(
$this->jwt->parse($response['access_token'])->getClaim('jti')
);
PersonalAccessTokenFactory.php:126, Laravel\Passport\PersonalAccessTokenFactory->findAccessToken()
$this->tokens Type:
\Laravel\Passport\TokenRepository
$this->jwt Type:
\Lcobucci\JWT\Parser
=> 由此方法可以用\Laravel\Passport\TokenRepository和 \Lcobucci\JWT\Parser 從 access_token 找回 oauth_access_tokens
用戶
如何用access_token身份驗證?
routes/api.php
Route::group([
'middleware' => 'auth:api'
], function() {
Route::get('user', 'AuthController@user');
});
app/Http/Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
];
Stack:
$this->authenticate($request, $guards);
Authenticate.php:41, App\Http\Middleware\Authenticate->handle()
$this->auth->guard($guard)->check()
Authenticate.php:62, App\Http\Middleware\Authenticate->authenticate()
return ! is_null($this->user());
GuardHelpers.php:60, Illuminate\Auth\RequestGuard->check()
return $this->user = call_user_func(
$this->callback, $this->request, $this->getProvider()
);
RequestGuard.php:58, Illuminate\Auth\RequestGuard->user()
return (new TokenGuard(
$this->app->make(ResourceServer::class),
Auth::createUserProvider($config['provider']),
$this->app->make(TokenRepository::class),
$this->app->make(ClientRepository::class),
$this->app->make('encrypter')
))->user($request);
PassportServiceProvider.php:283, Laravel\Passport\PassportServiceProvider->Laravel\Passport\{closure:/var/www/coolapp/vendor/laravel/passport/src/PassportServiceProvider.php:276-284}()
return $this->authenticateViaBearerToken($request);
TokenGuard.php:94, Laravel\Passport\Guards\TokenGuard->user()
if (! $psr = $this->getPsrRequestViaBearerToken($request)) {
TokenGuard.php:131, Laravel\Passport\Guards\TokenGuard->authenticateViaBearerToken()
return $this->server->validateAuthenticatedRequest($psr);
TokenGuard.php:184, Laravel\Passport\Guards\TokenGuard->getPsrRequestViaBearerToken()
return $this->getAuthorizationValidator()->validateAuthorization($request);
ResourceServer.php:84, League\OAuth2\Server\ResourceServer->validateAuthenticatedRequest()
if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
BearerTokenValidator.php:72, League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator->validateAuthorization()
$this->publicKey->getKeyPath()
file:///var/www/coolapp/storage/oauth-public.key
=> 使用 storage/oauth-public.key 驗證 access_token 是否合法
// Ensure access token hasn't expired
$data = new ValidationData();
$data->setCurrentTime(time());
if ($token->validate($data) === false) {
BearerTokenValidator.php:83, League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator->validateAuthorization()
=> 檢查token有無過期
// Check if token has been revoked
if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
BearerTokenValidator.php:88, League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator->validateAuthorization()
=> 檢查token是否被revoked
密碼授權令牌(grant_type=password)
curl --location --request POST 'http://app.test:5566/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=2' \
--data-urlencode 'client_secret=client_secret' \
--data-urlencode 'username=username@github.com' \
--data-urlencode 'password=password' \
--data-urlencode 'scope='
返回錯誤:
Symfony\Component\Debug\Exception\FatalThrowableError: Class 'Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory' not found in file /var/www/coolapp/vendor/laravel/framework/src/Illuminate/Routing/RoutingServiceProvider.php on line 131
原因
https://github.com/sfelix-martins/passport-multiauth/issues/124#issuecomment-596418813 Class 'Symfony\\Bridge\\PsrHttpMessage\\Factory\\DiactorosFactory' not found
https://github.com/sfelix-martins/passport-multiauth/pull/123#issue-383619596 Compatibility Update
這個問題將在 laravel 6.18+ / 7.x 修正
解法:
更新laravel
$ composer update
刷新令牌(grant_type=refresh_token)
https://stackoverflow.com/a/59334090 How do I get a refresh token in Laravel Passport?
使用【密碼授權令牌】獲取 access_token 和 refresh_token
{"token_type": "Bearer","expires_in": 31536000,"access_token": "access_token.xxx.xxx","refresh_token": "refresh_token_xxx"}
使用 refresh_token 獲取【刷新令牌】
curl --location --request POST 'http://app.test:5566/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=the-refresh-token' \
--data-urlencode 'client_id=2' \
--data-urlencode 'client_secret=client_secret' \
--data-urlencode 'scope='
將授權碼轉換為訪問令牌
https://learnku.com/docs/laravel/6.x/passport/5152#554d3e Passport OAuth 认证
準備兩個專案
http://app.test:5566/ => laravel 6 ,有安裝passport
http://app3.test:5566/ => laravel 8,無安裝passport
前端快速上手
$ php artisan vendor:publish --tag=passport-components
註冊組件
resources/js/app.js
Vue.component( 'passport-clients', require('./components/passport/Clients.vue').default ); Vue.component( 'passport-authorized-clients', require('./components/passport/AuthorizedClients.vue').default ); Vue.component( 'passport-personal-access-tokens', require('./components/passport/PersonalAccessTokens.vue').default );
在view resources/views/home.blade.php 中使用
<passport-clients></passport-clients>
<passport-authorized-clients></passport-authorized-clients>
<passport-personal-access-tokens></passport-personal-access-tokens>
在app.test新增用戶的oauth_clients
ps. 綠色那塊是 <passport-clients></passport-clients>原理
http://app3.test:5566/ 透過 http://app.test:5566/ 登錄後的授權,在 http://app3.test:5566/ 上使用用戶在app.test生成的oauth_clients(Client ID = 3)獲取 http://app.test:5566/ 的token,進而獲取 http://app.test:5566/ 上的資料
在laradock的 workspace和php-fpm 容器新增hosts
diff --git a/docker-compose.yml b/docker-compose.yml index bc737c0..62152fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -165,6 +165,7 @@ services: - ./php-worker/supervisord.d:/etc/supervisord.d extra_hosts: - "dockerhost:${DOCKER_HOST_IP}" + - "app.test:192.168.1.9" ports: - "${WORKSPACE_SSH_PORT}:22" - "${WORKSPACE_BROWSERSYNC_HOST_PORT}:3000" @@ -264,6 +265,7 @@ services: - "9000" extra_hosts: - "dockerhost:${DOCKER_HOST_IP}" + - "app.test:192.168.1.9" environment: - PHP_IDE_CONFIG=${PHP_IDE_CONFIG} - DOCKER_HOST=tcp://docker-in-docker:2376
app.test:192.168.1.9 => hyper-v的IP:192.168.1.9,非填laradock容器IP
實作
https://github.com/laravel/passport/issues/221#issuecomment-269828055 API Authentication Error: {"error":"invalid_client","message":"Client authentication failed"}
Route::get('/redirect', function () { $query = http_build_query([ 'client_id' => '3', 'redirect_uri' => 'http://app3.test:5566/callback', 'response_type' => 'code', 'scope' => '', ]); return redirect('http://app.test:5566/oauth/authorize?'.$query); }); Route::get('/callback', function (\Illuminate\Http\Request $request) { $http = new GuzzleHttp\Client; $response = $http->post('http://app.test:5566/oauth/token', [ 'form_params' => [ 'grant_type' => 'authorization_code', 'client_id' => '3', 'client_secret' => 'rR2P1HBeysX5NO9noSfMc3BNpEIQyANExxx', 'redirect_uri' => 'http://app3.test:5566/callback', 'code' => $request->code, ], ]); return json_decode((string) $response->getBody(), true); });
然後瀏覽器訪問 http://app3.test:5566/redirect 然後會轉跳到 http://app.test:5566/oauth/authorize?client_id=3&redirect_uri=http%3A%2F%2Fapp3.test%3A5566%2Fcallback&response_type=code&scope= (如果你在 http://app.test:5566/ 還沒登錄,會要求你先登錄 - http://app.test:5566/login )
授權後會轉跳到 http://app3.test:5566/callback?code=def50200ff1e6e1d070750700d964b58c208bcfc69ad68e12c16340c0bf80550de3f8c8c68609f0414365e55a2272a69f99d6a23226ccc01b58079cb3f17d8f800f6c4ef27563da3e98082c53d5abc9b4d1f3d4a6802274b4017fc91a553a297f7ce97b3e81d70c0033642328d0e16cf364edeefad089a476e62ca687d545719f72af9222eb0db3ccb825fc58aefc3bd1edff579fde203704975d66c7984d8873203e610b572c02e6254bffa79417897025fed73a890034300fd91e298bea302e3026f2e9af610d8b2c7848b996acb46574d2c0b75af3c1eca107ad50f56fa25a13a87361f0777ec8ae25e67f7a96751f95047eac0142807406c0cc1a2f61dfeb8eee7a246e89fd74c2c742d3e571a1f6cc7dbac5a1327c6dcde8597028dec48f58c67ee5fa1e3ba48b2326a3e9758e8f1561716e2579bca05f78c3ea0a6801aada0f1112cb0b35d86f838dc0ec85d4e88a4679b6d3cda388af5b0dd2547 以在 http://app3.test:5566/ 上獲取 該用戶(user_id=1,client_id=3) 的token 。
=》oauth_clients會新增client_id=3的token
app3.test 的 /redirect 路由中的 redirect_uri 必須和 app.test 系統上oauth_clients 該token的oauth_clients.redirect 一致,否則會驗證失敗