在服务代理中时不能使用 msdb.dbo.sp_send_dbmail - 作为来宾执行?

Nat*_*han 5 sql-server permissions sql-server-2008-r2 service-broker signature

我有一个TheNotificationProcedure对 msdb.dbo.sp_send_dbmail 进行跨数据库调用的过程。

它从服务代理队列中(间接地)调用:

CREATE QUEUE [Blah].[TheQueue]
WITH ACTIVATION (STATUS = ON, PROCEDURE_NAME = [Blah].[TheQueueProcedure], 
MAX_QUEUE_READERS = 1, EXECUTE AS N'TheUser');
Run Code Online (Sandbox Code Playgroud)

TheQueueProcedure 最终打电话 TheNotificationProcedure

如果我在 SSMS 中连接TheUser并执行TheNotificationProcedure,则一切正常,电子邮件会发送出去。

但是,当TheNotificationProcedure由于消息到达队列而被调用时,由于无法访问 msdb 过程而失败。

我已经尝试了所有我能想到的方法,包括在 msdb 中创建我自己的程序来包装 sp_send_dbmail 并签署我的 dbmail 包装器和TheNotificationProcedure相同的证书,并确保 msdb 中的证书用户是“DatabaseMailUserRole”的成员。

最后,在做了许多更详细的跟踪之后,我最终注意到了以下几点:

服务代理跟踪

也就是说,即使服务经纪人下执行登录TheUser,由于某种原因,它是数据库下执行用户guest,我怀疑至少部分地解释了我的权限问题。

登录 TheUser也映射到一个用户在MSDB叫TheUser-它肯定是不映射到来宾。

那么为什么它在通过服务代理时在 msdb 中作为来宾执行呢?

