我们对App Engine对Google Cloud Endpoints的支持感到非常兴奋。
那说我们还没有使用OAuth2,并且通常使用用户名/密码验证用户 所以我们可以支持没有Google帐户的客户。
我们希望将API迁移到Google Cloud Endpoints,因为我们可以免费获得所有好处(API控制台,客户端库,健壮性......),但我们的主要问题是......
如何向我们之前在现有API中检查有效用户会话+ CSRF令牌的云端点添加自定义身份验证。
有没有一种优雅的方法来实现这一点,而无需在protoRPC消息中添加会话信息和CSRF令牌等内容?
答案 0 :(得分:16)
我正在为整个应用程序使用webapp2身份验证系统。所以我尝试将其重复用于Google Cloud身份验证,我就明白了!
webapp2_extras.auth使用webapp2_extras.sessions存储身份验证信息。此会话可以以3种不同的格式存储:securecookie,datastore或memcache。
Securecookie是我正在使用的默认格式。我认为它足够安全,因为webapp2 auth系统用于生产环境中运行的大量GAE应用程序。
所以我解码这个securecookie并从GAE端点重用它。我不知道这是否会产生一些安全问题(我希望不会)但也许@bossylobster可以说是否可以看到安全方面。
我的Api:
import Cookie
import logging
import endpoints
import os
from google.appengine.ext import ndb
from protorpc import remote
import time
from webapp2_extras.sessions import SessionDict
from web.frankcrm_api_messages import IdContactMsg, FullContactMsg, ContactList, SimpleResponseMsg
from web.models import Contact, User
from webapp2_extras import sessions, securecookie, auth
import config
__author__ = 'Douglas S. Correa'
TOKEN_CONFIG = {
'token_max_age': 86400 * 7 * 3,
'token_new_age': 86400,
'token_cache_age': 3600,
}
SESSION_ATTRIBUTES = ['user_id', 'remember',
'token', 'token_ts', 'cache_ts']
SESSION_SECRET_KEY = '9C3155EFEEB9D9A66A22EDC16AEDA'
@endpoints.api(name='frank', version='v1',
description='FrankCRM API')
class FrankApi(remote.Service):
user = None
token = None
@classmethod
def get_user_from_cookie(cls):
serializer = securecookie.SecureCookieSerializer(SESSION_SECRET_KEY)
cookie_string = os.environ.get('HTTP_COOKIE')
cookie = Cookie.SimpleCookie()
cookie.load(cookie_string)
session = cookie['session'].value
session_name = cookie['session_name'].value
session_name_data = serializer.deserialize('session_name', session_name)
session_dict = SessionDict(cls, data=session_name_data, new=False)
if session_dict:
session_final = dict(zip(SESSION_ATTRIBUTES, session_dict.get('_user')))
_user, _token = cls.validate_token(session_final.get('user_id'), session_final.get('token'),
token_ts=session_final.get('token_ts'))
cls.user = _user
cls.token = _token
@classmethod
def user_to_dict(cls, user):
"""Returns a dictionary based on a user object.
Extra attributes to be retrieved must be set in this module's
configuration.
:param user:
User object: an instance the custom user model.
:returns:
A dictionary with user data.
"""
if not user:
return None
user_dict = dict((a, getattr(user, a)) for a in [])
user_dict['user_id'] = user.get_id()
return user_dict
@classmethod
def get_user_by_auth_token(cls, user_id, token):
"""Returns a user dict based on user_id and auth token.
:param user_id:
User id.
:param token:
Authentication token.
:returns:
A tuple ``(user_dict, token_timestamp)``. Both values can be None.
The token timestamp will be None if the user is invalid or it
is valid but the token requires renewal.
"""
user, ts = User.get_by_auth_token(user_id, token)
return cls.user_to_dict(user), ts
@classmethod
def validate_token(cls, user_id, token, token_ts=None):
"""Validates a token.
Tokens are random strings used to authenticate temporarily. They are
used to validate sessions or service requests.
:param user_id:
User id.
:param token:
Token to be checked.
:param token_ts:
Optional token timestamp used to pre-validate the token age.
:returns:
A tuple ``(user_dict, token)``.
"""
now = int(time.time())
delete = token_ts and ((now - token_ts) > TOKEN_CONFIG['token_max_age'])
create = False
if not delete:
# Try to fetch the user.
user, ts = cls.get_user_by_auth_token(user_id, token)
if user:
# Now validate the real timestamp.
delete = (now - ts) > TOKEN_CONFIG['token_max_age']
create = (now - ts) > TOKEN_CONFIG['token_new_age']
if delete or create or not user:
if delete or create:
# Delete token from db.
User.delete_auth_token(user_id, token)
if delete:
user = None
token = None
return user, token
@endpoints.method(IdContactMsg, ContactList,
path='contact/list', http_method='GET',
name='contact.list')
def list_contacts(self, request):
self.get_user_from_cookie()
if not self.user:
raise endpoints.UnauthorizedException('Invalid token.')
model_list = Contact.query().fetch(20)
contact_list = []
for contact in model_list:
contact_list.append(contact.to_full_contact_message())
return ContactList(contact_list=contact_list)
@endpoints.method(FullContactMsg, IdContactMsg,
path='contact/add', http_method='POST',
name='contact.add')
def add_contact(self, request):
self.get_user_from_cookie()
if not self.user:
raise endpoints.UnauthorizedException('Invalid token.')
new_contact = Contact.put_from_message(request)
logging.info(new_contact.key.id())
return IdContactMsg(id=new_contact.key.id())
@endpoints.method(FullContactMsg, IdContactMsg,
path='contact/update', http_method='POST',
name='contact.update')
def update_contact(self, request):
self.get_user_from_cookie()
if not self.user:
raise endpoints.UnauthorizedException('Invalid token.')
new_contact = Contact.put_from_message(request)
logging.info(new_contact.key.id())
return IdContactMsg(id=new_contact.key.id())
@endpoints.method(IdContactMsg, SimpleResponseMsg,
path='contact/delete', http_method='POST',
name='contact.delete')
def delete_contact(self, request):
self.get_user_from_cookie()
if not self.user:
raise endpoints.UnauthorizedException('Invalid token.')
if request.id:
contact_to_delete_key = ndb.Key(Contact, request.id)
if contact_to_delete_key.get():
contact_to_delete_key.delete()
return SimpleResponseMsg(success=True)
return SimpleResponseMsg(success=False)
APPLICATION = endpoints.api_server([FrankApi],
restricted=False)
答案 1 :(得分:1)
根据我的理解,Google Cloud Endpoints提供了一种实现(RESTful?)API并生成移动客户端库的方法。在这种情况下,身份验证将是OAuth2。 OAuth2提供不同的“流量”,其中一些支持移动客户端。 在使用委托人和凭证(用户名和密码)进行身份验证的情况下,这似乎不太适合。老实说,我认为使用OAuth2会更好。 实现自定义OAuth2流程以支持您的案例是一种可行的方法,但非常容易出错。 我还没有使用OAuth2,但也许可以为用户创建“API密钥”,这样他们就可以通过使用移动客户端来使用前端和后端。
答案 2 :(得分:1)
我编写了一个名为Authtopus的自定义python身份验证库,对于寻找此问题解决方案的任何人都可能感兴趣:https://github.com/rggibson/Authtopus
Authtopus支持基本的用户名和密码注册和登录,以及通过Facebook或Google进行社交登录(更多的社交提供商可能会被添加而不会有太多麻烦)。根据经过验证的电子邮件地址合并用户帐户,因此如果用户首先使用用户名和密码进行注册,然后使用社交登录,并且经过验证的帐户电子邮件地址匹配,则不会创建单独的用户帐户。
答案 3 :(得分:0)
您可以使用SetMemoryStream()进行身份验证。解决方案jwt
答案 4 :(得分:0)
我还没编码,但下一步想象的是:
当服务器收到登录请求时,它会在数据存储区中查找用户名/密码。如果用户未找到服务器响应某些错误对象,其中包含“用户不存在”等相应的消息。如果发现它存储在FIFO类型的集合(缓存)中,其大小有限,如100(或1000或10000)。
成功登录请求服务器返回客户端sessionid,如“; LKJLK345345LKJLKJSDF53KL”。可以是Base64编码的用户名:密码。 客户端将其存储在名为“authString”或“sessionid”(或不那么雄辩的东西)的Cookie中,并且有30分钟(任意)到期。
登录后,每次请求客户端都会发送从cookie获取的Autorization标头。每次使用cookie时,它都会更新 - 因此在用户激活时它永远不会过期。
在服务器端,我们将使用AuthFilter来检查每个请求中是否存在授权标头(不包括login,signup,reset_password)。如果找不到这样的标题,过滤器将返回对状态码为401的客户端的响应(客户端向用户显示登录屏幕)。如果找到标题过滤器首先检查缓存中是否存在用户,在数据存储区中后如果找到用户 - 什么都不做(请求通过适当的方法处理),找不到 - 401.
以上架构允许保持服务器无状态但仍然具有自动断开连接的会话。