当Foreach最初涉足微服务领域时,我们并没有真正构建微服务。 我们以为我们做到了,但是我们所有的服务中总存在一些逻辑。 当然,每个服务实际上应该只专注于自己的任务,而不应该专注于属于另一个微服务的事物。 我们这方面最明显的棘手是认证和授权逻辑。
在某个时候,我们有几个“微”服务,它们根据AuthenticationService
(甚至在较早的日子,甚至是针对共享数据库)验证了传入请求的Authorization标头。 这给我们的AuthenticationService
造成了比我们想要的更多的负载(多次验证同一个令牌),但是这也导致在所有这些服务中都存在一些重要的代码。 而且,正如任何开发人员所知,共享代码铺平了通往地狱的道路。 微服务变得超出其实际用途,这使得它们变得更难开发和维护。
在寻求救赎的过程中,我们Swift找到了一些可以帮助我们的解决方案。
JSON Web令牌
我们考虑的第一件事是开始使用JSON Web令牌(JWT) 。 JWT是一个开放标准,它定义了一种独立的方式来在各方之间安全地传输信息。 自包含意味着令牌本身可以包含我们需要的所有信息,例如用户的标识符或用户名。 安全意味着其他方不可能干扰这些令牌。 令牌包含一个加密部分,要解密它,您需要一个只有您知道的秘密密钥。 换句话说,如果令牌已被篡改,您将知道。
JWT是一个非常有趣的领导者,因为在我们这方面进行最小的调整,从理论上讲,我们甚至可以消除微服务中的一些额外工作量(无论如何它们都不应该这样做)。 令牌的验证是一个最小的过程,可以很好地集成到Spring框架中,因此我们不需要那么多代码。 令牌还将包含我们需要的所有信息,因此我们不再需要从另一个Web服务请求此信息。
但是,JWT的问题在于,已经有其他各方开发的其他一些应用程序与API集成在一起。 事实证明,当我们开始分发JWT令牌时,并不是所有的应用程序都那么满意。 由于短期内无法更改这些应用程序,因此我们暂时保留了这一想法。
API网关
我们的另一个想法是引入API网关。 这可以看作是我们API的包装,意在为最终用户抽象我们的API。 它可能会更改对另一种格式的响应。 它可以将多个HTTP请求合并为一个请求。 或者它可以提供其他监视功能(例如“谁向某个端点发送垃圾邮件?”)。 但最重要的是,它应该抽象与身份验证有关的所有内容。
在我们的例子中,想法是API网关甚至在请求被代理到我们的应用程序之前都会验证传入的Authorization标头。 它应该缓存结果,以便如果同一用户请求五个端点,我们仍然每小时仅验证一次令牌,并且应该将身份验证信息传递给我们的API,以便我们知道谁在请求资源。
我们的解决方案:AWS API Gateway
市场上有许多符合此描述的产品,但经过一番考虑,我们决定尝试一下AWS API Gateway。 我们实施了自定义的“授权人”。 这是一个Lambda函数,它接收客户端提供的授权令牌作为输入,并返回客户端是否有权访问所请求的资源。 如果身份验证被拒绝,API网关将向客户端返回403 HTTP代码。 否则,该请求将被代理到我们的服务中。 授权者Lambda的结果在缓存中保留了一个小时。 我们还希望使用HTTP标头将用户的身份传递给我们的基础服务。 这样,我们知道谁在我们的应用程序中执行请求。
授权者
我们的自定义Lambda函数是用Python编写的。 它从传入的请求中获取Authorization标头,并向我们的AuthenticationService
启动HTTP请求-这是我们唯一可以验证传入的信息是否有效以及令牌适用于谁的地方。 这个HTTP请求将告诉我们最终用户是谁。
Lambda函数的代码(主要基于AWS提供的示例代码)如下所示:
from __future__ import print_function
import re
import urllib2
import base64
import json
import os
def lambda_handler(event, context):
print("Client token (provided): " + event['authorizationToken'])
clientAuthorizationToken = re.sub('^%s' % 'Bearer', '', re.sub('^%s' % 'bearer', '', event['authorizationToken'])).strip()
print("Client token (parsed): " + clientAuthorizationToken)
print("Method ARN: " + event['methodArn'])
url = os.environ['CHECK_TOKEN_ENDPOINT'] + "?token=" + clientAuthorizationToken
print("Check token URL: " + url)
authorizationHeader = 'Basic %s' % base64.b64encode(os.environ['CHECK_TOKEN_ENDPOINT_CLIENT_ID'] + ':' + os.environ['CHECK_TOKEN_ENDPOINT_CLIENT_SECRET'])
print("Our authorization header: " + authorizationHeader)
tmp = event['methodArn'].split(':')
apiGatewayArnTmp = tmp[5].split('/')
awsAccountId = tmp[4]
policy = AuthPolicy('urn:user:unknown', awsAccountId)
policy.restApiId = apiGatewayArnTmp[0]
policy.region = tmp[3]
policy.stage = apiGatewayArnTmp[1]
request = urllib2.Request(url, headers={"Authorization": authorizationHeader})
try:
result = urllib2.urlopen(request)
data = json.load(result)
print("HTTP Response data: " + str(data))
context = {
'userUrn': data['user_urn'] if data.has_key('user_urn') else None,
'clientId': data['client_id']
}
policy.principalId = data['user_urn'] if data.has_key('user_urn') else 'urn:client:%s' % data['client_id']
policy.allowMethod('*', '*')
print('Allowing resource %s. Client: %s, User: %s, Principal: %s' % (policy.allowMethods[0]['resourceArn'], context['clientId'], context['userUrn'], policy.principalId))
except urllib2.HTTPError, e:
print("Error during the HTTP call: %s" % e)
policy.denyAllMethods()
context = {}
authResponse = policy.build()
authResponse['context'] = context
return authResponse
class HttpVerb:
GET = 'GET'
POST = 'POST'
PUT = 'PUT'
PATCH = 'PATCH'
HEAD = 'HEAD'
DELETE = 'DELETE'
OPTIONS = 'OPTIONS'
ALL = '*'
class AuthPolicy(object):
awsAccountId = ''
principalId = ''
version = '2012-10-17'
pathRegex = '^[/.a-zA-Z0-9-\*]+$'
allowMethods = []
denyMethods = []
restApiId = '*'
region = '*'
stage = '*'
def __init__(self, principal, awsAccountId):
self.awsAccountId = awsAccountId
self.principalId = principal
self.allowMethods = []
self.denyMethods = []
def _addMethod(self, effect, verb, resource, conditions):
if verb != '*' and not hasattr(HttpVerb, verb):
raise NameError('Invalid HTTP verb ' + verb + '. Allowed verbs in HttpVerb class')
resourcePattern = re.compile(self.pathRegex)
if not resourcePattern.match(resource):
raise NameError('Invalid resource path: ' + resource + '. Path should match ' + self.pathRegex)
if resource[:1] == '/':
resource = resource[1:]
resourceArn = 'arn:aws:execute-api:{}:{}:{}/{}/{}/{}'.format(self.region, self.awsAccountId, self.restApiId, self.stage, verb, resource)
if effect.lower() == 'allow':
self.allowMethods.append({
'resourceArn': resourceArn,
'conditions': conditions
})
elif effect.lower() == 'deny':
self.denyMethods.append({
'resourceArn': resourceArn,
'conditions': conditions
})
def _getEmptyStatement(self, effect):
statement = {
'Action': 'execute-api:Invoke',
'Effect': effect[:1].upper() + effect[1:].lower(),
'Resource': []
}
return statement
def _getStatementForEffect(self, effect, methods):
statements = []
if len(methods) > 0:
statement = self._getEmptyStatement(effect)
for curMethod in methods:
if curMethod['conditions'] is None or len(curMethod['conditions']) == 0:
statement['Resource'].append(curMethod['resourceArn'])
else:
conditionalStatement = self._getEmptyStatement(effect)
conditionalStatement['Resource'].append(curMethod['resourceArn'])
conditionalStatement['Condition'] = curMethod['conditions']
statements.append(conditionalStatement)
if statement['Resource']:
statements.append(statement)
return statements
def allowAllMethods(self):
self._addMethod('Allow', HttpVerb.ALL, '*', [])
def denyAllMethods(self):
self._addMethod('Deny', HttpVerb.ALL, '*', [])
def allowMethod(self, verb, resource):
self._addMethod('Allow', verb, resource, [])
def denyMethod(self, verb, resource):
self._addMethod('Deny', verb, resource, [])
def allowMethodWithConditions(self, verb, resource, conditions):
self._addMethod('Allow', verb, resource, conditions)
def denyMethodWithConditions(self, verb, resource, conditions):
self._addMethod('Deny', verb, resource, conditions)
def build(self):
if ((self.allowMethods is None or len(self.allowMethods) == 0) and
(self.denyMethods is None or len(self.denyMethods) == 0)):
raise NameError('No statements defined for the policy')
policy = {
'principalId': self.principalId,
'policyDocument': {
'Version': self.version,
'Statement': []
}
}
policy['policyDocument']['Statement'].extend(self._getStatementForEffect('Allow', self.allowMethods))
policy['policyDocument']['Statement'].extend(self._getStatementForEffect('Deny', self.denyMethods))
return policy
网关配置
创建Lambda函数之后,该配置网关了。 您可以在AWS控制台中或使用CloudFormation模板执行此操作。 我们不会详细解释如何配置API网关,因为这是AWS站点上记录良好的任务 。 但是,我将解释一些配置授权者的细节。
授权人
在“ API网关配置”部分中的左侧,您会看到“授权者”选项。 您可以在那里选择创建新的授权者。 当您单击按钮时,您将看到以下表格:
重要事项:
- Lambda函数:选择之前创建的授权者Lambda
- Lamba事件有效负载:令牌
- 令牌来源:授权(如果您的客户端使用“授权”标头发送令牌)
- 授权缓存:已启用
资源资源
接下来,我们转到您要保护的方法。 单击左侧的资源,然后在列表中选择一种方法。 您应该看到类似于以下屏幕的屏幕:
点击“方法请求”。 然后,您可以在顶部配置为使用之前添加的授权者。
返回上一个屏幕,然后单击“集成请求”。 在底部,我们将配置一些要发送到API的标头。 这些包含有关用户的信息,我们将在API中使用这些信息来了解谁在发出请求。 注意:我们不必担心恶意用户在请求中发送这些标头。 我们的自定义授权者的结果将覆盖它们。
未来
虽然我们当前的实施在生产中运行良好,但我们始终在寻找有关如何改进产品以及由此向客户提供服务的想法。 我们将继续关注的事情之一是,有一天开始使用JWT令牌,这很可能与API Gateway结合使用。 这将使设置更加容易,但是将需要对某些应用程序进行更改,而这是我们目前无法做到的。
此外,我们确实对如何从API网关中获取更多信息有一些想法。 我们对每个应用程序和每个用户的速率限制非常感兴趣。 我们希望能够以这种方式配置移动应用程序,例如,仅允许每小时执行一百个请求,或者仅允许某个最终用户少量请求。
将API Gateway与AWS Lambda结合使用是一种相对简单的方法,可以向您的应用程序添加可靠的身份验证方法,而不会中断其他服务。
翻译自: https://www.javacodegeeks.com/2018/11/api-gateway-aws-lambda-authentication.html