diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf3f6de --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.buildpath +.settings/ +.project +*.patch +.idea/ +.git/ +runtime/ +vendor/ +temp/ +*.lock +.phpintel/ +.DS_Store diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..7821b07 --- /dev/null +++ b/.php_cs @@ -0,0 +1,39 @@ +setRiskyAllowed(true) + ->setRules([ + '@PSR2' => true, + 'header_comment' => [ + 'commentType' => 'PHPDoc', + 'header' => $header, + 'separate' => 'none' + ], + 'array_syntax' => [ + 'syntax' => 'short' + ], + 'single_quote' => true, + 'class_attributes_separation' => true, + 'no_unused_imports' => true, + 'standardize_not_equals' => true, + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->exclude('public') + ->exclude('resources') + ->exclude('config') + ->exclude('runtime') + ->exclude('vendor') + ->in(__DIR__) + ) + ->setUsingCache(false); + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..93def9e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: php + +php: + - 7.0 + - 7.1 + +services: + - mysql + +before_install: + - mysql -e 'CREATE DATABASE IF NOT EXISTS test;' + +install: + - wget https://github.com/redis/hiredis/archive/v0.13.3.tar.gz -O hiredis.tar.gz && mkdir -p hiredis && tar -xf hiredis.tar.gz -C hiredis --strip-components=1 && cd hiredis && sudo make -j$(nproc) && sudo make install && sudo ldconfig && cd .. + - pecl install -f swoole-2.0.12 + +before_script: + - composer update + +script: composer test + diff --git a/README.md b/README.md new file mode 100644 index 0000000..086fdc2 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Swoft Auth + +Swoft Auth Component + +## Install + +- composer command + +```bash +composer require swoft/auth +``` + +## Document [wiki](https://github.com/aprchen/swoft-auth/wiki) + +now +- BasicAuth +- BearerToken (JWT) +- Acl + +feature +- oauth 2.0 + + + +## Unit testing + +```bash +phpunit +``` + +## LICENSE + +The Component is open-sourced software licensed under the [Apache license](LICENSE). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c24d5fa --- /dev/null +++ b/composer.json @@ -0,0 +1,41 @@ +{ + "name": "swoft/auth", + "type": "library", + "keywords": [ + "php", + "swoole", + "swoft" + ], + "description": "microservice framework base on swoole", + "license": "Apache-2.0", + "require": { + "swoft/http-server": "^1.0", + "firebase/php-jwt": "^5.0", + "psr/simple-cache": "^1.0", + "swoft/framework": "^1.0" + }, + "autoload": { + "psr-4": { + "Swoft\\Auth\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "SwoftTest\\Auth\\": "test/Cases" + } + }, + "repositories": [ + { + "type": "composer", + "url": "https://packagist.phpcomposer.com" + } + ], + "require-dev": { + "eaglewu/swoole-ide-helper": "dev-master", + "phpunit/phpunit": "^5.7", + "friendsofphp/php-cs-fixer": "^2.11" + }, + "scripts": { + "test": "./vendor/bin/phpunit -c phpunit.xml" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..37e68d6 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,22 @@ + + + + + ./test/Cases + + + + + ./app + + + + diff --git a/src/AuthManager.php b/src/AuthManager.php new file mode 100644 index 0000000..b695ab8 --- /dev/null +++ b/src/AuthManager.php @@ -0,0 +1,261 @@ +sessionDuration; + } + + public function setSessionDuration($time) + { + $this->sessionDuration = $time; + } + + /** + * @return AuthSession; + */ + public function getSession() + { + return RequestContext::getContextDataByKey(AuthConstants::AUTH_SESSION); + } + + /** + * @param AuthSession $session + */ + public function setSession(AuthSession $session) + { + RequestContext::setContextData([AuthConstants::AUTH_SESSION => $session]); + } + + /** + * @return bool + * + * Check if a user is currently logged in + */ + public function isLoggedIn() + { + return $this->getSession() instanceof AuthSession; + } + + /** + * @param $accountTypeName + * @param array $data + * @return AuthSession + */ + public function login(string $accountTypeName, array $data):AuthSession + { + if (!$account = $this->getAccountType($accountTypeName)) { + throw new AuthException(ErrorCode::AUTH_INVALID_ACCOUNT_TYPE); + } + $result = $account->login($data); + if (!$result instanceof AuthResult || $result->getIdentity() == '') { + throw new AuthException(ErrorCode::AUTH_LOGIN_FAILED); + } + $session = $this->generateSession($accountTypeName, $result->getIdentity(), $result->getExtendedData()); + $this->setSession($session); + if ($this->cacheEnable === true) { + try { + $this->getCacheClient()->set( + $this->getCacheKey($result->getIdentity()), + $session->getToken(), + $session->getExpirationTime() + ); + } catch (InvalidArgumentException $e) { + $err = sprintf('%s Invalid Argument : %s', $session->getIdentity(), $e->getMessage()); + throw new AuthException(ErrorCode::POST_DATA_NOT_PROVIDED, $err); + } + } + return $session; + } + + protected function getCacheKey($identity) + { + return $this->prefix . $identity; + } + + /** + * @param string $accountTypeName + * @param string $identity + * @param array $data + * @return AuthSession + */ + public function generateSession(string $accountTypeName, string $identity, array $data = []) + { + $startTime = time(); + $exp = $startTime + (int)$this->sessionDuration; + $session = new AuthSession(); + $session + ->setExtendedData($data) + ->setExpirationTime($exp) + ->setCreateTime($startTime) + ->setIdentity($identity) + ->setAccountTypeName($accountTypeName); + $session->setExtendedData($data); + $token = $this->getTokenParser()->getToken($session); + $session->setToken($token); + return $session; + } + + /** + * @param $name + * @return AccountTypeInterface|null + */ + public function getAccountType($name) + { + if (!App::hasBean($name)) { + return null; + } + $account = App::getBean($name); + if (!$account instanceof AccountTypeInterface) { + return null; + } + return $account; + } + + /** + * @return TokenParserInterface + */ + public function getTokenParser(): TokenParserInterface + { + if (!$this->tokenParser instanceof TokenParserInterface) { + if (!App::hasBean($this->tokenParserClass)) { + throw new RuntimeException('Can`t find tokenParserClass'); + } + $tokenParser = App::getBean($this->tokenParserClass); + if (!$tokenParser instanceof TokenParserInterface) { + throw new RuntimeException("TokenParser need implements Swoft\Auth\Mapping\TokenParserInterface "); + } + $this->tokenParser = $tokenParser; + } + return $this->tokenParser; + } + + /** + * @return CacheInterface + */ + public function getCacheClient() + { + if (!$this->cache instanceof CacheInterface) { + if (!App::hasBean($this->cacheClass)) { + throw new RuntimeException('Can`t find cacheClass'); + } + $cache = App::getBean($this->cacheClass); + if (!$cache instanceof CacheInterface) { + throw new RuntimeException('CacheClient need implements Psr\SimpleCache\CacheInterface'); + } + $this->cache = $cache; + } + return $this->cache; + } + + /** + * @param $token + * @return bool + * @throws AuthException + */ + public function authenticateToken(string $token):bool + { + try { + /** @var AuthSession $session */ + $session = $this->getTokenParser()->getSession($token); + } catch (\Exception $e) { + throw new AuthException(ErrorCode::AUTH_TOKEN_INVALID); + } + + if (!$session) { + return false; + } + + if ($session->getExpirationTime() < time()) { + throw new AuthException(ErrorCode::AUTH_SESSION_EXPIRED); + } + + if (!$account = $this->getAccountType($session->getAccountTypeName())) { + throw new AuthException(ErrorCode::AUTH_SESSION_INVALID); + } + + if (!$account->authenticate($session->getIdentity())) { + throw new AuthException(ErrorCode::AUTH_TOKEN_INVALID); + } + + if ($this->cacheEnable === true) { + try { + $cache = $this->getCacheClient()->get($this->getCacheKey($session->getIdentity())); + if (!$cache || $cache !== $token) { + throw new AuthException(ErrorCode::AUTH_TOKEN_INVALID); + } + } catch (InvalidArgumentException $e) { + $err = sprintf('%s 参数无效,message : %s', $session->getIdentity(), $e->getMessage()); + throw new AuthException(ErrorCode::POST_DATA_NOT_PROVIDED, $err); + } + } + + $this->setSession($session); + return true; + } +} diff --git a/src/AuthUserService.php b/src/AuthUserService.php new file mode 100644 index 0000000..a538caa --- /dev/null +++ b/src/AuthUserService.php @@ -0,0 +1,83 @@ +getSession()) { + return ''; + } + return $this->getSession()->getIdentity() ?? ''; + } + + public function getUserExtendData(): array + { + if (!$this->getSession()) { + return []; + } + return $this->getSession()->getExtendedData() ?? []; + } + + /** + * @return AuthSession |null + */ + public function getSession() + { + return RequestContext::getContextDataByKey(AuthConstants::AUTH_SESSION) ?? null; + } + + /** + * + * $controller = $this->getHandlerArray($requestHandler)[0]; + * $method = $this->getHandlerArray($requestHandler)[1]; + * $id = $this->getUserIdentity(); + * if ($id) { + * return true; + * } + * return false; + * + * + * @param string $requestHandler + * @param ServerRequestInterface $request + * @return bool + */ + public function auth(string $requestHandler, ServerRequestInterface $request): bool + { + throw new AuthException(ErrorCode::POST_DATA_NOT_PROVIDED, sprintf('AuthUserService::auth() method should be implemented in %s', static::class)); + } + + /** + * @param string $handler + * @return array|null + */ + protected function getHandlerArray(string $handler) + { + $segments = explode('@', trim($handler)); + if (!isset($segments[1])) { + return null; + } + return $segments; + } +} diff --git a/src/Bean/AuthResult.php b/src/Bean/AuthResult.php new file mode 100644 index 0000000..f484fa9 --- /dev/null +++ b/src/Bean/AuthResult.php @@ -0,0 +1,68 @@ +identity; + } + + /** + * @param string $identity + * @return AuthResult + */ + public function setIdentity(string $identity) + { + $this->identity = $identity; + return $this; + } + + /** + * @return array + */ + public function getExtendedData(): array + { + return $this->extendedData; + } + + /** + * @param array $extendedData + * @return AuthResult + */ + public function setExtendedData(array $extendedData) + { + $this->extendedData = $extendedData; + return $this; + } +} diff --git a/src/Bean/AuthSession.php b/src/Bean/AuthSession.php new file mode 100644 index 0000000..10bd75e --- /dev/null +++ b/src/Bean/AuthSession.php @@ -0,0 +1,160 @@ +identity; + } + + /** + * @param string $identity + * @return AuthSession + */ + public function setIdentity(string $identity) + { + $this->identity = $identity; + return $this; + } + + /** + * @return string + */ + public function getAccountTypeName(): string + { + return $this->accountTypeName; + } + + /** + * @param string $accountTypeName + * @return AuthSession + */ + public function setAccountTypeName(string $accountTypeName) + { + $this->accountTypeName = $accountTypeName; + return $this; + } + + /** + * @return string + */ + public function getToken(): string + { + return $this->token; + } + + /** + * @param string $token + * @return AuthSession + */ + public function setToken(string $token) + { + $this->token = $token; + return $this; + } + + /** + * @return int + */ + public function getCreateTime(): int + { + return $this->createTime; + } + + /** + * @param int $createTime + * @return AuthSession + */ + public function setCreateTime(int $createTime) + { + $this->createTime = $createTime; + return $this; + } + + /** + * @return int + */ + public function getExpirationTime(): int + { + return $this->expirationTime; + } + + /** + * @param int $expirationTime + * @return AuthSession + */ + public function setExpirationTime(int $expirationTime) + { + $this->expirationTime = $expirationTime; + return $this; + } + + /** + * @return array + */ + public function getExtendedData() + { + return $this->extendedData; + } + + /** + * @param $extendedData + * @return AuthSession + */ + public function setExtendedData($extendedData) + { + $this->extendedData = $extendedData; + return $this; + } +} diff --git a/src/Bootstrap/CoreBean.php b/src/Bootstrap/CoreBean.php new file mode 100644 index 0000000..7fc8329 --- /dev/null +++ b/src/Bootstrap/CoreBean.php @@ -0,0 +1,48 @@ + [ + 'class' => AuthorizationHeaderParser::class + ], + AuthManagerInterface::class=>[ + 'class' => AuthManager::class, + 'tokenParserClass'=>JWTTokenParser::class, + ], + AuthServiceInterface::class=>[ + 'class'=>AuthUserService::class + ] + ]; + } +} diff --git a/src/Constants/AuthConstants.php b/src/Constants/AuthConstants.php new file mode 100644 index 0000000..c22e858 --- /dev/null +++ b/src/Constants/AuthConstants.php @@ -0,0 +1,27 @@ + [ + 'statusCode' => 500, + 'message' => 'General: System Error' + ], + + ErrorCode::GENERAL_NOT_IMPLEMENTED => [ + 'statusCode' => 500, + 'message' => 'General: Not Implemented' + ], + + ErrorCode::GENERAL_NOT_FOUND => [ + 'statusCode' => 404, + 'message' => 'General: Not Found' + ], + + // Authentication + ErrorCode::AUTH_INVALID_ACCOUNT_TYPE => [ + 'statusCode' => 400, + 'message' => 'Authentication: Invalid Account Type' + ], + + ErrorCode::AUTH_LOGIN_FAILED => [ + 'statusCode' => 401, + 'message' => 'Authentication: Login Failed' + ], + + ErrorCode::AUTH_TOKEN_INVALID => [ + 'statusCode' => 401, + 'message' => 'Authentication: Login Failed' + ], + + ErrorCode::AUTH_SESSION_EXPIRED => [ + 'statusCode' => 401, + 'message' => 'Authentication: Session Expired' + ], + + ErrorCode::AUTH_SESSION_INVALID => [ + 'statusCode' => 401, + 'message' => 'Authentication: Session Invalid' + ], + + ErrorCode::ACCESS_DENIED => [ + 'statusCode' => 403, + 'message' => 'Access: Denied' + ], + + ErrorCode::DATA_FAILED => [ + 'statusCode' => 500, + 'message' => 'Data: Failed' + ], + + ErrorCode::DATA_NOT_FOUND => [ + 'statusCode' => 404, + 'message' => 'Data: Not Found' + ], + + ErrorCode::POST_DATA_NOT_PROVIDED => [ + 'statusCode' => 400, + 'message' => 'Postdata: Not provided' + ], + + ErrorCode::POST_DATA_INVALID => [ + 'statusCode' => 400, + 'message' => 'Postdata: Invalid' + ] + ]; + + /** + * @param $code + * + * @return array|null + */ + public function get($code) + { + return $this->has($code) ? $this->getErrors()[$code] : null; + } + + /** + * @param $code + * + * @return bool + */ + public function has($code) + { + return array_key_exists($code, $this->getErrors()); + } + + /** + * @param $code + * @param $message + * @param $statusCode + * @return ErrorCodeHelper + */ + public function error($code, $message, $statusCode) + { + $this->errors[$code] = [ + 'statusCode' => $statusCode, + 'message' => $message + ]; + + return $this; + } + + /** + * @return array + */ + public function getErrors() + { + return $this->errors; + } +} diff --git a/src/Mapping/AccountTypeInterface.php b/src/Mapping/AccountTypeInterface.php new file mode 100644 index 0000000..57d0918 --- /dev/null +++ b/src/Mapping/AccountTypeInterface.php @@ -0,0 +1,34 @@ + + * $controller = $this->getHandlerArray($requestHandler)[0]; + * $method = $this->getHandlerArray($requestHandler)[1]; + * $id = $this->getUserIdentity(); + * if ($id) { + * return true; + * } + * return false; + * + * + * @param string $requestHandler + * @param ServerRequestInterface $request + * @return bool + */ + public function auth(string $requestHandler, ServerRequestInterface $request): bool; +} diff --git a/src/Mapping/AuthorizationParserInterface.php b/src/Mapping/AuthorizationParserInterface.php new file mode 100644 index 0000000..b96c8ac --- /dev/null +++ b/src/Mapping/AuthorizationParserInterface.php @@ -0,0 +1,22 @@ +getAttributes()['requestHandler'][2]['handler'] ?? ''; + $service = App::getBean(AuthServiceInterface::class); + if (!$service instanceof AuthServiceInterface) { + throw new AuthException(ErrorCode::POST_DATA_NOT_PROVIDED, 'AuthService should implement Swoft\Auth\Mapping\AuthServiceInterface'); + } + if (!$service->auth($requestHandler, $request)) { + throw new AuthException(ErrorCode::ACCESS_DENIED); + } + $response = $handler->handle($request); + return $response; + } +} diff --git a/src/Middleware/AuthMiddleware.php b/src/Middleware/AuthMiddleware.php new file mode 100644 index 0000000..e45bf89 --- /dev/null +++ b/src/Middleware/AuthMiddleware.php @@ -0,0 +1,48 @@ +parse($request); + $response = $handler->handle($request); + return $response; + } +} diff --git a/src/Parser/AuthorizationHeaderParser.php b/src/Parser/AuthorizationHeaderParser.php new file mode 100644 index 0000000..0f6c786 --- /dev/null +++ b/src/Parser/AuthorizationHeaderParser.php @@ -0,0 +1,80 @@ +getHeaderLine($this->headerKey); + $type = $this->getHeadString($authValue); + if (isset($this->mergeTypes()[$type])) { + $handler = App::getBean($this->mergeTypes()[$type]); + if (!$handler instanceof AuthHandlerInterface) { + throw new AuthException(ErrorCode::POST_DATA_NOT_PROVIDED, sprintf('%s should implement Swoft\Auth\Mapping\AuthHandlerInterface', $this->mergeTypes()[$type])); + } + $request = $handler->handle($request); + } + return $request; + } + + private function getHeadString(string $val):string + { + return explode(' ', $val)[0] ?? ''; + } + + private function mergeTypes(): array + { + if (empty($this->authTypes)) { + $this->authTypes = ArrayHelper::merge($this->types, $this->defaultTypes()); + } + return $this->authTypes; + } + + public function defaultTypes(): array + { + return [ + BearerTokenHandler::NAME => BearerTokenHandler::class, + BasicAuthHandler::NAME => BasicAuthHandler::class + ]; + } +} diff --git a/src/Parser/Handler/BasicAuthHandler.php b/src/Parser/Handler/BasicAuthHandler.php new file mode 100644 index 0000000..6f4695a --- /dev/null +++ b/src/Parser/Handler/BasicAuthHandler.php @@ -0,0 +1,64 @@ +getHeaderLine(AuthConstants::HEADER_KEY) ?? ''; + $basic = $this->parseValue($authHeader); + if ($basic) { + $request = $request + ->withAttribute(AuthConstants::BASIC_USER_NAME, $this->getUsername($basic)) + ->withAttribute(AuthConstants::BASIC_PASSWORD, $this->getPassword($basic)); + } + return $request; + } + + protected function getUsername(array $basic) + { + return $basic[0]??''; + } + + protected function getPassword(array $basic) + { + return $basic[1]??''; + } + + protected function parseValue($string):array + { + if (strpos(trim($string), self::NAME) !== 0) { + return null; + } + $val = preg_replace('/.*\s/', '', $string); + if (!$val) { + return null; + } + return explode(':', base64_decode($val)); + } +} diff --git a/src/Parser/Handler/BearerTokenHandler.php b/src/Parser/Handler/BearerTokenHandler.php new file mode 100644 index 0000000..f48f0cd --- /dev/null +++ b/src/Parser/Handler/BearerTokenHandler.php @@ -0,0 +1,59 @@ +getToken($request); + /** @var AuthManagerInterface $manager */ + $manager = App::getBean(AuthManagerInterface::class); + if ($token) { + $res = $manager->authenticateToken($token); + $request = $request->withAttribute(AuthConstants::IS_LOGIN, $res); + } + return $request; + } + + protected function getToken(ServerRequestInterface $request) + { + $authHeader = $request->getHeaderLine(AuthConstants::HEADER_KEY) ?? ''; + $authQuery = $request->getQueryParams()['token'] ?? ''; + return $authQuery ? $authQuery : $this->parseValue($authHeader); + } + + protected function parseValue($string) + { + if (strpos(trim($string), self::NAME) !== 0) { + return null; + } + return preg_replace('/.*\s/', '', $string); + } +} diff --git a/src/Parser/JWTTokenParser.php b/src/Parser/JWTTokenParser.php new file mode 100644 index 0000000..f9212c0 --- /dev/null +++ b/src/Parser/JWTTokenParser.php @@ -0,0 +1,142 @@ +create( + $session->getAccountTypeName(), + $session->getIdentity(), + $session->getCreateTime(), + $session->getExpirationTime(), + $session->getExtendedData() + ); + return $this->encode($tokenData); + } + + /** + * @param string $token + * @return AuthSession + */ + public function getSession(string $token):AuthSession + { + $tokenData = $this->decode($token); + return (new AuthSession()) + ->setAccountTypeName($tokenData->iss) + ->setIdentity($tokenData->sub) + ->setCreateTime($tokenData->iat) + ->setExpirationTime($tokenData->exp) + ->setToken($token) + ->setExtendedData($tokenData->data); + } + + protected function create(string $issuer, string $user, int $iat, int $exp, array $data):array + { + return [ + /* + The iss (issuer) claim identifies the principal + that issued the JWT. The processing of this claim + is generally application specific. + The iss value is a case-sensitive string containing + a StringOrURI value. Use of this claim is OPTIONAL. + ------------------------------------------------*/ + 'iss' => $issuer, + + /* + The sub (subject) claim identifies the principal + that is the subject of the JWT. The Claims in a + JWT are normally statements about the subject. + The subject value MUST either be scoped to be + locally unique in the context of the issuer or + be globally unique. The processing of this claim + is generally application specific. The sub value + is a case-sensitive string containing a + StringOrURI value. Use of this claim is OPTIONAL. + ------------------------------------------------*/ + 'sub' => $user, + + /* + The iat (issued at) claim identifies the time at + which the JWT was issued. This claim can be used + to determine the age of the JWT. Its value MUST + be a number containing a NumericDate value. + Use of this claim is OPTIONAL. + ------------------------------------------------*/ + 'iat' => $iat, + + /* + The exp (expiration time) claim identifies the + expiration time on or after which the JWT MUST NOT + be accepted for processing. The processing of the + exp claim requires that the current date/time MUST + be before the expiration date/time listed in the + exp claim. Implementers MAY provide for some small + leeway, usually no more than a few minutes, + to account for clock skew. Its value MUST be a + number containing a NumericDate value. + Use of this claim is OPTIONAL. + ------------------------------------------------*/ + 'exp' => $exp, + + /* + Expand data + ------------------------------------------------*/ + 'data' => $data, + ]; + } + + public function encode($token): string + { + return (string)JWT::encode($token, $this->secret, $this->algorithm); + } + + public function decode($token) + { + return JWT::decode($token, $this->secret, [$this->algorithm]); + } +} diff --git a/test/.env b/test/.env new file mode 100644 index 0000000..c0280f4 --- /dev/null +++ b/test/.env @@ -0,0 +1,50 @@ +# test config +TEST_NAME=test +TEST_URI=127.0.0.1:6378 +TEST_MAX_IDEL=2 +TEST_MAX_ACTIVE=2 +TEST_MAX_WAIT=2 +TEST_TIMEOUT=2 +TEST_BALANCER=r1 +TEST_USE_PROVIDER=true +TEST_PROVIDER=c1 + +# the pool of master nodes pool +DB_NAME=master2 +DB_URI=127.0.0.1:3302,127.0.0.1:3302 +DB_MAX_IDEL=2 +DB_MAX_ACTIVE=2 +DB_MAX_WAIT=2 +DB_TIMEOUT=2 +DB_USE_PROVIDER=true +DB_BALANCER=random2 +DB_PROVIDER=consul2 + +# the pool of slave nodes pool +DB_SLAVE_NAME=slave2 +DB_SLAVE_URI=127.0.0.1:3306/test?user=root&password=&charset=utf8,127.0.0.1:3306/test?user=root&password=&charset=utf8 +DB_SLAVE_MAX_IDEL=2 +DB_SLAVE_MAX_ACTIVE=2 +DB_SLAVE_MAX_WAIT=2 +DB_SLAVE_TIMEOUT=2 +DB_SLAVE_USE_PROVIDER=false +DB_SLAVE_BALANCER=random +DB_SLAVE_PROVIDER=consul2 + +# the pool of redis +REDIS_NAME=redis2 +REDIS_URI=127.0.0.1:2222,127.0.0.1:2222 +REDIS_MAX_IDEL=2 +REDIS_MAX_ACTIVE=2 +REDIS_MAX_WAIT=2 +REDIS_TIMEOUT=2 +REDIS_USE_PROVIDER=true +REDIS_BALANCER=random2 +REDIS_PROVIDER=consul2 + + +# consul provider +PROVIDER_CONSUL_ADDRESS=http://127.0.0.1:82 +PROVIDER_CONSUL_TAGS=1,2 +PROVIDER_CONSUL_TIMEOUT=2 +PROVIDER_CONSUL_INTERVAL=2 diff --git a/test/Cases/AbstractTestCase.php b/test/Cases/AbstractTestCase.php new file mode 100644 index 0000000..52fda46 --- /dev/null +++ b/test/Cases/AbstractTestCase.php @@ -0,0 +1,214 @@ +get('/', function () { + return [1]; + }); + } + + /** + * Send a mock request + * + * @param string $method + * @param string $uri + * @param array $parameters + * @param string $accept + * @param array $headers + * @param string $rawContent + * @return bool|\Swoft\Http\Message\Testing\Web\Response + */ + public function request( + string $method, + string $uri, + array $parameters = [], + string $accept = self::ACCEPT_JSON, + array $headers = [], + string $rawContent = '' + ) { + $method = strtoupper($method); + $swooleResponse = new TestSwooleResponse(); + $swooleRequest = new TestSwooleRequest(); + $this->registerRoute(); + $this->buildMockRequest($method, $uri, $parameters, $accept, $swooleRequest, $headers); + + $swooleRequest->setRawContent($rawContent); + + $request = Request::loadFromSwooleRequest($swooleRequest); + $response = new Response($swooleResponse); + + /** @var \Swoft\Http\Server\ServerDispatcher $dispatcher */ + $dispatcher = App::getBean('serverDispatcher'); + return $dispatcher->dispatch($request, $response); + } + + /** + * Send a mock json request + * + * @param string $method + * @param string $uri + * @param array $parameters + * @param array $headers + * @param string $rawContent + * @return bool|\Swoft\Http\Message\Testing\Web\Response + */ + public function json( + string $method, + string $uri, + array $parameters = [], + array $headers = [], + string $rawContent = '' + ) { + return $this->request($method, $uri, $parameters, self::ACCEPT_JSON, $headers, $rawContent); + } + + /** + * Send a mock view request + * + * @param string $method + * @param string $uri + * @param array $parameters + * @param array $headers + * @param string $rawContent + * @return bool|\Swoft\Http\Message\Testing\Web\Response + */ + public function view( + string $method, + string $uri, + array $parameters = [], + array $headers = [], + string $rawContent = '' + ) { + return $this->request($method, $uri, $parameters, self::ACCEPT_VIEW, $headers, $rawContent); + } + + /** + * Send a mock raw content request + * + * @param string $method + * @param string $uri + * @param array $parameters + * @param array $headers + * @param string $rawContent + * @return bool|\Swoft\Http\Message\Testing\Web\Response + */ + public function raw( + string $method, + string $uri, + array $parameters = [], + array $headers = [], + string $rawContent = '' + ) { + return $this->request($method, $uri, $parameters, self::ACCEPT_RAW, $headers, $rawContent); + } + + /** + * @param string $method + * @param string $uri + * @param array $parameters + * @param string $accept + * @param \Swoole\Http\Request $swooleRequest + * @param array $headers + */ + protected function buildMockRequest( + string $method, + string $uri, + array $parameters, + string $accept, + &$swooleRequest, + array $headers = [] + ) { + $urlAry = parse_url($uri); + $urlParams = []; + if (isset($urlAry['query'])) { + parse_str($urlAry['query'], $urlParams); + } + $defaultHeaders = [ + 'host' => '127.0.0.1', + 'connection' => 'keep-alive', + 'cache-control' => 'max-age=0', + 'user-agent' => 'PHPUnit', + 'upgrade-insecure-requests' => '1', + 'accept' => $accept, + 'dnt' => '1', + 'accept-encoding' => 'gzip, deflate, br', + 'accept-language' => 'zh-CN,zh;q=0.8,en;q=0.6,it-IT;q=0.4,it;q=0.2', + ]; + + $swooleRequest->fd = 1; + $swooleRequest->header = ArrayHelper::merge($headers, $defaultHeaders); + $swooleRequest->server = [ + 'request_method' => $method, + 'request_uri' => $uri, + 'path_info' => '/', + 'request_time' => microtime(), + 'request_time_float' => microtime(true), + 'server_port' => 80, + 'remote_port' => 54235, + 'remote_addr' => '10.0.2.2', + 'master_time' => microtime(), + 'server_protocol' => 'HTTP/1.1', + 'server_software' => 'swoole-http-server', + ]; + + if ($method == 'GET') { + $swooleRequest->get = $parameters; + } elseif ($method == 'POST') { + $swooleRequest->post = $parameters; + } + + if (!empty($urlParams)) { + $get = empty($swooleRequest->get) ? [] : $swooleRequest->get; + $swooleRequest->get = array_merge($urlParams, $get); + } + } + + protected function tearDown() + { + parent::tearDown(); + swoole_timer_after(1 * 1000, function () { + swoole_event_exit(); + }); + } + + protected function setCoName($name): String + { + $name = "{$name}-co"; + + return $name; + } +} diff --git a/test/Cases/Account/TestAccount.php b/test/Cases/Account/TestAccount.php new file mode 100644 index 0000000..ca18a69 --- /dev/null +++ b/test/Cases/Account/TestAccount.php @@ -0,0 +1,46 @@ +setIdentity(1); + $result->setExtendedData([]); + return $result; + } + + /** + * @param string $identity Identity + * + * @return bool Authentication successful + */ + public function authenticate(string $identity): bool + { + return true; + } +} diff --git a/test/Cases/Handler/BasicAuthParserTest.php b/test/Cases/Handler/BasicAuthParserTest.php new file mode 100644 index 0000000..a3c78a7 --- /dev/null +++ b/test/Cases/Handler/BasicAuthParserTest.php @@ -0,0 +1,49 @@ +get('/', function (Request $request) { + $name = $request->getAttribute(AuthConstants::BASIC_USER_NAME); + $pd = $request->getAttribute(AuthConstants::BASIC_PASSWORD); + return ['username' => $name, 'password' => $pd]; + }); + } + + /** + * @test + * @covers BasicAuthHandler::handle() + */ + public function testHandle() + { + $username = 'user'; + $password = '123'; + $parser = base64_encode($username . ':' . $password); + $response = $this->request('GET', '/', [], self::ACCEPT_JSON, ['Authorization' => 'Basic ' . $parser], 'test'); + $res = $response->getBody()->getContents(); + $this->assertEquals(json_decode($res, true), ['username' => $username, 'password' => $password]); + } +} diff --git a/test/Cases/Handler/BearerTokenParserTest.php b/test/Cases/Handler/BearerTokenParserTest.php new file mode 100644 index 0000000..9d37a65 --- /dev/null +++ b/test/Cases/Handler/BearerTokenParserTest.php @@ -0,0 +1,48 @@ +get('/test', function (Request $request) { + /** @var AuthUserService $service */ + $service = App::getBean(AuthServiceInterface::class); + $session = $service->getSession(); + return ['id'=>$session->getIdentity()]; + }); + } + + /** + * @test + * @covers AuthManager::authenticateToken() + * @covers BearerTokenHandler::handle() + * @covers AuthUserService::getSession() + */ + public function testHandle() + { + $jwt = new JWTTokenParserTest(); + $token = $jwt->testGetToken(); + $response = $this->request('GET', '/test', [], self::ACCEPT_JSON, ['Authorization' => 'Bearer ' . $token], 'test'); + $res = $response->getBody()->getContents(); + $this->assertEquals(json_decode($res, true), ['id' => 1]); + } +} diff --git a/test/Cases/Helper/ErrorCodeHelperTest.php b/test/Cases/Helper/ErrorCodeHelperTest.php new file mode 100644 index 0000000..d483ca5 --- /dev/null +++ b/test/Cases/Helper/ErrorCodeHelperTest.php @@ -0,0 +1,29 @@ +get(ErrorCode::ACCESS_DENIED); + $this->assertArrayHasKey('statusCode', $arr); + } +} diff --git a/test/Cases/Parser/JWTTokenParserTest.php b/test/Cases/Parser/JWTTokenParserTest.php new file mode 100644 index 0000000..a9348e4 --- /dev/null +++ b/test/Cases/Parser/JWTTokenParserTest.php @@ -0,0 +1,50 @@ +setIdentity(1); + $session->setExpirationTime(time()+10); + $session->setAccountTypeName(TestAccount::class); + $token = $parser->getToken($session); + $this->assertStringStartsWith('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9', $token); + return $token; + } + + /** + * @test + * @covers JWTTokenParser::getSession() + */ + public function testGetSession() + { + $token = $this->testGetToken(); + $parser = App::getBean(JWTTokenParser::class); + /** @var AuthSession $session */ + $session = $parser->getSession($token); + $this->assertEquals(1, $session->getIdentity()); + } +} diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..b7aae49 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,30 @@ +bootstrap(); + +\Swoft\Bean\BeanFactory::reload([ + 'application' => [ + 'class' => \Swoft\Testing\Application::class, + 'inTest' => true + ], +]); +$initApplicationContext = new \Swoft\Core\InitApplicationContext(); +$initApplicationContext->init(); diff --git a/test/config/beans/base.php b/test/config/beans/base.php new file mode 100644 index 0000000..25772e0 --- /dev/null +++ b/test/config/beans/base.php @@ -0,0 +1,16 @@ + [ + 'middlewares' => [ + Swoft\Auth\Middleware\AuthMiddleware::class, + ] + ], +]; diff --git a/test/config/beans/log.php b/test/config/beans/log.php new file mode 100644 index 0000000..14e02d8 --- /dev/null +++ b/test/config/beans/log.php @@ -0,0 +1,41 @@ + [ + 'class' => \Swoft\Log\FileHandler::class, + 'logFile' => '@runtime/logs/notice.log', + 'formatter' => '${lineFormatter}', + 'levels' => [ + \Swoft\Log\Logger::NOTICE, + \Swoft\Log\Logger::INFO, + \Swoft\Log\Logger::DEBUG, + \Swoft\Log\Logger::TRACE, + ] + ], + 'applicationHandler' => [ + 'class' => \Swoft\Log\FileHandler::class, + 'logFile' => '@runtime/logs/error.log', + 'formatter' => '${lineFormatter}', + 'levels' => [ + \Swoft\Log\Logger::ERROR, + \Swoft\Log\Logger::WARNING + ] + ], + 'logger' => [ + 'class' => \Swoft\Log\Logger::class, + 'name' => APP_NAME, + 'flushInterval' => 100, + 'flushRequest' => true, + 'handlers' => [ + '${noticeHandler}', + '${applicationHandler}' + ] + ], +]; diff --git a/test/config/define.php b/test/config/define.php new file mode 100644 index 0000000..410c414 --- /dev/null +++ b/test/config/define.php @@ -0,0 +1,33 @@ + BASE_PATH, + '@app' => '@root/app', + '@res' => '@root/resources', + '@runtime' => '@root/runtime', + '@configs' => '@root/config', + '@resources' => '@root/resources', + '@beans' => '@configs/beans', + '@properties' => '@configs/properties', + '@console' => '@beans/console.php', +]; +App::setAliases($aliases); diff --git a/test/config/properties/app.php b/test/config/properties/app.php new file mode 100644 index 0000000..7d5c60b --- /dev/null +++ b/test/config/properties/app.php @@ -0,0 +1,37 @@ + '1.0', + 'autoInitBean' => true, + 'bootScan' => [ + 'Swoft\\Auth' => BASE_PATH . '/../src', + ], + 'beanScan' => [ + 'Swoft\\Auth' => BASE_PATH . '/../src', + 'SwoftTest\\Auth\\Account'=> BASE_PATH .'/Cases/Account' + ], + 'I18n' => [ + 'sourceLanguage' => '@root/resources/messages/', + ], + 'env' => 'Base', + 'auth' => [ + 'jwt' => [ + 'algorithm' => 'HS256', + 'secret' => '1231231' + ] + ], + 'Service' => [ + 'user' => [ + 'timeout' => 3000 + ] + ], + 'cache' => require dirname(__FILE__) . DS . 'cache.php', +]; diff --git a/test/config/properties/cache.php b/test/config/properties/cache.php new file mode 100644 index 0000000..ddc2d61 --- /dev/null +++ b/test/config/properties/cache.php @@ -0,0 +1,25 @@ + [ + 'name' => 'redis1', + 'uri' => [ + '127.0.0.1:1111', + '127.0.0.1:1111', + ], + 'maxIdel' => 1, + 'maxActive' => 1, + 'maxWait' => 1, + 'timeout' => 1, + 'balancer' => 'random1', + 'useProvider' => true, + 'provider' => 'consul1', + ], +]; diff --git a/test/config/server.php b/test/config/server.php new file mode 100644 index 0000000..26030c8 --- /dev/null +++ b/test/config/server.php @@ -0,0 +1,45 @@ + [ + 'pfile' => env('PFILE', '/tmp/swoft.pid'), + 'pname' => env('PNAME', 'php-swoft'), + 'tcpable' => env('TCPABLE', true), + 'cronable' => env('CRONABLE', false), + 'autoReload' => env('AUTO_RELOAD', true), + ], + 'tcp' => [ + 'host' => env('TCP_HOST', '0.0.0.0'), + 'port' => env('TCP_PORT', 8099), + 'mode' => env('TCP_MODE', SWOOLE_PROCESS), + 'type' => env('TCP_TYPE', SWOOLE_SOCK_TCP), + 'package_max_length' => env('TCP_PACKAGE_MAX_LENGTH', 2048), + 'open_eof_check' => env('TCP_OPEN_EOF_CHECK', false), + ], + 'http' => [ + 'host' => env('HTTP_HOST', '0.0.0.0'), + 'port' => env('HTTP_PORT', 80), + 'mode' => env('HTTP_MODE', SWOOLE_PROCESS), + 'type' => env('HTTP_TYPE', SWOOLE_SOCK_TCP), + ], + 'crontab' => [ + 'task_count' => env('CRONTAB_TASK_COUNT', 1024), + 'task_queue' => env('CRONTAB_TASK_QUEUE', 2048), + ], + 'setting' => [ + 'worker_num' => env('WORKER_NUM', 1), + 'max_request' => env('MAX_REQUEST', 10000), + 'daemonize' => env('DAEMONIZE', 0), + 'dispatch_mode' => env('DISPATCH_MODE', 2), + 'log_file' => env('LOG_FILE', '@runtime/logs/swoole.log'), + 'task_worker_num' => env('TASK_WORKER_NUM', 1), + 'upload_tmp_dir' => env('UPLOAD_TMP_DIR', '@runtime/uploadfiles'), + ], +];