下面是在 Hyperf 中正确实现 OAuth2.0/sOIDC 的完整指南。 --- Hyperf OAuth2.0/ OIDC 实现指南 推荐技术栈 ┌──────────────────┬───────────────────────────────────────────┐ │ 角色 │ 包 │ ├──────────────────┼───────────────────────────────────────────┤ │ OAuth2.0服务端 │ league/oauth2-server │ ├──────────────────┼───────────────────────────────────────────┤ │ OIDC 扩展 │ steverhoades/oauth2-openid-connect-server │ ├──────────────────┼───────────────────────────────────────────┤ │ Hyperf 集成层 │ xtwoend/oauth-server 或手动集成 │ ├──────────────────┼───────────────────────────────────────────┤ │ OAuth2.0客户端 │ league/oauth2-client │ └──────────────────┴───────────────────────────────────────────┘ --- 一、安装依赖composerrequire league/oauth2-servercomposerrequire steverhoades/oauth2-openid-connect-server# OIDC 支持composerrequire league/oauth2-client# 作为客户端时--- 二、目录结构 app/ ├── OAuth/ │ ├── Repositories/ │ │ ├── UserRepository.php │ │ ├── ClientRepository.php │ │ ├── AccessTokenRepository.php │ │ ├── RefreshTokenRepository.php │ │ ├── ScopeRepository.php │ │ └── AuthCodeRepository.php │ ├── Entities/ │ │ ├── UserEntity.php │ │ ├── ClientEntity.php │ │ ├── AccessTokenEntity.php │ │ └── ScopeEntity.php │ └── Grant/ │ └── OidcAuthCodeGrant.php ├── Http/ │ ├── Controller/ │ │ └── OAuthController.php │ └── Middleware/ │ └── OAuthMiddleware.php └── ConfigProvider.php --- 三、核心实体<?php // app/OAuth/Entities/ClientEntity.php namespace App\OAuth\Entities;use League\OAuth2\Server\Entities\ClientEntityInterface;use League\OAuth2\Server\Entities\Traits\ClientTrait;use League\OAuth2\Server\Entities\Traits\EntityTrait;class ClientEntity implements ClientEntityInterface{use EntityTrait, ClientTrait;publicfunction__construct(string$identifier, string$name, string|array$redirectUri, bool$isConfidential=true){$this->identifier=$identifier;$this->name=$name;$this->redirectUri=$redirectUri;$this->isConfidential=$isConfidential;}}<?php // app/OAuth/Entities/UserEntity.php namespace App\OAuth\Entities;use League\OAuth2\Server\Entities\UserEntityInterface;use OpenIDConnectServer\Entities\ClaimSetInterface;class UserEntity implements UserEntityInterface, ClaimSetInterface{publicfunction__construct(private array$claims){}publicfunctiongetIdentifier(): string{return(string)$this->claims['id'];}// OIDC 标准 claims publicfunctiongetClaims(): array{return['sub'=>$this->claims['id'],'name'=>$this->claims['name'],'email'=>$this->claims['email'],];}}--- 四、Repository 实现(协程安全)<?php // app/OAuth/Repositories/ClientRepository.php namespace App\OAuth\Repositories;use App\OAuth\Entities\ClientEntity;use Hyperf\DbConnection\Db;use League\OAuth2\Server\Repositories\ClientRepositoryInterface;class ClientRepository implements ClientRepositoryInterface{publicfunctiongetClientEntity(string$clientIdentifier): ?ClientEntity{// Hyperf 的 Db::table()是协程安全的$row=Db::table('oauth_clients')->where('id',$clientIdentifier)->first();if(!$row)returnnull;returnnew ClientEntity($row->id,$row->name, explode(',',$row->redirect_uris),(bool)$row->is_confidential);}publicfunctionvalidateClient(string$clientIdentifier, ?string$clientSecret, ?string$grantType): bool{$row=Db::table('oauth_clients')->where('id',$clientIdentifier)->first();if(!$row)returnfalse;// 公开客户端不需要 secretif(!$row->is_confidential)returntrue;returnpassword_verify($clientSecret??'',$row->secret);}}<?php // app/OAuth/Repositories/AccessTokenRepository.php namespace App\OAuth\Repositories;use App\OAuth\Entities\AccessTokenEntity;use Hyperf\Redis\Redis;use League\OAuth2\Server\Entities\AccessTokenEntityInterface;use League\OAuth2\Server\Entities\ClientEntityInterface;use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;class AccessTokenRepository implements AccessTokenRepositoryInterface{publicfunction__construct(private Redis$redis){}publicfunctiongetNewToken(ClientEntityInterface$clientEntity, array$scopes, ?string$userIdentifier=null): AccessTokenEntityInterface{$token=new AccessTokenEntity();$token->setClient($clientEntity);$token->setUserIdentifier($userIdentifier);foreach($scopesas$scope){$token->addScope($scope);}return$token;}publicfunctionpersistNewAccessToken(AccessTokenEntityInterface$accessTokenEntity): void{// 用 Redis 存储,避免阻塞协程$this->redis->setex('oauth:token:'.$accessTokenEntity->getIdentifier(),$accessTokenEntity->getExpiryDateTime()->getTimestamp()- time(), json_encode(['client_id'=>$accessTokenEntity->getClient()->getIdentifier(),'user_id'=>$accessTokenEntity->getUserIdentifier(),'scopes'=>array_map(fn($s)=>$s->getIdentifier(),$accessTokenEntity->getScopes()),]));}publicfunctionrevokeAccessToken(string$tokenId): void{$this->redis->del('oauth:token:'.$tokenId);$this->redis->setex('oauth:revoked:'.$tokenId,86400,'1');}publicfunctionisAccessTokenRevoked(string$tokenId): bool{return(bool)$this->redis->exists('oauth:revoked:'.$tokenId);}}--- 五、服务端配置(DI 注册)<?php // config/autoload/dependencies.php use App\OAuth\Repositories\{AccessTokenRepository, AuthCodeRepository, ClientRepository, RefreshTokenRepository, ScopeRepository, UserRepository};use League\OAuth2\Server\AuthorizationServer;use League\OAuth2\Server\Grant\AuthCodeGrant;use League\OAuth2\Server\ResourceServer;use OpenIDConnectServer\ClaimExtractor;use OpenIDConnectServer\IdTokenResponse;use Psr\Container\ContainerInterface;return[AuthorizationServer::class=>function(ContainerInterface$c){$claimExtractor=new ClaimExtractor();$responseType=new IdTokenResponse($c->get(UserRepository::class),$claimExtractor);$server=new AuthorizationServer($c->get(ClientRepository::class),$c->get(AccessTokenRepository::class),$c->get(ScopeRepository::class), // 私钥路径(生产环境用环境变量)'file://'.BASE_PATH.'/storage/oauth/private.key', env('OAUTH_ENCRYPTION_KEY'),$responseType);$authCodeGrant=new AuthCodeGrant($c->get(AuthCodeRepository::class),$c->get(RefreshTokenRepository::class), new\DateInterval('PT10M')// auth code 有效期10分钟);$authCodeGrant->setRefreshTokenTTL(new\DateInterval('P1M'));$server->enableGrantType($authCodeGrant, new\DateInterval('PT1H'));return$server;}, ResourceServer::class=>function(ContainerInterface$c){returnnew ResourceServer($c->get(AccessTokenRepository::class),'file://'.BASE_PATH.'/storage/oauth/public.key');},];--- 六、Controller<?php // app/Http/Controller/OAuthController.php namespace App\Http\Controller;use Hyperf\HttpServer\Annotation\Controller;use Hyperf\HttpServer\Annotation\GetMapping;use Hyperf\HttpServer\Annotation\PostMapping;use League\OAuth2\Server\AuthorizationServer;use League\OAuth2\Server\Exception\OAuthServerException;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;#[Controller(prefix: '/oauth')]class OAuthController{publicfunction__construct(private AuthorizationServer$server){}#[GetMapping(path: '/authorize')]publicfunctionauthorize(ServerRequestInterface$request, ResponseInterface$response): ResponseInterface{try{$authRequest=$this->server->validateAuthorizationRequest($request);// 检查用户是否已登录(从 session/JWT 中获取)$userId=$request->getAttribute('user_id');if(!$userId){// 重定向到登录页,携带 return_to 参数return$response->withHeader('Location','/login?return_to='.urlencode((string)$request->getUri()))->withStatus(302);}$authRequest->setUser(new\App\OAuth\Entities\UserEntity(['id'=>$userId]));$authRequest->setAuthorizationApproved(true);return$this->server->completeAuthorizationRequest($authRequest,$response);}catch(OAuthServerException$e){return$e->generateHttpResponse($response);}}#[PostMapping(path: '/token')]publicfunctiontoken(ServerRequestInterface$request, ResponseInterface$response): ResponseInterface{try{return$this->server->respondToAccessTokenRequest($request,$response);}catch(OAuthServerException$e){return$e->generateHttpResponse($response);}}#[GetMapping(path: '/.well-known/openid-configuration')]publicfunctiondiscovery(): array{$base=env('APP_URL');return['issuer'=>$base,'authorization_endpoint'=>"$base/oauth/authorize",'token_endpoint'=>"$base/oauth/token",'userinfo_endpoint'=>"$base/oauth/userinfo",'jwks_uri'=>"$base/oauth/jwks",'response_types_supported'=>['code'],'subject_types_supported'=>['public'],'id_token_signing_alg_values_supported'=>['RS256'],'scopes_supported'=>['openid','profile','email'],];}}--- 七、资源保护中间件<?php // app/Http/Middleware/OAuthMiddleware.php namespace App\Http\Middleware;use League\OAuth2\Server\Exception\OAuthServerException;use League\OAuth2\Server\ResourceServer;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;use Psr\Http\Server\MiddlewareInterface;use Psr\Http\Server\RequestHandlerInterface;class OAuthMiddleware implements MiddlewareInterface{publicfunction__construct(private ResourceServer$server){}publicfunctionprocess(ServerRequestInterface$request, RequestHandlerInterface$handler): ResponseInterface{try{$request=$this->server->validateAuthenticatedRequest($request);return$handler->handle($request);}catch(OAuthServerException$e){return$e->generateHttpResponse(new\Hyperf\HttpMessage\Server\Response());}}}--- 八、作为 OIDC 客户端(第三方登录)<?php // app/Service/OidcClientService.php namespace App\Service;use GuzzleHttp\Client;use Hyperf\Guzzle\ClientFactory;class OidcClientService{private Client$http;publicfunction__construct(ClientFactory$factory){// Hyperf 的 Guzzle 工厂是协程安全的$this->http=$factory->create(['timeout'=>5.0]);}publicfunctiongetAuthorizationUrl(string$state, string$nonce): string{$params=http_build_query(['response_type'=>'code','client_id'=>env('OIDC_CLIENT_ID'),'redirect_uri'=>env('OIDC_REDIRECT_URI'),'scope'=>'openid profile email','state'=>$state,'nonce'=>$nonce,]);returnenv('OIDC_AUTHORIZATION_ENDPOINT').'?'.$params;}publicfunctionexchangeCode(string$code): array{$response=$this->http->post(env('OIDC_TOKEN_ENDPOINT'),['form_params'=>['grant_type'=>'authorization_code','code'=>$code,'redirect_uri'=>env('OIDC_REDIRECT_URI'),'client_id'=>env('OIDC_CLIENT_ID'),'client_secret'=>env('OIDC_CLIENT_SECRET'),],]);returnjson_decode((string)$response->getBody(),true);} public function verifyIdToken(string $idToken):array {//用 firebase/php-jwt 或 web-token/jwt-framework 验证签名 [$header,$payload]=array_map(fn($part)=>json_decode(base64_decode(strtr($part,'-_','+/')),true), explode('.',$idToken));// 验证 iss、aud、exp、nonceif($payload['iss']!==env('OIDC_ISSUER')){throw new\RuntimeException('Invalid issuer');}if($payload['aud']!==env('OIDC_CLIENT_ID')){throw new\RuntimeException('Invalid audience');}if($payload['exp']<time()){throw new\RuntimeException('Token expired');}return$payload;}}--- 九、密钥生成# 生成 RSA 密钥对(用于 JWT 签名)openssl genrsa-outstorage/oauth/private.key4096openssl rsa-instorage/oauth/private.key-pubout-outstorage/oauth/public.keychmod600storage/oauth/private.key# 生成加密密钥php-r"echo base64_encode(random_bytes(32));"--- 十、关键注意事项 协程安全 - 用 Hyperf\DbConnection\Db 而非原生 PDO,前者自动管理协程连接池 - 用 Hyperf\Guzzle\ClientFactory 创建 HTTP 客户端,每个协程独立连接 - 避免在 Repository 中使用静态变量缓存状态 安全要点 - private.key 权限设为600,不提交到git- OAUTH_ENCRYPTION_KEY 通过环境变量注入,不写死在代码里 - PKCE(code_challenge)对公开客户端是必须的 - state 参数防 CSRF,nonce 防重放攻击 - Token 端点只允许 HTTPS 数据库表(最小集) CREATE TABLE oauth_clients(idVARCHAR(100)PRIMARY KEY, name VARCHAR(255)NOT NULL, secret VARCHAR(255), redirect_uris TEXT NOT NULL, is_confidential TINYINT(1)DEFAULT1);CREATE TABLE oauth_auth_codes(idVARCHAR(100)PRIMARY KEY, user_id BIGINT, client_id VARCHAR(100), scopes TEXT, revoked TINYINT(1)DEFAULT0, expires_at DATETIME);Access Token 和 Refresh Token 建议全部存 Redis,减少数据库压力。Hyperf OAuth 2.0 / OIDC 在 PHP 中的正确实现
张小明
前端开发工程师
Neovim集成本地大语言模型:nvim-llama插件配置与实战指南
1. 项目概述:当Neovim遇上本地大语言模型如果你和我一样,是个重度Neovim用户,同时又对本地运行的大语言模型(LLM)充满好奇,那么jpmcb/nvim-llama这个项目绝对值得你花时间研究。简单来说,它就是…
DLSS Swapper:一键升级游戏画质的终极解决方案,让您的NVIDIA显卡性能飙升45%
DLSS Swapper:一键升级游戏画质的终极解决方案,让您的NVIDIA显卡性能飙升45% 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper DLSS Swapper是一款专为NVIDIA显卡用户设计的智能DLSS文件管理工具&…
解密高效自动化工具:pycatia如何用Python彻底征服CATIA V5
解密高效自动化工具:pycatia如何用Python彻底征服CATIA V5 【免费下载链接】pycatia python module for CATIA V5 automation 项目地址: https://gitcode.com/gh_mirrors/py/pycatia 在机械设计和航空航天领域,CATIA V5作为行业标准CAD软件&#…
如何快速掌握League Akari:英雄联盟玩家的效率提升完整指南
如何快速掌握League Akari:英雄联盟玩家的效率提升完整指南 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power 🚀. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit League Akari是一款基于…
hadoop冷热数据分离
将Hive表的历史数据从三副本改为单副本,以节省存储空间 对于历史数据的副本数调整,修改全局配置后(hdfs-site.xml中的dfs.replication默认副本数)只影响设置生效后新写入的数据。历史数据的副本数不会改变,必须手动执行命令来降低。另外只是对…
3分钟永久激活Windows和Office:KMS_VL_ALL_AIO完整指南
3分钟永久激活Windows和Office:KMS_VL_ALL_AIO完整指南 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 还在为Windows系统激活烦恼吗?KMS_VL_ALL_AIO是你的终极解决方案&…