我需要避免将数据库标记为Trustworthy. 我希望通过签署程序(例如http://www.sommarskog.se/grantperm.html)我可以获得跨数据库传输的权限 - 是否execute as否定了通常通过证书用户关联的任何权限?

这是一个脚本,可以在通过服务代理时在没有任何模块签名(提供相同的“访客”跟踪)的情况下复制上述权限问题:

设置:

--REPLACE EMAIL, and db_mail profile
--@profile_name = 'Test db mail profile',
--@recipients = 'test@test.test',

use master;
GO

IF EXISTS(select * FROM sys.databases where name='http://dba.stackexchange.com/questions/166033')
BEGIN
    ALTER DATABASE [http://dba.stackexchange.com/questions/166033] SET OFFLINE WITH ROLLBACK IMMEDIATE;
    ALTER DATABASE [http://dba.stackexchange.com/questions/166033] SET ONLINE WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [http://dba.stackexchange.com/questions/166033];
END

CREATE DATABASE [http://dba.stackexchange.com/questions/166033];
GO

IF EXISTS(select * FROM sys.server_principals WHERE name = 'TheUser' AND type_desc='SQL_LOGIN') DROP LOGIN TheUser;

CREATE LOGIN [TheUser] WITH PASSWORD=N'jL839lIFKttcm3cNuk1WUazfk5lS76RKMscZ01UdFkI='
    , DEFAULT_DATABASE=[http://dba.stackexchange.com/questions/166033]
    , DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF;

use [msdb];
GO

IF (NOT EXISTS(select * FROM sys.database_principals WHERE name = 'TheUser')) CREATE USER [TheUser] FOR LOGIN [TheUser] WITH DEFAULT_SCHEMA=[dbo];

exec sp_addrolemember 'DatabaseMailUserRole', 'TheUser';
GO
use [http://dba.stackexchange.com/questions/166033];
GO

CREATE USER [TheUser] FOR LOGIN [TheUser] WITH DEFAULT_SCHEMA=[dbo]
GO

CREATE SCHEMA [Blah] AUTHORIZATION dbo;
GO

CREATE QUEUE [Blah].[SourceQueue];
GO

CREATE SERVICE [//FromService]
    AUTHORIZATION [dbo]
    ON QUEUE [Blah].[SourceQueue];
GO

CREATE MESSAGE TYPE [//TestMessage]
    AUTHORIZATION [dbo]
    VALIDATION = NONE;
GO

CREATE CONTRACT [//ServiceContract]
    AUTHORIZATION [dbo]
    ([//TestMessage] SENT BY INITIATOR);
GO

CREATE PROCEDURE [Blah].[SendMessage]
AS
DECLARE @message varchar(50),
        @conversationHandle UNIQUEIDENTIFIER

    SET    @message = 'Test Message Content';

    -- Begin the dialog.
    BEGIN DIALOG CONVERSATION @conversationHandle
        FROM SERVICE [//FromService]
        TO SERVICE '//ToService'
        ON CONTRACT [//ServiceContract] 
        WITH ENCRYPTION = OFF;

    -- Send the message on the dialog.
    SEND ON CONVERSATION @conversationHandle
      MESSAGE TYPE [//TestMessage]
      (@message) ;

    END CONVERSATION @conversationHandle ;
GO


CREATE  PROCEDURE [dbo].[TheNotificationProcedure]
AS
    PRINT 'DEBUG - Entering [dbo].[TheNotificationProcedure]'

    -- Send notification
    PRINT 'DEBUG - [dbo].[TheNotificationProcedure] - PRIOR TO msdb.dbo.sp_send_dbmail'

    declare @log nvarchar(max) = ''; 
    select @log = @log + 'name: ' + name + ' ' + 'type: ' + type + ' usage: ' + usage + ' || ' FROM sys.login_token 
    print @log

    declare @mailitem_id int;

    --exec [msdb].[dbo].[WRAP__sp_send_dbmail]
    exec [msdb].[dbo].[sp_send_dbmail]
                @profile_name = 'Test db mail profile',
                @recipients = 'test@test.test', --@Recipient,
                @subject = 'Testing sp_send_dbmail', --@NotificationSubject,
                @body = 'Testing sp_sdend_dbmail from service broker', --@NotificationBody,
                @exclude_query_output = 1,
                @mailitem_id = @mailitem_id OUTPUT

    PRINT 'DEBUG - [dbo].[TheNotificationProcedure] - AFTER msdb.dbo.sp_send_dbmail'

GO

CREATE PROCEDURE [Blah].[TestMessageHandler]
AS
    --has other logic that eventully calls notification
    EXECUTE [dbo].[TheNotificationProcedure]
GO

CREATE PROCEDURE [Blah].[TheQueueProcedure]
AS
--Service Broker variables
DECLARE @conversation_handle UNIQUEIDENTIFIER,
        @conversation_group_id  UNIQUEIDENTIFIER,
        @message_body varchar(255),
        @message_type_name NVARCHAR(256),
        @dialog UNIQUEIDENTIFIER,
        @RowsReceived    int

PRINT 'Start'
    WHILE (1 = 1)
    BEGIN

        -- Get next conversation group.

        WAITFOR(
           GET CONVERSATION GROUP @conversation_group_id FROM [Blah].[TheQueue]),
           TIMEOUT 500 ;

        -- If there are no more conversation groups, roll back the
        -- transaction and break out of the outermost WHILE loop.

        IF @conversation_group_id IS NULL
        BEGIN
            BREAK ;
        END ;

        WHILE (1 = 1)
        BEGIN
            BEGIN TRANSACTION
            PRINT 'Get Message'
            ;        RECEIVE TOP (1) 
                            @dialog = conversation_handle,
                            @message_type_name=message_type_name,
                            @message_body=message_body
                    FROM    [Blah].[TheQueue]
                    WHERE conversation_group_id = @conversation_group_id ;

            SET    @RowsReceived = @@ROWCOUNT
            PRINT 'Queue Read: ' + ISNULL(@message_body, '<NULL>')
            PRINT '@RowsReceived: ' + CAST(@RowsReceived as varchar(200))
            IF (@RowsReceived = 0)
                BEGIN
                    BREAK ;
                END ;

            PRINT 'Deal with Message'

            IF (@message_type_name = 'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog')
            BEGIN
                PRINT 'End Dialog received for dialog # ' + cast(@dialog as nvarchar(40)) ;
                END CONVERSATION @dialog ;
            END ;

            IF (@message_type_name = '//TestMessage')
            BEGIN
                print 'Have //TestMessage: ' + @message_body

                exec [Blah].[TestMessageHandler];

            END

            COMMIT TRANSACTION;
        END

    END
    RETURN

GO





CREATE QUEUE [Blah].[TheQueue]
    WITH ACTIVATION (STATUS = ON, PROCEDURE_NAME = [Blah].[TheQueueProcedure], MAX_QUEUE_READERS = 1, EXECUTE AS N'TheUser');
GO

CREATE SERVICE [//ToService]
    AUTHORIZATION [dbo]
    ON QUEUE [Blah].[TheQueue]
    ([//ServiceContract]);
GO


GRANT EXECUTE ON [Blah].[TheQueueProcedure] TO [TheUser];
GO
Run Code Online (Sandbox Code Playgroud)

然后开始一切:

--kick everything off
EXEC [Blah].[SendMessage];

GO

--read results from error log
--(might need to execute once or twice to get results - because service broker is asynchronous)
declare @sqlErrorLog table (LogDate datetime, ProcessInfo nvarchar(max), Text nvarchar(max));
INSERT INTO @sqlErrorLog EXEC xp_ReadErrorLog 

SELECT * FROM @sqlErrorLog
WHERE LogDate >= DATEADD(SECOND, -15, GETDATE()) AND Text NOT LIKE 'CHECKDB%' AND Text NOT LIKE 'Starting up database ''upgrade%' AND Text NOT LIKE '%upgrade%information%' AND TEXT <> 'Error: 9001, Severity: 21, State: 1.'
ORDER BY LogDate
Run Code Online (Sandbox Code Playgroud)

Sol*_*zky 12

我需要避免将数据库标记为Trustworthy.

这当然是对 的正确态度TRUSTWORTHY,是的,这是可能的。

那么为什么它在通过服务代理时在 msdb 中作为来宾执行呢?

我原本以为这个问题是使用 Impersonation 时跨数据库问题的典型原因:默认情况下,模拟数据库级别的主体(这是EXECUTE AS子句,而不是语句可以做的)将被隔离到本地数据库.

然而,与 OP 的额外测试和讨论导致发现这种情况略有不同。似乎使用 Service Broker极少数之一模块签名不能解决所有安全问题情况之一。这就是我在设置模块签署的典型实现,因为它没有工作思路。所以,我尝试了几件事,发现只有 SQLCLR 能够做到这一点。

然后最近我发现了一个相关的问题,sa 通过与 Service Broker 的同义词没有权限访问其他数据库,该问题引用了@Remus Rusanu 的一篇文章,其中 Remus 说这确实是可能的。考虑到 Remsus 的示例代码有效,我得出结论,我一定错过了一些小细节。而且,在查看细节时,我发现使用了一个违反直觉的选项:

更改程序以具有 EXECUTE AS 子句(否则代码签名基础结构不起作用)

通常模块签名允许您删除 EXECUTE AS子句和语句,但这里是必需的。由于 Service Broker 通过EXECUTE AS USER =语句在仅数据库安全上下文中工作,因此需要它。通过将EXECUTE AS子句添加到CREATE PROCEDURE语句中,创建了一个新的安全上下文,它可以访问服务器级和/或其他数据库,这是模块签名设置的其余部分处理的。

Sooooo,嗯,很抱歉把它弄对了,然后把它改成可行的,但由于缺少那一件而并不理想;-)。但是,我现在让它按照我最初所说的方式工作:-)。下面的第一组示例代码是可使用的纯 T-SQL 模块签名方法TRUSTWORTHY OFF(现在我添加了缺少的WITH EXECUTE AS N'dbo')。我会将 SQLCLR 方法保留在底部,因为它确实有效并且可能更适合其他一些场景。

我希望通过签署程序......我可以获得跨数据库传输的权限

你可以。我认为我从未见过你的模块签名设置,但很可能你错过了我最初错过的一个小的、非典型的选项(这是让整个事情工作的关键)。

是否execute as否定通常通过证书用户关联的任何权限?

仅当它是EXECUTE AS USER语句时(不是语句的EXECUTE AS子句CREATE object,也不是EXECUTE AS LOGIN语句)。在那种情况下,安全上下文是并且只能是仅数据库,并且无法看到服务器级或其他数据库,即使模块签名就位。而且,幸运的是,这(即EXECUTE AS USER语句)正是 Service Broker 为执行激活过程所做的。所以,是的,这就是阻止您最初尝试进行模块签名的原因。并且,修复它的技巧是简单地在访问另一个数据库的 procWITH EXECUTE AS N'dbo'CREATE PROCEDURE语句中添加一个子句。你使用什么用户并不重要,但我发现dbo使用简单化OWNER如果所有者发生更改,则警告需要重新签署存储过程。当然,也可以更改数据库的所有者,所以我也希望我的选择会收到警告,但事实并非如此,所以我暂时选择忽略这种潜在的细微差别;-)。

理想解决方案 (T-SQL)

主要设置

USE [master];
GO

IF (DB_ID(N'SendDbMailFromServiceBrokerQueue') IS NOT NULL)
BEGIN
  RAISERROR(N'Dropping DB: [SendDbMailFromServiceBrokerQueue]...', 10, 1) WITH NOWAIT;
  ALTER DATABASE [SendDbMailFromServiceBrokerQueue] SET OFFLINE WITH ROLLBACK IMMEDIATE;
  ALTER DATABASE [SendDbMailFromServiceBrokerQueue] SET ONLINE WITH ROLLBACK IMMEDIATE;
  DROP DATABASE [SendDbMailFromServiceBrokerQueue];
END


RAISERROR(N'Creating DB: [SendDbMailFromServiceBrokerQueue]...', 10, 1) WITH NOWAIT;
CREATE DATABASE [SendDbMailFromServiceBrokerQueue]
  COLLATE Latin1_General_100_CI_AS_KS_SC
  WITH DB_CHAINING OFF,
       TRUSTWORTHY OFF;

ALTER DATABASE [SendDbMailFromServiceBrokerQueue]
SET  RECOVERY SIMPLE,
     PAGE_VERIFY CHECKSUM,
     ENABLE_BROKER;
GO
-------------------------------------------------

USE [SendDbMailFromServiceBrokerQueue];
GO

CREATE SCHEMA [FunStuff] AUTHORIZATION [dbo];
GO

CREATE USER [BrokerUser] WITHOUT LOGIN WITH DEFAULT_SCHEMA=[dbo];

CREATE QUEUE [FunStuff].[SendingQueue];

CREATE SERVICE [//SendingService]
    AUTHORIZATION [dbo]
    ON QUEUE [FunStuff].[SendingQueue];

CREATE MESSAGE TYPE [//AuditMessage]
    AUTHORIZATION [dbo]
    VALIDATION = NONE;

CREATE CONTRACT [//AuditContract]
    AUTHORIZATION [dbo]
    ([//AuditMessage] SENT BY INITIATOR);
GO

CREATE PROCEDURE [FunStuff].[SendMessage]
(
  @Content NVARCHAR(MAX)
)
AS
SET NOCOUNT ON;
DECLARE @ConversationHandle UNIQUEIDENTIFIER;

  BEGIN DIALOG CONVERSATION @ConversationHandle
    FROM SERVICE [//SendingService]
    TO SERVICE '//ReceivingService'
    ON CONTRACT [//AuditContract] 
    WITH ENCRYPTION = OFF;

  SEND ON CONVERSATION @ConversationHandle
    MESSAGE TYPE [//AuditMessage]
    (@Content) ;

  END CONVERSATION @ConversationHandle ;
GO
---------------------------------------------------------------------------

GO
CREATE PROCEDURE [dbo].[EmailHandler]
(
  @EmailSubject VARCHAR(255),
  @EmailContent NVARCHAR(MAX)
)
WITH EXECUTE AS N'dbo' -- THIS IS REQUIRED (when used with Service Broker)!!!
AS
  DECLARE @Recipients NVARCHAR(4000) = N'recipient@place.tld';

  EXEC [msdb].[dbo].[sp_send_dbmail]
              @profile_name = N'{my_pofile_name}',
              @recipients = @Recipients,
              @subject = @EmailSubject,
              @body = @EmailContent,
              @exclude_query_output = 1;
GO

CREATE PROCEDURE [FunStuff].[AuditMessageHandler]
(
  @EmailSubject VARCHAR(255),
  @EmailContent NVARCHAR(MAX)
)
AS
  EXECUTE [dbo].[EmailHandler] @EmailSubject, @EmailContent;
GO

CREATE PROCEDURE [FunStuff].[AuditActivation]
AS
SET XACT_ABORT ON;

DECLARE @ConversationHandle UNIQUEIDENTIFIER,
        @ConversationGroupID UNIQUEIDENTIFIER,
        @MessageBody NVARCHAR(MAX),
        @MessageTypeName NVARCHAR(256),
        @RowsReceived INT;

WHILE (1 = 1)
BEGIN

   WAITFOR(
             GET CONVERSATION GROUP @ConversationGroupID
             FROM [FunStuff].[ReceivingQueue]
        ), TIMEOUT 500;

   IF (@ConversationGroupID IS NULL)
   BEGIN
        BREAK;
   END;

   WHILE (2 = 2)
   BEGIN
      BEGIN TRANSACTION;

      RECEIVE TOP (1) 
           @ConversationHandle = [conversation_handle],
           @MessageTypeName = [message_type_name],
           @MessageBody = [message_body]
      FROM    [FunStuff].[ReceivingQueue]
      WHERE   CONVERSATION_GROUP_ID = @ConversationGroupID;

      SET @RowsReceived = @@ROWCOUNT;

      IF (@RowsReceived = 0)
      BEGIN
           COMMIT;
           BREAK;
      END;

      IF (@MessageTypeName = N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog')
      BEGIN
           END CONVERSATION @ConversationHandle;
      END;

      IF (@MessageTypeName = N'//AuditMessage')
      BEGIN
           EXEC [FunStuff].[AuditMessageHandler] N'Email From Broker test', @MessageBody;
      END;

      COMMIT TRANSACTION;
   END; -- WHILE (2 = 2)


END; -- WHILE (1 = 1)
GO

GRANT EXECUTE ON [FunStuff].[AuditActivation] TO [BrokerUser];
GO


CREATE QUEUE [FunStuff].[ReceivingQueue]
    WITH ACTIVATION (STATUS = ON,
        PROCEDURE_NAME = [FunStuff].[AuditActivation],
        MAX_QUEUE_READERS = 1,
        EXECUTE AS N'BrokerUser'
       );

CREATE SERVICE [//ReceivingService]
    AUTHORIZATION [dbo]
    ON QUEUE [FunStuff].[ReceivingQueue]
    ([//AuditContract]);
GO
Run Code Online (Sandbox Code Playgroud)

使此工作正常的模块签名步骤

USE [SendDbMailFromServiceBrokerQueue];

CREATE CERTIFICATE [Permission:SendDbMail]
  ENCRYPTION BY PASSWORD = N'MyCertificate!MineMineMine!'
  WITH SUBJECT = N'Grant permission to Send DB Mail',
  EXPIRY_DATE = '2099-12-31';

-- Sign the Stored Procedure that accesses another DB
ADD SIGNATURE
  TO [dbo].[EmailHandler]
  BY CERTIFICATE [Permission:SendDbMail]
  WITH PASSWORD = N'MyCertificate!MineMineMine!';

-- Copy the Certificate to [msdb]
DECLARE @PublicKey VARBINARY(MAX),
        @SQL NVARCHAR(MAX);

SET @PublicKey = CERTENCODED(CERT_ID(N'Permission:SendDbMail'));

SET @SQL = N'
CREATE CERTIFICATE [Permission:SendDbMail]
FROM BINARY = ' + CONVERT(NVARCHAR(MAX), @PublicKey, 1) + N';';
PRINT @SQL; -- DEBUG

EXEC [msdb].[sys].[sp_executesql] @SQL;


-- Create the Certificate-based User in [msdb]
EXEC [msdb].[sys].[sp_executesql] N'CREATE USER [Permission:SendDbMail]
FROM CERTIFICATE [Permission:SendDbMail];

GRANT AUTHENTICATE TO [Permission:SendDbMail];

PRINT ''Adding Certificate-based User to DB Role [DatabaseMailUserRole]...'';
EXEC sp_addrolemember N''DatabaseMailUserRole'', N''Permission:SendDbMail'';
';
Run Code Online (Sandbox Code Playgroud)

测试

USE [SendDbMailFromServiceBrokerQueue];

-- execute statement below if there is an error and the queue is disabled:
-- ALTER QUEUE [FunStuff].[ReceivingQueue] WITH STATUS = ON, ACTIVATION (STATUS = ON);

EXEC [FunStuff].[SendMessage] @Content = N'Woo hoo!';
Run Code Online (Sandbox Code Playgroud)

替代解决方案 (SQLCLR)

我也能够使用 SQLCLR 让它工作(是的,没有启用TRUSTWORTHY:-)。

SQLCLR C# 代码

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public class UserDefinedFunctions
{
    [SqlProcedure()]
    public static void ExecSendDbMail([SqlFacet(MaxSize = 255)] SqlString EmailSubject,
                                       SqlString EmailContent)
    {
        using (SqlConnection _Connection = new
                  SqlConnection("Server=(local); Trusted_Connection=true; Enlist=false;"))
        {
            using (SqlCommand _Command = _Connection.CreateCommand())
            {
                _Command.CommandType = CommandType.StoredProcedure;
                _Command.CommandText = @"dbo.sp_send_dbmail";

                SqlParameter _ParamProfileName =
                             new SqlParameter("profile_name", SqlDbType.NVarChar, 128);
                _ParamProfileName.Value = "{replace_with_your_profile_name}";
                _Command.Parameters.Add(_ParamProfileName);

                SqlParameter _ParamRecipients = new
                      SqlParameter("recipients", SqlDbType.VarChar, (int)SqlMetaData.Max);
                _ParamRecipients.Value = "{replace_with_your_recipients}";
                _Command.Parameters.Add(_ParamRecipients);

                SqlParameter _ParamSubject =
                          new SqlParameter("subject", SqlDbType.NVarChar, 255);
                _ParamSubject.Value = EmailSubject.Value;
                _Command.Parameters.Add(_ParamSubject);

                SqlParameter _ParamBody = new
                          SqlParameter("body", SqlDbType.NVarChar, (int)SqlMetaData.Max);
                _ParamBody.Value = EmailContent.Value;
                _Command.Parameters.Add(_ParamBody);

                SqlParameter _ParamExcludeQueryOutput =
                          new SqlParameter("exclude_query_output", SqlDbType.Bit);
                _ParamExcludeQueryOutput.Value = true;
                _Command.Parameters.Add(_ParamExcludeQueryOutput);


                _Connection.Open();
                _Connection.ChangeDatabase("msdb");

                _Command.ExecuteNonQuery();
            }
        }

        return;
    }
}
Run Code Online (Sandbox Code Playgroud)

设置

USE [master];

CREATE DATABASE [SendDbMailFromServiceBrokerQueue]
  COLLATE Latin1_General_100_CI_AS_SC
  WITH DB_CHAINING OFF,
       TRUSTWORTHY OFF;

ALTER DATABASE [SendDbMailFromServiceBrokerQueue]
SET  RECOVERY SIMPLE,
     PAGE_VERIFY CHECKSUM,
     ENABLE_BROKER;
GO

-- Create objects needed to allow for EXTERNAL_ACCESS without TRUSTWORTHY ON:
CREATE ASYMMETRIC KEY [Permission:SendDbMail$Key]
    FROM EXECUTABLE FILE = N'C:\...\NoTrustworthy.dll';
CREATE LOGIN [Permission:SendDbMail$Login]
    FROM ASYMMETRIC KEY [Permission:SendDbMail$Key];
GRANT EXTERNAL ACCESS ASSEMBLY TO [Permission:SendDbMail$Login];
GO
-------------------------------------------------

USE [SendDbMailFromServiceBrokerQueue];
GO

CREATE ASSEMBLY [NoTrustworthy]
    AUTHORIZATION [dbo]
    FROM N'C:\...\NoTrustworthy.dll'
    WITH PERMISSION_SET = EXTERNAL_ACCESS;
GO
CREATE PROCEDURE [dbo].[ExecSendDbMail]
(
  @EmailSubject NVARCHAR (255),
  @EmailContent NVARCHAR (MAX)
)
AS EXTERNAL NAME [NoTrustworthy].[UserDefinedFunctions].[ExecSendDbMail];
GO

CREATE SCHEMA [FunStuff] AUTHORIZATION [dbo];
GO

CREATE USER [BrokerUser] WITHOUT LOGIN WITH DEFAULT_SCHEMA=[dbo];

CREATE QUEUE [FunStuff].[SendingQueue];

CREATE SERVICE [//SendingService]
    AUTHORIZATION [dbo]
    ON QUEUE [FunStuff].[SendingQueue];

CREATE MESSAGE TYPE [//AuditMessage]
    AUTHORIZATION [dbo]
    VALIDATION = NONE;

CREATE CONTRACT [//AuditContract]
    AUTHORIZATION [dbo]
    ([//AuditMessage] SENT BY INITIATOR);
GO


CREATE PROCEDURE [FunStuff].[SendMessage]
(
  @Content NVARCHAR(MAX)
)
AS
SET NOCOUNT ON;
DECLARE @ConversationHandle UNIQUEIDENTIFIER;

    BEGIN DIALOG CONVERSATION @ConversationHandle
        FROM SERVICE [//SendingService]
        TO SERVICE '//ReceivingService'
        ON CONTRACT [//AuditContract] 
        WITH ENCRYPTION = OFF;

    SEND ON CONVERSATION @ConversationHandle
      MESSAGE TYPE [//AuditMessage]
      (@Content) ;

    END CONVERSATION @ConversationHandle ;
GO
---------------------------------------------------------------------------

CREATE PROCEDURE [dbo].[EmailHandler]
(
  @EmailSubject VARCHAR(255),
  @EmailContent NVARCHAR(MAX)
)
AS
    -- other logic

    EXEC [dbo].[ExecSendDbMail] @EmailSubject, @EmailContent;
GO


GO
CREATE PROCEDURE [FunStuff].[AuditMessageHandler]
(
  @EmailSubject VARCHAR(255),
  @EmailContent NVARCHAR(MAX)
)
AS
    EXECUTE [dbo].[EmailHandler] @EmailSubject, @EmailContent;
GO

CREATE PROCEDURE [FunStuff].[AuditActivation]
AS
SET XACT_ABORT ON;

DECLARE @ConversationHandle UNIQUEIDENTIFIER,
        @ConversationGroupID UNIQUEIDENTIFIER,
        @MessageBody NVARCHAR(MAX),
        @MessageTypeName NVARCHAR(256),
        @RowsReceived INT;

WHILE (1 = 1)
BEGIN

     WAITFOR(
               GET CONVERSATION GROUP @ConversationGroupID
               FROM [FunStuff].[ReceivingQueue]
          ), TIMEOUT 500;

     IF (@ConversationGroupID IS NULL)
     BEGIN
          BREAK;
     END;

     WHILE (2 = 2)
     BEGIN
          BEGIN TRANSACTION;
          PRINT 'Get Message';

          RECEIVE TOP (1) 
               @ConversationHandle = [conversation_handle],
               @MessageTypeName = [message_type_name],
               @MessageBody = [message_body]
          FROM    [FunStuff].[ReceivingQueue]
          WHERE   CONVERSATION_GROUP_ID = @ConversationGroupID;

          SET @RowsReceived = @@ROWCOUNT;

          IF (@RowsReceived = 0)
          BEGIN
               COMMIT;
               BREAK;
          END;


          IF (@MessageTypeName = 
      N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog')
          BEGIN
               END CONVERSATION @ConversationHandle;
          END;

          IF (@MessageTypeName = N'//AuditMessage')
          BEGIN
               EXEC [FunStuff].[AuditMessageHandler]
                       N'Email From Broker test', @MessageBody;
          END;

          COMMIT TRANSACTION;
     END; -- WHILE (2 = 2)

END; -- WHILE (1 = 1)
GO

GRANT EXECUTE ON [FunStuff].[AuditActivation] TO [BrokerUser];
GO



CREATE QUEUE [FunStuff].[ReceivingQueue]
    WITH ACTIVATION (STATUS = ON,
        PROCEDURE_NAME = [FunStuff].[AuditActivation],
        MAX_QUEUE_READERS = 1,
        EXECUTE AS N'BrokerUser'
       );

CREATE SERVICE [//ReceivingService]
    AUTHORIZATION [dbo]
    ON QUEUE [FunStuff].[ReceivingQueue]
    ([//AuditContract]);
GO

---------------------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

测试

USE [SendDbMailFromServiceBrokerQueue];

-- execute statement below if there is an error and the queue is disabled:
-- ALTER QUEUE [FunStuff].[ReceivingQueue] WITH STATUS = ON, ACTIVATION (STATUS = ON);

EXEC [FunStuff].[SendMessage] @Content = N'try me!';
Run Code Online (Sandbox Code Playgroud)

清理

IF (DB_ID(N'SendDbMailFromServiceBrokerQueue') IS NOT NULL)
BEGIN
  RAISERROR(N'Dropping DB: [SendDbMailFromServiceBrokerQueue]...', 10, 1) WITH NOWAIT;
  ALTER DATABASE [SendDbMailFromServiceBrokerQueue] SET OFFLINE WITH ROLLBACK IMMEDIATE;
  ALTER DATABASE [SendDbMailFromServiceBrokerQueue] SET ONLINE WITH ROLLBACK IMMEDIATE;
  DROP DATABASE [SendDbMailFromServiceBrokerQueue];
END

DROP LOGIN [Permission:SendDbMail$Login];
DROP ASYMMETRIC KEY [Permission:SendDbMail$Key];
Run Code Online (Sandbox Code Playgroud)