Django中的Salesforce Oauth
我现在是个进阶初学者,最近几天一直被这个问题困扰。我要创建的应用需要访问每个用户的Salesforce账户。为了建立API连接,我们需要保存他们的Salesforce用户名、密码和访问令牌。
用户会输入他们的Salesforce用户名和密码,然后点击一个按钮来授权。这个按钮会把用户引导到:
Salesforce Oauth服务器的响应应该是:
https://www.mysite.com/oauth/salesforce/?display=popup&code=aPrx6RMKPrMpm.SHNCxBmHz3ZiHMc.xZRY9RHLekkzW_G8Uu.KyqmmY0.JGCr5roqPT49vTCbg%3D%3D
这是views.py文件
from django.views.generic.base import RedirectView, TemplateView, View
from django.http import Http404, HttpResponse
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy, reverse
from guardian.mixins import LoginRequiredMixin
from simple_salesforce import Salesforce
import logging
from campaigns.views import CampaignOwnerPermission
from . import api, utils, settings
from .models import OauthToken
class SalesforceOauthRedirectView(
LoginRequiredMixin,
RedirectView
):
def get_redirect_url(self):
logger = logging.getLogger(__name__)
# Extract 'code' parameter from return URL and set to variable
if (
not self.request.GET.get(
'code', None
) is None
):
try:
existing_credentials = OauthToken.objects.get(
user=request.user
)
# get stored Salesforce username and password
username = str(existing_credentials.salesforce_user_id)
password = str(existing_credentials.password)
payload = {
'grant_type': 'password',
'client_id': str(settings.CONSUMER_KEY),
'client_secret': str(settings.CONSUMER_SECRET),
'username': username,
# must concatenate password & code before passing to server
'password': password + str(code)
}
try:
# Post payload to Salesforce Oauth server and get user
# token in response.
r = requests.post(
"https://login.salesforce.com/services/oauth2/token",
headers={
"Content-Type":"application/x-www-form-urlencoded"
},
data=payload
)
# Decode the JSON response from Salesforce Oauth server
decoded = json.loads(r.content)
# Store access_token to database
existing_credentials.token = decoded['access_token']
existing_credentials.active = True
existing_credentials.save()
messages.add_message(
self.request,
messages.SUCCESS,
_(
'Successfully updated Salesforce \
authentication with user credentials: "%s"'
%
salesforce_user_id
)
)
# Success point
return reverse_lazy('providers:provider_list')
# except (ValueError, KeyError, TypeError):
# logger.error('Could not decode response from Salesforce API')
# Not sure how this should be arranged
except:
logger.error(
'Could not get Oauth_token from Salesforce API.'
)
messages.add_message(
self.request,
messages.WARNING,
('Could not get Oauth_token from Salesforce API.\n\n \
Salesforce may be experiencing an outage. Try again \
in a few minutes and contact explorro support if the \
problem persists.'
)
)
return reverse_lazy('providers:provider_list')
except:
logger.error('Could not get users Salesforce credentials')
messages.add_message(
self.request,
messages.WARNING,
('There was a problem authenticating with \
Salesforce. Be sure to enter your Salesforce \
username and password before attempting to authorize your\
account. Contact our support team if you need some help.'
)
)
return reverse_lazy('providers:provider_list')
else:
pass
return reverse_lazy('providers:provider_list')
messages.add_message(
self.request,
messages.WARNING,
('Could not retrieve Salesforce Authorization Code\n\n \
Contact your Salesforce administrator for assistance.'
)
)
从响应的URL中提取的'code'参数(示例值 = randomTextCode),会作为'payload'变量的一部分(第38行)传递给https://login.salesforce.com/services/oauth2/token
最后一步应该是从https://login.salesforce.com/services/oauth2/token接收到一个包含access_token的JSON响应
错误似乎出现在第一次尝试/异常处理的语句中,因为我在第#103-106行收到了异常错误信息。
在本地运行时,服务器的响应是(301)
"GET /oauth/salesforce/?display=popup&code=aPrx6RMKPrMpm.SHNCxBmHz3ZgWIqBWQKmln4Q6TfdI8TbmeWuMw5H..Di.342no15VYNvmgzA%3D%3D HTTP/1.1" 301 0
这是models.py
from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
class OauthToken(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name='Salesforce User',
verbose_name=_('Salesforce User'),
db_index=True,
)
salesforce_user_id = models.EmailField(
verbose_name=_('Salesforce user id'),
db_index=True, unique=True
)
password = models.CharField( # TODO: This needs to be encrypted!!
verbose_name=_('Salesforce password'),
max_length=256,
)
token = models.CharField(
verbose_name=_('Token'),
max_length=256,
null=True,
blank=True,
)
security_token = models.CharField(
verbose_name=_('Security Token'),
max_length=256
)
active = models.BooleanField(
verbose_name=_('Active'),
default=False,
db_index=True
)
def __unicode__(self):
return str(self.salesforce_user_id)
class Meta:
verbose_name = _('Oauth token')
verbose_name_plural = _('Oauth tokens')
任何帮助我排查这个问题的建议都非常感谢。
2 个回答
看起来问题可能出在你这边的语法错误或者数据错误,特别是在握手的过程中。产生错误信息的那段代码其实是在 requests.post 发送到 Salesforce 之外的,也就是说,错误是由以下这些代码行中的某一行引起的:
existing_credentials = OauthToken.objects.get(
user=request.user
)
# get stored Salesforce username and password
username = str(existing_credentials.salesforce_user_id)
password = str(existing_credentials.password)
payload = {
'grant_type': 'password',
'client_id': str(settings.CONSUMER_KEY),
'client_secret': str(settings.CONSUMER_SECRET),
'username': username,
# must concatenate password & code before passing to server
'password': password + str(code)
}
这里可能有一个更有用的错误信息被这个宽泛的 try/except 块给隐藏了,这个信息可以帮助你解决问题。我建议你让这个异常被抛出来,看看会发现什么(比如,可能是 ORM 找不到你要查找的 OauthToken)。你可以直接修改 except 块:
# Line 96
except BaseException as e:
raise e # To print to the console
# or, to print to the error logs
logger.error("%s: %s" % (e.__class__, e.message))
messages.add_message(
self.request,
messages.WARNING,
('There was a problem authenticating with \
Salesforce. Be sure to enter your Salesforce \
username and password before attempting to authorize your\
account. Contact our support team if you need some help.'
)
)
return reverse_lazy('providers:provider_list')
这样做应该能帮助你找到实际抛出的异常,以便你进一步调试。
我之前做了一些错误的事情。决定在这里写下我的答案,等我有了更多改进再更新。特别感谢Casey Kinsey给我的建议,提到用BaseException来帮助排查问题。
首先,我使用了错误的grant_type参数。我一直用的是'password'这个参数,但正确的应该是'authorization_code'。
其次,我的测试过程设计得太复杂了。我在更新代码后,把它部署到一个临时的Heroku环境中,然后再进行排查。为了加快排查速度,我修改了几个地方的redirect_uri:第一,用户点击授权他们账户的链接(这个在另一个文件里),第二,发送给Salesforce的payload变量,第三,Salesforce连接应用里的redirect_uri。
from django.views.generic.base import RedirectView, TemplateView, View
from django.http import Http404, HttpResponse
from django.conf import settings
from django.conf.urls import patterns, url, include
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy, reverse
from guardian.mixins import LoginRequiredMixin
from simple_salesforce import Salesforce
import logging, requests, json
from campaigns.views import CampaignOwnerPermission
from . import api, utils, settings
from .models import OauthToken
class SalesforceOauthRedirectView(
LoginRequiredMixin,
RedirectView
):
# permanent = False
# query_string = False
def get_redirect_url(self):
logger = logging.getLogger(__name__)
try:
payload = {
'grant_type': 'authorization_code',
'client_id': settings.CONSUMER_KEY,
'client_secret': settings.CONSUMER_SECRET,
'code': self.request.GET.get('code'), # get code param from response URL
# TODO: redirect_uri should NOT be hardcoded
'redirect_uri': 'https://127.0.0.1:8000/oauth/salesforce/'
}
try:
# Post payload to Salesforce Oauth server and get user
# token in response.
r = requests.post(
"https://login.salesforce.com/services/oauth2/token",
headers={
"Content-Type":"application/x-www-form-urlencoded"
},
data=payload
)
try:
# Decode the JSON response from Salesforce Oauth server
decoded = json.loads(r.content)
# Store tokens & Salesforce user info to database
creds = OauthToken.objects.get(user=self.request.user)
# TODO: Store salesforce_user_id, Thumbnail, etc.
creds.access_token = decoded['access_token']
# creds.salesforce_organization_id = decoded['refresh_token']
# creds.refresh_token = creds['refresh_token']
# creds.id_token = creds['id_token']
# creds.instance_url = decoded['instance_url']
creds.active = True
creds.save()
messages.add_message(
self.request,
messages.SUCCESS,
_(
'Successfully updated Salesforce \
authentication with user credentials: "%s"'
%
creds.salesforce_user_id
)
)
except:
logger.error("%s: %s" % (e.__class__, e.args))
messages.add_message(
self.request,
messages.WARNING,
('Error connecting with Salesforce. \
Contact explorro support. [Error 003]')
)
return reverse_lazy('providers:provider_list')
except BaseException as e:
#raise e # Print error to the console
# or, to print to the error logs
logger.error("%s: %s" % (e.__class__, e.args))
messages.add_message(
self.request,
messages.WARNING,
('Could not get Oauth_token from Salesforce API.\n\n \
Salesforce may be experiencing an outage. Try again \
in a few minutes and contact explorro support if the \
problem persists. [Error 002]'
)
)
return reverse_lazy('providers:provider_list')
except BaseException as e:
raise e # Print error to console
logger.error("%s: %s" % (e.__class__, e.args))
messages.add_message(
self.request,
messages.WARNING,
('There was a problem authenticating with \
Salesforce. Be sure to enter your Salesforce \
username and password before attempting to authorize your\
account. Contact our support team if you need some help. \
[Error 003]'
)
)
return reverse_lazy('providers:provider_list')
这个解决方案还不是100%完整,接下来还有一些工作要做:
(1) 还有其他参数需要存储到数据库里,密码和所有的令牌也需要在数据库中加密(这是我接下来要做的事情)。
(2) 当这个功能推向生产环境时,redirect_uri需要在各个地方更新(在Salesforce应用里、用户授权链接里和payload变量里)。
(3) 我们需要使用可用的Salesforce ID参数(包括Salesforce用户名、密码、头像等),并把这些存储到数据库中,这样才能实现真正的一键认证。