如何使用 Lumen 开发一个简单的 API Gateway
Routes
create a file called gateway.php in config folder.
<?php return (function () {
$cmsModuleName = env('APP_CMS_MODULE_NAME');
$userModuleName = env('APP_USER_MODULE_NAME');
$routes = [
'cms_get_slider' => [
'method' => 'GET',
'name' => 'Get Slider',
'path' => '/cms/v1/slider',
'dist' => $cmsModuleName . '/v1/slider',
'middleware' => 'auth'
],
'cms_get_pages' => [
'method' => 'GET',
'name' => 'Get Pages',
'path' => '/cms/v1/pages',
'dist' => $cmsModuleName . '/v1/pages',
'middleware' => 'auth'
],
'cms_users_pages' => [
'method' => 'GET',
'name' => 'Get Pages',
'path' => '/cms/v1/pages',
'dist' => $userModuleName . '/v1/users/{id:[0-9]+}/pages/{status}',
'middleware' => 'auth'
],
'acs_get_roles' => [
'method' => 'GET',
'name' => 'acs_get_roles',
'path' => '/acs/v1/roles',
'dist' => $userModuleName . '/acs/v1/roles',
'middleware' => 'role:administrator,super-admin'
],
];
return [
'routes' => $routes,
];
})();
Register gateway to lumen routes.
Edit AppServiceProvider.php in app/Providers/ folder.
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
}
public function boot()
{
$this->registerRoutes();
}
/**
* Register Routes
*
* @throws
* @return void
*/
protected function registerRoutes()
{
$routes = config('gateway');
if ($routes) {
$router = app('router');
$routeArgs = ['uses' => 'App\Http\Controllers\GatewayController@request'];
foreach ($routes['routes'] as $key => $route) {
$method = strtolower($route['method']);
$routeArgs['as'] = $key;
$route['middleware'] ? $routeArgs['middleware'] = [$route['middleware']] : $routeArgs['middleware'] = [];
$router->{$method}($route['path'], $routeArgs);
}
}
}
}
Controller
AppServiceProvider will send all request to GatewayController.
So create GatewayController.php in app/Http/Controllers to handle requests.
namespace App\Http\Controllers;
use GuzzleHttp\Exception\BadResponseException;
use Illuminate\Http\Request;
use GuzzleHttp\Client as HttpClient;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
class GatewayController extends Controller
{
private $request;
private $gateway;
private $method;
private $httpClient;
private $currentRoute;
private $currentUserId = 0;
private $currentUserRoles = [];
public function __construct(Request $request)
{
$this->request = $request;
$this->setCurrentUserId();
$this->setCurrentUserRoles();
$this->gateway = config('gateway');
$this->method = $request->getMethod();
$this->pathInfo = $request->getPathInfo();
$this->httpClient = new HttpClient(['headers' => $this->headers()]);
$this->requestRoute = $this->request->route();
$this->currentRoute = $this->gateway['routes'][$this->getCurrentRouteName()];
}
public function setCurrentUserId()
{
if (isset($this->request->currentUserId)) {
$this->currentUserId = $this->request->currentUserId;
}
}
public function setCurrentUserRoles()
{
if (isset($this->request->roles)) {
$this->currentUserRoles = $this->request->roles;
}
}
public function getCurrentRouteName()
{
$route = $this->requestRoute;
if (isset($route[1]) && $route[1]["as"]) {
return $route[1]["as"];
}
return "";
}
public function getCurrentRequestUserId()
{
$route = $this->requestRoute;
if (isset($route[2]) && isset($route[2]["user_id"])) {
$userId = $route[2]["user_id"];
} else {
$userId = $this->request->get('user_id', $this->request->get('sender_id'));
}
return (int) $userId;
}
public function getRequestUri()
{
$uri = $this->currentRoute['dist'];
if (isset($this->requestRoute[2]) && count($this->requestRoute[2])) {
foreach ($this->requestRoute[2] as $key => $value) {
$uri = str_replace("{" . $key . "}", $value, $uri);
$uri = preg_replace('/{' . $key . ':.+?}/', $value, $uri);
}
}
return $uri;
}
/**
* set guzzle HTTP headers
*
* "headers" => array:10 [
* "Accept" => "application/json"
* "User-Agent" => "PostmanRuntime/7.24.1"
* "X-Forwarded-For" => "172.21.0.1"
* "X-User-Id" => 2
* "X-Roles" => "administrator"
* "X-Is-Admin" => "yes"
* ]
*
* @return void
*/
public function headers()
{
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$clientIps = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$clientIp = explode(':', $clientIps[0])[0];
} else {
$clientIp = $this->request->getClientIp();
}
return [
'Accept' => 'application/json',
'User-Agent' => $this->request->header('User-Agent'),
'X-Forwarded-For' => $clientIp,
'X-User-Id' => $this->currentUserId, // add current user id to guzzle header
'X-Roles' => join(',', $this->currentUserRoles),
'X-Is-Admin' => $this->isAdmin()
];
}
public function isAdmin()
{
return in_array('administrator', $this->currentUserRoles) || in_array('super-admin', $this->currentUserRoles) ? 'yes' : 'no';
}
public function request()
{
switch ($this->method) {
case "POST":
case "PUT":
$requestOption = "json";
break;
case "GET":
$requestOption = "query";
break;
default:
$requestOption = "query";
}
$contentType = $this->request->headers->get('content-type');
// 如果请求里有附件
if (false !== strpos($contentType, 'multipart/form-data') && $this->request->hasFile('file')) {
$file = $this->request->file;
$fileName = $file->getClientOriginalName();
$requestOption = 'multipart';
$requestArgs = [
[
'name' => 'file',
'contents' => file_get_contents($file->getPathname(), 'r'),
'filename' => $fileName
],
[
'name' => 'prefix',
'contents' => $this->request->input('prefix', '')
],
[
'name' => 'disk',
'contents' => $this->request->input('disk', '')
]
];
} else {
$requestArgs = $this->request->all();
}
try {
$response = $this->httpClient->request(
$this->method,
$this->getRequestUri(),
[
$requestOption => $requestArgs
]
);
} catch (BadResponseException $e) {
$response = $e->getResponse();
Log::error('Bad response exception', [
'request' => [
'method' => $this->method,
'uri' => $this->getRequestUri(),
],
'response' => [
'headers' => $response->getHeaders(),
'body' => $response->getBody()->getContents(),
],
]);
return response($response->getBody(), $response->getStatusCode(), $response->getHeaders());
} catch (\Exception $e) {
Log::error($e->getMessage());
$response = response()->json(['code' => $e->getCode(), 'message' => $e->getMessage()], 500);
return $response;
}
return response($response->getBody(), $response->getStatusCode(), $response->getHeaders());
}
}
Middleware
Update Authenticate middleware to handle Auth.
Update Authenticate.php in app/Http/Middleware folder.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Firebase\JWT\JWT;
use Firebase\JWT\ExpiredException;
use Exception;
use Illuminate\Support\Facades\Log;
class Authenticate
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
$token = $request->bearerToken();
$errorCode = null;
$errorMessage = null;
try {
// decode and check Token
$credentials = JWT::decode($token, env('JWT_SECRET'), ['HS256']);
if ($credentials && $credentials->iss == 'you_application_name' && $credentials->data->user->id) {
$request->currentUserId = $credentials->data->user->id;
// 生产TOKEN时,需要将用户角色保存到TOKEN里
$request->roles = $credentials->data->user->roles;
if (env('ENVIRONMENT') === 'local') {
return $next($request);
}
if (!$this->isValidToken($credentials->data->user->id, $token)) {
$errorCode = 'token_abandoned';
$errorMessage = 'The token is abandoned. Please get a new token.';
} else {
return $next($request);
}
}
} catch (ExpiredException $e) {
Log::warning('token_expired');
Log::warning($e->getMessage());
$errorCode = 'token_expired';
$errorMessage = 'Provided token is expired.';
} catch (Exception $e) {
Log::warning('decode_token_failed');
Log::warning($e->getMessage());
$errorCode = 'decode_token_failed';
$errorMessage = 'An error while decoding the token.';
}
// unauthorized
return response()->json(
[
'code' => $errorCode,
'message' => $errorMessage,
'data' => [
'status' => JsonResponse::HTTP_UNAUTHORIZED,
]
],
JsonResponse::HTTP_UNAUTHORIZED
);
}
/**
* 如果需要限制登录数量。
* 可以在登录的时候,将TOKEN更新到REDIS作为可用TOKEN
* 再次请求的时候,确认TOKEN是否是可用的TOKEN
*
* @param integer $userId
* @param string $token
* @return boolean
*/
public function isValidToken(int $userId, string $token): bool
{
$redis = \Illuminate\Support\Facades\Redis::connection('auth');
$userToken = $redis->get('auth:' . $userId . ':token', '');
if (is_array($userToken)) {
return in_array($token, $userToken);
}
return $userToken === $token;
}
}
或者,使用角色认证 role middleware.
Create Role.php in app/Http/Middleware folder.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Firebase\JWT\JWT;
use Firebase\JWT\ExpiredException;
use Exception;
use Illuminate\Support\Facades\Log;
class Role
{
/**
* Role MiddleWare handle function
*
* @param [type] $request
* @param Closure $next
* @param [type] ...$roles
* @return void
*/
public function handle($request, Closure $next, ...$roles)
{
$token = $request->bearerToken();
$roles = is_array($roles) ? $roles : explode(',', $roles);
$errorCode = null;
$errorMessage = null;
try {
// decode and check Token
$credentials = JWT::decode($token, env('JWT_SECRET'), ['HS256']);
if ($credentials && $credentials->iss == 'your_application_name' && $credentials->data->user->roles) {
// check if the middleware roles and user role have at least 1 match
$rolesMatches = count(array_intersect($roles, $credentials->data->user->roles));
if ($rolesMatches) {
$request->currentUserId = $credentials->data->user->id;
$request->roles = $credentials->data->user->roles;
return $next($request);
}
}
// unauthorized
$errorCode = 'not_authorized';
$errorMessage = 'You are not authorized to access this route';
} catch (ExpiredException $e) {
Log::warning('token_expired');
Log::warning($e->getMessage());
$errorCode = 'token_expired';
$errorMessage = 'Provided token is expired.';
} catch (Exception $e) {
Log::warning('decode_token_failed');
Log::warning($e->getMessage());
$errorCode = 'decode_token_failed';
$errorMessage = 'An error while decoding the token.';
}
// unauthorized
return response()->json(
[
'code' => $errorCode,
'message' => $errorMessage,
'data' => [
'status' => JsonResponse::HTTP_UNAUTHORIZED,
]
],
JsonResponse::HTTP_UNAUTHORIZED
);
}
}