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'),
+ ],
+];