sun-asterisk / php-auth
Sun* Auth | PHP
Requires
- php: ^8.0
- firebase/php-jwt: ^6.3
- illuminate/contracts: ^8.0|^9.0|^10.0
- illuminate/database: ^8.0|^9.0|^10.0
- illuminate/http: ^8.0|^9.0|^10.0
- illuminate/routing: ^8.0|^9.0|^10.0
- illuminate/support: ^8.0|^9.0|^10.0
- illuminate/validation: ^8.0|^9.0|^10.0
- socialiteproviders/manager: ^4.3
Requires (Dev)
- mockery/mockery: ^1.5
- phpunit/phpunit: ^9.6
README
🌟 简介
Sun* Auth 是一个库,旨在为 Web/移动应用程序提供身份验证和授权功能。它使用现代技术构建,并提供了一系列功能,使得它易于集成到现有应用程序中。
使用 Sun* Auth 的一个关键原因是其易于使用。它提供了一个简单的库,使得向任何应用程序添加身份验证和授权变得容易。此外,它设计得高度可定制,允许开发者根据其特定需求进行定制。
使用 Sun* Auth 可以节省开发者的时间和精力,因为我们不必独立编写和测试复杂的身份验证和授权功能。此外,Sun* Auth 提供了一个简单易懂的库,使其易于集成到现有的 Web/移动应用程序中。
Sun* Auth 提供的一些功能包括用户注册和登录、密码重置和恢复。
🔌 安装
通过 Composer 安装包
composer require sun-asterisk/php-auth
Laravel
将服务提供者添加到 config/app.php 配置文件中的 providers 数组中,如下所示
<?php # config/app.php return [ // ... 'providers' => [ // ... SunAsterisk\Auth\SunServiceProvider::class ] ];
发布配置文件
php artisan vendor:publish --provider="SunAsterisk\Auth\SunServiceProvider" --tag=sun-asterisk
Lumen
<?php // bootstrap/app.php $app->register(SunAsterisk\Auth\SunServiceProvider::class);
配置
mkdir -p config cp vendor/sun-asterisk/config/sun-asterisk.php config/sun-asterisk.php
其他
use SunAsterisk\Auth\Factory; $configs = config('/path/to/sun-asterisk.php') $factory = (new Factory)->withConfig($configs); $service = $factory->createAuthJwt();
配置
mkdir -p config cp vendor/sun-asterisk/config/sun-asterisk.php config/sun-asterisk.php
审查配置文件
config/sun-asterisk.php
详细
<?php return [ 'auth' => [ /* |--------------------------------------------------------- | Attribute login |--------------------------------------------------------- | | E.g. 'email or username' | */ 'login_username' => 'email', /* |--------------------------------------------------------- | Attribute field_credentials |--------------------------------------------------------- | Use 1 of the list for authentication | E.g. 'username or email or phone' | */ 'field_credentials' => [ 'email', ], /* |--------------------------------------------------------- | Attribute token_payload_fields |--------------------------------------------------------- | Use the items in the list to create an access token | | E.g. 'id or email' | */ 'token_payload_fields' => [ 'id', 'email', ], /* |--------------------------------------------------------- | Attribute login |--------------------------------------------------------- | | E.g. 'password or passwd' | */ 'login_password' => 'password', /* |--------------------------------------------------------- | Model login |--------------------------------------------------------- | | E.g. 'App\Models\User::class or App\Models\Admin::class' | */ 'model' => App\Models\User::class, /* |--------------------------------------------------------- | Token forgot password |--------------------------------------------------------- | | Default 5 minutes | E.g. '5' | */ 'token_expires' => 5, // minutes /* |--------------------------------------------------------- | Key for jwt access token |--------------------------------------------------------- | | E.g. 'xxxx' | */ 'jwt_key' => 'jwt_key', /* |--------------------------------------------------------- | Key for jwt refresh access token |--------------------------------------------------------- | | E.g. 'xxxx' | */ 'jwt_refresh_key' => 'jwt_refresh_key', /* |--------------------------------------------------------- | TTL for jwt |--------------------------------------------------------- | | Default 60 minutes | E.g. '60' | */ 'jwt_ttl' => 60, // minutes /* |--------------------------------------------------------- | TTL for refresh access token |--------------------------------------------------------- | | Default 20160 minutes | E.g. '60' | */ 'jwt_refresh_ttl' => 20160, // minutes /* |--------------------------------------------------------- | use Socialite Providers for social login |--------------------------------------------------------- | | Default false | */ 'enabled_social' => false, ], ];
使用
我们将以 Laravel 为例说明使用方法。
配置 Auth 守护者
在 config/auth.php 文件中,您需要做一些更改以进行配置
'guards' => [ 'api' => [ 'driver' => 'sun', 'provider' => 'users', ], ]
将 auth:api 中间件添加到需要登录认证的路由中
Route::group([ 'middleware' => 'auth:api', ], function ($router) { // routes }
使用 JWT 注入依赖项
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use SunAsterisk\Auth\Contracts\AuthJWTInterface; class AuthController extends Controller { protected AuthJWTInterface $service; public function __construct(AuthJWTInterface $service) { $this->service = $service; } ... }
使用 Session 注入依赖项
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use SunAsterisk\Auth\Contracts\AuthSessionInterface; class AuthController extends Controller { protected AuthSessionInterface $service; public function __construct(AuthSessionInterface $service) { $this->service = $service; } ... }
登录
POST /login
- 参数
- 响应
JWT 使用示例
详细
public function login(Request $request) { $params = $request->only(['username', 'password']); // use service package $rs = $this->service->login($params, [], function ($entity) { return $entity->only(['id', 'email', 'username']); }); return response()->json($rs['auth']); }
Session 使用示例
详细
public function showLoginForm() { return view('auth.login'); } public function login(Request $request) { $params = $request->only(['email', 'password']); $this->service->login($params, [], function ($entity) { // }); return redirect()->intended('home'); }
示例
curl -X 'POST' \ 'https:///api/login' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "username": "user1234", "password": "passwordRequired@123" }'
{ "refresh_token": "eyJ0eXAiOiJKV1QiLC...", "access_token": "eyJ0eXAiOiJKV1...", "token_type": "bearer", "expires_at": 1676281826 }
登出
POST /logout
- 参数
- 响应
JWT 使用示例
详细
public function logout(Request $request) { auth('api')->logout(); return response()->noContent(); }
Session 使用示例
详细
public function logout(Request $request) { $this->service->logout($request); return view('auth.login'); }
示例
curl -X 'POST' \ 'https:///api/logout' \ -H 'accept: application/json' \ -H 'Authorization: Bearer eyJ0eXAiOiJKV1...' \ -d ''
{}
注册
POST /register
- 参数
- 响应
JWT 使用示例
详细
public function register(Request $request) { $rules = []; $fields = $request->only(['username', 'password', 'email']); $result = $this->service->register($fields, $rules, function ($entity) { return $entity->only(['id', 'email', 'username']); }); return response()->json($result); }
Session 使用示例
详细
public function showRegistrationForm() { return view('auth.register'); } public function register(Request $request) { $fields = $request->only(['username', 'password', 'email']); $fields['name'] = $fields['username']; $this->service->register($fields, [], function ($entity) { // }); return redirect()->intended('home'); }
如果您想在注册后进行用户认证,请将 $setGuard 参数设置为 true
$this->service->register($fields, [], function ($entity) { // }, true);
示例
curl -X 'POST' \ 'https:///api/register' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "username": "user123456", "password": "passwordRequired@123", "email": "testuser02@local.ltd" }'
{ "id": 13, "email": "testuser02@local.ltd", "username": "user123456" }
忘记密码
POST /forgot-password
- 参数
- 响应
创建 postForgotPassword 函数如下
详细
# App\Http\Controllers\AuthController public function postForgotPassword(Request $request) { $email = $request->email; $status = $this->service->postForgotPassword($email, function ($token, $user) { // Use send mail from framework sendEmail($user, $token); }); return response()->json([ 'ok' => $status, 'type' => 'forgotPassword', ]); }
创建 confirm 函数如下
详细
public function confirm(Request $request) { $token = $request->token; $status = $this->service->verifyToken($token); return response()->json([ 'ok' => $status, ]); }
创建 newPassword 函数如下
详细
# App\Http\Controllers\AuthController public function postNewPassword(Request $request) { $params = $request->only(['password', 'token']); $status = $this->service->changePassword($params, null, function ($user, &$attr) { // Update attr }); return response()->json([ 'ok' => $status, 'type' => 'newPassword', ]); }
示例
curl -X 'POST' \ 'https:///api/forgot-password' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "email": "testuser01@local.ltd" }'
{ "ok": true, "type": "forgotPassword" }
验证
GET /confirm
- 参数
- 响应
curl -X 'GET' \ 'https:///api/confirm?token=eyJ0eXAiOiJKV1QiLC...' \ -H 'accept: application/json'
{ "ok": true }
新密码
POST /new-password
- 参数
- 响应
curl -X 'POST' \ 'https:///api/new-password' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "password": "passwordRequired@123", "token": "eyJ0eXAiOiJKV1QiLC..." }'
{ "ok": true, "type": "newPassword" }
刷新令牌
POST /refresh
- 参数
- 响应
创建刷新函数如下
# App\Http\Controllers\AuthController; public function refresh(Request $request) { $token = $request->refresh_token; $rs = $this->service->refresh($token); return response()->json($rs); }
示例
curl -X 'POST' \ 'https:///api/refresh' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "refresh_token": "refresh_token_secret" }'
{ "refresh_token": "eyJ0eXAiOiJKV1QiLC...", "access_token": "eyJ0eXAiOiJKV1...", "token_type": "bearer", "expires_at": 1676281826 }
自定义社交登录
使用 Google 登录
要使用它,您需要通过 composer 安装 socialiteproviders/google 包
composer require socialiteproviders/google
将配置添加到 config/services.php
# config/services.php ... 'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT_URI'), ],
将 config/sun-asterisk.php 中的 config enabled_social 设置为 true
config/sun-asterisk.php
详细
# config/sun-asterisk.php ... /* |--------------------------------------------------------- | use Socialite Providers for social login |--------------------------------------------------------- | | Default false | */ 'enabled_social' => true,
routes/web.php
详细
# routes/web.php use Illuminate\Support\Facades\Route; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use SunAsterisk\Auth\Contracts\AuthSocialInterface; ... // Redirect Endpoint Route::get('social/{provider}/redirect', function (Request $request) { $provider = $request->provider; // use service from package return app(AuthSocialInterface::class)->socialSignIn($provider); }); // Callback Endpoint Route::get('social/{provider}/callback', function (Request $request) { $provider = $request->provider; // use service from package $socialUser = app(AuthSocialInterface::class)->socialCallback($provider); $user = \App\Models\User::updateOrCreate([ 'social_id' => $socialUser->id, 'social_type' => $provider, ], [ 'name' => $socialUser->name, 'email' => $socialUser->email, 'avatar' => $socialUser->avatar, ]); Auth::login($user); return redirect('/dashboard'); });
架构
架构登录
工作流程
解释
接口
/** * [login] * @param array $credentials [The user's attributes for authentication.] * @param array|null $attributes [The attributes use for query.] * @param callable|null $callback [The callback function has the entity model.] * @return [array] */ public function login(array $credentials = [], ?array $attributes = [], ?callable $callback = null): array;
- 验证器
# SunAsterisk\Auth\Services\AuthJWTService; public function login(array $credentials = [], ?array $attributes = [], ?callable $callback = null): array { $this->loginValidator($credentials)->validate(); ... } ... protected function loginValidator(array $data) { return Validator::make($data, [ $this->username() => 'required', $this->passwd() => 'required', ]); }
- 按属性查找项
# SunAsterisk\Auth\Services\AuthJWTService; public function login(array $credentials = [], ?array $attributes = [], ?callable $callback = null): array { ... $item = $this->repository->findByAttribute($attributes); }
- 比较哈希密码
# SunAsterisk\Auth\Services\AuthJWTService; public function login(array $credentials = [], ?array $attributes = [], ?callable $callback = null): array { ... if (! $item || ! Hash::check(Arr::get($credentials, $this->passwd()), $item->{$this->passwd()})) { throw ValidationException::withMessages([ 'message' => $this->getFailedLoginMessage(), ]); } }
- 从 jwt 生成 accessToken & refreshToken
# SunAsterisk\Auth\Services\AuthJWTService; public function login(array $credentials = [], ?array $attributes = [], ?callable $callback = null): array { ... $payload = $this->jwt->make($itemArr)->toArray(); $payloadRefresh = $this->jwt->make($itemArr, true)->toArray(); $jwt = $this->jwt->encode($payload); $refresh = $this->jwt->encode($payloadRefresh, true); $this->tokenMapper->add($payload, $refresh); }
login 方法将返回一个数组
'item' => $itemArr, 'auth' => [ 'refresh_token' => 'eyJhbGciOiJIUzI1NiIsIn...', 'access_token' => 'eyJiwibmFtZSI6Ikpva...', 'token_type' => 'bearer', 'expires_at' => 1675742447, ],
$itemArr
是用户模型的对象数组。我们可以通过回调函数进行自定义,如下所示
# App\Http\Controllers\AuthController $rs = $this->service->login($params, [], function ($entity) { // Custom $itemArr return $entity->only(['id', 'email', 'username']); });
BTW:我们还可以更改登录流程的 SQL 查询,如下所示
# App\Http\Controllers\AuthController $rs = $this->service->login( $params, [ 'username' => $params['username'], 'is_active' => true, // custom query attributes ], function ($entity) { return $entity->only(['id', 'email', 'username']); }, );
架构登出
工作流程
解释
- 将守护者设置为服务提供者
# SunAsterisk\Auth\SunServiceProvider /** * Extend Laravel's Auth. * * @return void */ protected function extendAuthGuard(): void { $this->app['auth']->extend('sun', function ($app, $name, array $config) { $storage = $app->make(Providers\Storage::class); $blackList = new SunBlacklist($storage); $jwt = new SunJWT($blackList, $app->config->get('sun-asterisk.auth')); $tokenMapper = new SunTokenMapper($storage); $guard = new SunGuard( $jwt, $app['auth']->createUserProvider($config['provider']), $app['request'], $tokenMapper ); app()->refresh('request', $guard, 'setRequest'); return $guard; }); }
- 在 SunGuard 中创建 logout 方法
# SunAsterisk\Auth\SunGuard /** * Logout the user, thus invalidating the token. * * @return void */ public function logout() { try { $token = $this->request->bearerToken(); $rawToken = $this->jwt->decode($token); $refreshToken = $this->tokenMapper->pullRefreshToken($rawToken['jti']); $this->jwt->invalidate($token); if ($refreshToken) { $this->jwt->invalidate($refreshToken, true); } } catch (\Exception $e) { throw new Exceptions\JWTException($e->getMessage()); } }
- 使 token 和 refreshToken 失效
# SunAsterisk\Auth\SunJWT public function invalidate(string $token, bool $isRefresh = false): bool { if (! $this->blackList) { throw new Exceptions\JWTException('You must have the blacklist enabled to invalidate a token.'); } $payload = $this->decode($token, $isRefresh, false); return $this->blackList->add($payload); }
架构注册
工作流程
解释
接口
/** * [register] * @param array $fields [The user's attributes for register.] * @param array $rules [The rules for register validate.] * @param callable|null $callback [The callback function has the entity model.] * @return [array] */ public function register(array $params = [], array $rules = [], callable $callback = null): array;
- 验证器
# SunAsterisk\Auth\Services\AuthJWTService; public function register(array $params = [], array $rules = [], callable $callback = null): array { if (empty($rules)) { $rules = [ 'username' => ['required', 'string', "unique:{$table}," . $this->username()], 'password' => [ 'required', 'min:6', 'regex:/^.*(?=.{3,})(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[\d\x])(?=.*[!@#$%]).*$/', ], ]; if (isset($params['email'])) { $rules['email'] = ['required', 'string', "unique:{$table},email"]; } } ... }
- 哈希密码
# SunAsterisk\Auth\Services\AuthJWTService; public function register(array $params = [], array $rules = [], callable $callback = null): array { ... $params[$this->passwd()] = Hash::make($params[$this->passwd()]); }
- 将参数插入数据库
# SunAsterisk\Auth\Services\AuthJWTService; public function register(array $params = [], array $rules = [], callable $callback = null): array { ... $item = $this->repository->create($params); }
register 方法将返回一个数组
自定义注册
您可以根据以下内容更改或验证属性
# App\Http\Controllers\AuthController public function register(Request $request) { $fields = $request->only(['password', 'email']); // customer validate $rules = [ 'email' => 'required|email', 'password' => 'required', ]; $result = $this->service->register($fields, $rules); return response()->json($result); }
架构忘记密码
工作流程
解释
接口
/** * [postForgotPassword] * @param string $email [The user's email for receive token] * @param callable|null $callback [The callback function response token & entity model.] * @return [bool] */ public function postForgotPassword(string $email, callable $callback = null): bool; /** * [verifyForgotPasswordToken] * @param string $token [The token from user's email] * @param callable|null $callback [The callback function has the token & entity model.] * @return [bool] */ public function verifyToken(string $token, callable $callback = null): bool; /** * [changePassword] * @param array $params [The params for change password (passwd | ?old_passwd | ?token)] * @param int|null $userId [The user's id when user authenticate.] * @param callable|null $callback [The callback function have the entity model & pointer of users's attributes.] * @return [bool] */ public function changePassword(array $params = [], ?int $userId = null, callable $callback = null): bool;
- 验证电子邮件
# SunAsterisk\Auth\Services\AuthJWTService; public function postForgotPassword(string $email, callable $callback = null): bool { ... // Validate Email Validator::make(['email' => $email], [ 'email' => ['required', 'email'], ])->validate(); // Check Email exists $item = $this->repository->findByAttribute(['email' => $email]); if (!$item) { throw ValidationException::withMessages([ 'message' => 'The email is invalid.', ]); } ... }
- 生成令牌
# SunAsterisk\Auth\Services\AuthJWTService; public function postForgotPassword(string $email, callable $callback = null): bool { ... $obj = [ 'id' => $item->id, 'created_at' => Carbon::now()->timestamp, ]; $token = Crypt::encryptString(json_encode($obj)); ... }
- 验证令牌
# SunAsterisk\Auth\Services\AuthJWTService; public function verifyToken(string $token, callable $callback = null): bool { ... $objStr = Crypt::decryptString($token); $obj = json_decode($objStr, true); ... $diffSeconds = Carbon::now()->diffInSeconds(Carbon::createFromTimestamp($obj['created_at'])); if ($diffSeconds >= $this->config['token_expires'] * 60) { throw new AuthException('Token is invalid!'); } }
- 更改密码
# SunAsterisk\Auth\Services\AuthJWTService; public function changePassword(array $params = [], ?int $userId = null, callable $callback = null): bool { ... $user = null; $attr = []; // For usecase forgot password if (isset($params['token'])) { $this->verifyToken($params['token'], function ($entity) use (&$user) { $user = $entity; }); } ... if ($user) { $attr[$this->passwd()] = Hash::make($params[$this->passwd()]); ... $this->repository->updateById($user->id, $attr); } ... }
架构刷新令牌
工作流程
解释
接口
/** * [refresh] * @param string $refreshToken [refresh_token for user get access_token] * @param callable|null $callback [The callback function has the entity model.] * @return [array] */ public function refresh(?string $refreshToken, callable $callback = null): array;
- 解码刷新令牌
# SunAsterisk\Auth\Services\AuthJWTService; public function refresh(?string $refreshToken, callable $callback = null): array { ... $payload = $this->jwt->decode($refreshToken ?: '', true); }
- 比较刷新令牌的过期时间
# SunAsterisk\Auth\Services\AuthJWTService; public function refresh(?string $refreshToken, callable $callback = null): array { ... if (Carbon::createFromTimestamp($payload['exp'])->lte(Carbon::now())) { throw new InvalidArgumentException('The RefreshToken is invalid.'); } }
- 验证用户是否存在
# SunAsterisk\Auth\Services\AuthJWTService; public function refresh(?string $refreshToken, callable $callback = null): array { ... $item = $this->repository->findById($sub?->id); if (!$item) { throw new InvalidArgumentException('The RefreshToken is invalid.'); } }
- 撤销所有访问令牌
# SunAsterisk\Auth\Services\AuthJWTService; public function revoke(array $keys = []): bool { try { return $this->jwt->revoke($keys); } catch (\Exception $e) { throw new Exceptions\JWTException('Revoke token is wrong.'); } }
- 重新生成访问令牌
# SunAsterisk\Auth\Services\AuthJWTService; public function refresh(?string $refreshToken, callable $callback = null): array { ... $payload = $this->jwt->make((array) $sub)->toArray(); $jwt = $this->jwt->encode($payload); $this->tokenMapper->add($payload, $refreshToken); }
方法刷新将返回一个数组
# SunAsterisk\Auth\Services\AuthJWTService; public function refresh(?string $refreshToken, callable $callback = null): array { ... return [ 'refresh_token' => 'eyJhbGciOiJIUzI1NiIsIn...', 'access_token' => 'eyJiwibmFtZSI6Ikpva...', 'token_type' => 'bearer', 'expires_at' => 1675742447, ]; }
自定义社交登录架构
工作流程
解释
接口
/** * [socialSignIn] * @param string $provider [The Provider should received from https://socialiteproviders.com/about/] * @return [Illuminate\Http\RedirectResponse] */ public function socialSignIn(?string $provider): RedirectResponse; /** * [socialCallback] * @param string $provider [The Provider should received from https://socialiteproviders.com/about/] * @return [stdClass] */ public function socialCallback(?string $provider): stdClass;
- 重定向
# SunAsterisk\Auth\Services\AuthSocialService /** * [socialSignIn] * @param string $provider [The Provider should received from https://socialiteproviders.com/about/] * @return [Illuminate\Http\RedirectResponse] */ public function socialSignIn(?string $provider): RedirectResponse { try { return Socialite::driver($provider)->redirect(); } catch (\Exception $e) { throw new InvalidArgumentException('provider is invalid!');
- 回调
# SunAsterisk\Auth\Services\AuthSocialService /** * [socialCallback] * @param string $provider [The Provider should received from https://socialiteproviders.com/about/] * @return [stdClass] */ public function socialCallback(?string $provider): stdClass { try { return Socialite::driver($provider)->user(); } catch (\Exception $e) { throw new InvalidArgumentException('provider is invalid!'); } }