Django中的Salesforce Oauth

0 投票
2 回答
1737 浏览
提问于 2025-04-18 15:47

我现在是个进阶初学者,最近几天一直被这个问题困扰。我要创建的应用需要访问每个用户的Salesforce账户。为了建立API连接,我们需要保存他们的Salesforce用户名、密码和访问令牌。

用户会输入他们的Salesforce用户名和密码,然后点击一个按钮来授权。这个按钮会把用户引导到:

https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=dsf3434&redirect_uri=https%3A%2F%2Fwww.mysite.com/oauth/salesforce/&display=popup

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 个回答

0

看起来问题可能出在你这边的语法错误或者数据错误,特别是在握手的过程中。产生错误信息的那段代码其实是在 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')

这样做应该能帮助你找到实际抛出的异常,以便你进一步调试。

1

我之前做了一些错误的事情。决定在这里写下我的答案,等我有了更多改进再更新。特别感谢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用户名、密码、头像等),并把这些存储到数据库中,这样才能实现真正的一键认证。

撰写回答