1

本周,我发现自己需要一些动态查询。现在,动态查询和动态 where 子句已经不是什么新鲜事了,而且在整个网络上都有很好的记录。然而,我需要更多的东西。我需要一种流畅的方式将新的 where 字段拉到客户端,并允许用户根据需要制作尽可能多的过滤器。甚至在单个字段上有多个过滤器。更重要的是,我需要访问 SQL Server 中所有可能的运算符。以下代码是实现此目的的一种方法。我将尝试用底部的完整代码指出代码的亮点。

希望你喜欢代码。

要求

  1. 该解决方案永远不会允许 SQL 注入。(不能使用 exec(command))
  2. 存储过程的调用者可以是任何东西。
  3. 数据集必须来自存储过程。
  4. 任何字段都可以根据需要过滤多次,几乎可以进行任何操作。
  5. 应允许过滤器的任何组合。
  6. 存储过程应该允许强制参数

首先,让我们看一下参数。

CREATE PROCEDURE [dbo].[MyReport]
-- Add the parameters for the stored procedure here
@p_iDistributorUID  INT,        --    manditory
@p_xParameters      XML = null  --optional parameters (hostile)

第一个参数必须始终发送,在这个演示中,我们有一个必须发送的经销商 ID。第二个参数是一个 XML 文档。这些是“动态 Where 子句”,我们认为这些潜在的 sql 注入,或者我认为这个参数是敌对的。

<root>
<OrFilters>
    <AndFilter Name="vcinvoicenumber" Operator="2" Value="inv12"/>
    <AndFilter Name="vcID" Operator="1" Value="asdqwe"/>
</OrFilters>
<OrFilters>
    <AndFilter Name="iSerialNumber" Operator="1" Value="123456"/>
</OrFilters>

NAME= 字段名(如果你想混淆,你可以只使用 object_id) OPERATOR = SQL 运算符,例如 <,>,=,like,ect。VALUE 是用户输入的内容。

这是最终代码的样子。

Select *
FROM someTable
Where (
vcinvoicenumber like ‘inv12%’
and vcID = ‘asdqwe’
) 
Or
(
iSerialNumber   = ‘123456’  
)

首先是找出有多少“OrFilters”标签。

SELECT @l_OrFilters = COUNT(1)
FROM   @p_xParameters.nodes('/root/OrFilters') Tab(Col)

接下来,我们需要一个临时表来保存 XML 文档中的值。

CREATE TABLE #temp
(keyid int IDENTITY(1,1) NOT NULL,value varchar(max))

我们现在为第一个“OrFilters”标签创建一个光标。

DECLARE OrFilter_cursor CURSOR LOCAL
FOR 
SELECT Tab.Col.value('@Name','varchar(max)') AS Name
    ,Tab.Col.value('@Operator','Smallint') AS Operator
    ,Tab.Col.value('@Value','varchar(max)') AS Value
    FROM   @p_xParameters.nodes('/root/OrFilters[sql:variable("@l_OrFilters")]/AndFilter') Tab(Col)

为了确保我们有一个有效的字段,我们检查系统表。

SELECT @l_ParameterInName = [all_columns].Name 
,@l_ParameterDataType= [systypes].Name
,@l_ParameterIsVariable= Variable
,@l_ParameterMax_length=max_length
,@l_ParameterpPrecision=precision
,@l_ParameterScale =[all_columns].scale
 FROM [AprDesktop].[sys].[all_views]
INNER JOIN [AprDesktop].[sys].[all_columns]
    ON [all_views].object_id = [all_columns].object_id
INNER JOIN [AprDesktop].[sys].[systypes]
    ON [all_columns].system_type_id = [systypes].xtype
WHERE [all_views].name = 'vw_CreditMemo_Lists'
and [all_columns].Name= @l_Name

现在我们将参数保存到临时表

IF @@ROWCOUNT = 1   
BEGIN
INSERT INTO #temp (value) SELECT @l_Value
SET @l_FilterKey = @@IDENTITY
.
.   
.

我们调用一个将实际构建 where 子句的函数。

SET @l_TemporaryWhere +=  
dbo.sfunc_FilterWhereBuilder2(@l_Operator
,@l_ParameterInName
,@l_TemporaryWhere
,CAST(@l_FilterKey AS VARCHAR(10))
,@l_ParameterDataType
,@l_ParameterVariable)

查看这个函数,您可以看到我们使用 case 语句来生成 where 子句字符串。

set @l_CastToType = ' CAST( VALUE as ' + @p_DataType + @p_PrecisionScale + ') '
set @l_CastToString = ' CAST( '+@p_Field+' as VARCHAR(MAX)) '

-- Add the T-SQL statements to compute the return value here
SELECT @l_Return =    
    CASE 
--EQUAL
--ex: vcUID = (select  value FROM #temp where keyid = 1)
WHEN @p_Command = 1 
    THEN @p_Field + ' = (select  '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')' 

--BEGIN WITH
--ex:vcInvoiceNumber LIKE (select value+'%' FROM #temp where keyid = 2)
WHEN @p_Command = 2 
    THEN @l_CastToString +' LIKE (select value+'+ QUOTENAME('%','''') +' FROM #temp where keyid = ' + @p_KeyValue + ')'
.
.
.

最后调用 sp_execute。

EXECUTE sp_executesql @l_SqlCommand ,@l_Parameters, @p_iDistributorUID

呼叫代码

DECLARE @return_value int

DECLARE @myDoc xml
SET @myDoc = 
'<root>
<OrFilters>
    <AndFilter Name="vcinvoicenumber" Operator="1" Value="123"/>
</OrFilters>

</root>'

EXEC    @return_value = [dbo].[spp_CreditMemo_Request_List_v2]
    @p_siShowView = 1,
    @p_iDistributorUID = 3667,
    @p_xParameters = @myDoc

SELECT  'Return Value' = @return_value

主存储过程

ALTER PROCEDURE [dbo].[MyReport]
    -- Add the parameters for the stored procedure here
    @p_iDistributorUID          INT                 ,   --manditory
    @p_xParameters              XML         = null      --optional parameters(hostile)
AS
BEGIN

    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    DECLARE @l_TemporaryWhere       NVARCHAR(MAX)

    -- declare variables 
    DECLARE @l_SqlCommand           NVARCHAR(MAX)
    DECLARE @l_Parameters           NVARCHAR(MAX)
    DECLARE @l_WhereClause          NVARCHAR(MAX)

    DECLARE @l_OrFilters            INT

    --cursor variables
    DECLARE @l_Name                 VARCHAR(MAX)
    DECLARE @l_Operator             SMALLINT
    DECLARE @l_Value                VARCHAR(MAX)

    --variables from the database views 
    DECLARE @l_ParameterInName      NVARCHAR(128)
    DECLARE @l_ParameterDataType    NVARCHAR(128)
    DECLARE @l_ParameterIsVariable  BIT
    DECLARE @l_ParameterMax_length  SMALLINT
    DECLARE @l_ParameterpPrecision  TINYINT
    DECLARE @l_ParameterScale       TINYINT

    --the variable that holds the latest @@identity
    DECLARE @l_FilterKey INT


    --init local variables
    SET @l_SqlCommand   =''
    SET @l_Parameters   =''
    SET @l_WhereClause  =''


    BEGIN TRY

        --verify manditory variables        
        if @p_iDistributorUID is null
            raiserror('Null values not allowed for @p_iDistributorUID', 16, 1)



        --Build the base query
        -- only the fields needed in the tile should be selected
        SET @l_SqlCommand = 
            ' SELECT * ' +
            ' FROM vw_Lists ' 



        --how many "OR" filters are there
        SELECT @l_OrFilters = COUNT(1)
        FROM   @p_xParameters.nodes('/root/OrFilters') Tab(Col)


        --create a temp table to 
        --hold the parameters to send into the sp
        CREATE TABLE #temp
        (
            keyid int IDENTITY(1,1) NOT NULL,value varchar(max)
        )


        --Cycle through all the "OR" Filters
        WHILE @l_OrFilters > 0
        BEGIN
            SET @l_TemporaryWhere = '';

            --Create a cursor of the Next "OR" filter
            DECLARE OrFilter_cursor CURSOR LOCAL
            FOR 
                SELECT Tab.Col.value('@Name','varchar(max)') AS Name
                    ,Tab.Col.value('@Operator','Smallint') AS Operator
                    ,Tab.Col.value('@Value','varchar(max)') AS Value
                FROM   @p_xParameters.nodes('/root/OrFilters[sql:variable("@l_OrFilters")]/AndFilter') Tab(Col)

            OPEN OrFilter_cursor
            FETCH NEXT FROM OrFilter_cursor 
            INTO @l_Name, @l_Operator,@l_Value  

            WHILE    @@FETCH_STATUS = 0
            BEGIN

                --verify the parameter actual exists
                --  and get parameter details
                SELECT @l_ParameterInName = [all_columns].Name 
                    ,@l_ParameterDataType= [systypes].Name
                    ,@l_ParameterIsVariable= Variable
                    ,@l_ParameterMax_length=max_length
                    ,@l_ParameterpPrecision=precision
                    ,@l_ParameterScale =[all_columns].scale
                FROM [AprDesktop].[sys].[all_views]
                    INNER JOIN [sys].[all_columns]
                        ON [all_views].object_id = [all_columns].object_id
                    INNER JOIN [sys].[systypes]
                        ON [all_columns].system_type_id = [systypes].xtype
                WHERE [all_views].name = 'vw_CreditMemo_Lists'
                    and [all_columns].Name= @l_Name

                --if the paremeter exists, create a where clause
                --  if the parameters does not exists, possible injection
                IF @@ROWCOUNT = 1   
                BEGIN

                    --insert into the temp table the parameter value
                    --NOTE: we have turned in the @@identity as the key
                    INSERT INTO #temp (value) SELECT @l_Value
                    SET @l_FilterKey = @@IDENTITY


                    -- if the parameter is variable in length, add the length
                    DECLARE @l_ParameterVariable VARCHAR(1000)
                    IF @l_ParameterIsVariable = 1
                    BEGIN
                        SET @l_ParameterVariable ='(' + CAST(@l_ParameterMax_length as VARCHAR(MAX))  + ') '
                    END
                    ELSE
                    BEGIN
                        SET @l_ParameterVariable = ''
                    END

                    -- create the where clause for this filter
                    SET @l_TemporaryWhere +=  
                        dbo.sfunc_FilterWhereBuilder2(@l_Operator
                                                     ,@l_ParameterInName
                                                     ,@l_TemporaryWhere
                                                     ,CAST(@l_FilterKey AS VARCHAR(10))
                                                     ,@l_ParameterDataType
                                                     ,@l_ParameterVariable)


                END

                FETCH NEXT FROM OrFilter_cursor 
                INTO @l_Name, @l_Operator,@l_Value  

            END

            -- clean up the cursor
            CLOSE OrFilter_cursor
            DEALLOCATE OrFilter_cursor

            --add the and filers
            IF @l_TemporaryWhere != ''
            BEGIN

                --if the where clause is not empty, we need to add an OR
                IF @l_WhereClause != ''
                BEGIN
                    SET @l_WhereClause += ' or ';
                END

                --add temp to where clause including the 
                SET @l_WhereClause += '(' + @l_TemporaryWhere + ')';

            END

            --get the next AND set
            SET @l_OrFilters = @l_OrFilters - 1

        END

        --generate the where clause
        IF @l_WhereClause != ''
        BEGIN
            SET @l_WhereClause ='('+ @l_WhereClause + ') AND '
        END

        --add in the first mandatory parameter          
        SET @l_WhereClause  += '  vw_CreditMemo_Lists.iDistributorUID = @l_iDistributorUID ' 
        SET @l_Parameters   += '@l_iDistributorUID int'


        --do we need to attach the where clause
        if @l_WhereClause IS NOT NULL AND RTRIM(LTRIM(@l_WhereClause)) != ''
        BEGIN
            SET @l_SqlCommand += ' WHERE '+ @l_WhereClause;
        END


        print @l_SqlCommand

        --query for the data
        EXECUTE sp_executesql @l_SqlCommand ,@l_Parameters, @p_iDistributorUID

    END TRY

    BEGIN CATCH

        DECLARE @ErrorUID int;
        DECLARE @ErrorMessage NVARCHAR(4000);
        DECLARE @ErrorSeverity INT;
        DECLARE @ErrorState INT;

        SELECT 
            @ErrorMessage = ERROR_MESSAGE(),
            @ErrorSeverity = ERROR_SEVERITY(),
            @ErrorState = ERROR_STATE();


        --write the to stored procedure log 
        EXEC @ErrorUID = spp_Errors_CreateEntry @l_SqlCommand 


        -- Use RAISERROR inside the CATCH block to return error
        -- information about the original error that caused
        -- execution to jump to the CATCH block.
        RAISERROR (@ErrorUID, -- Message text.
                   @ErrorSeverity, -- Severity.
                   @ErrorState -- State.
                   );


        IF(CURSOR_STATUS('LOCAL','OrFilter_cursor') >= 0)
        BEGIN
            CLOSE OrFilter_cursor
        END     

        IF(CURSOR_STATUS('LOCAL','OrFilter_cursor') = -1)
        BEGIN
            DEALLOCATE OrFilter_cursor
        END     


    END CATCH

    return

END

功能

ALTER FUNCTION [dbo].[sfunc_FilterWhereBuilder2]
(
    @p_Command          SMALLINT                ,
    @p_Field            VARCHAR(1000)           ,
    @p_WhereClause      VARCHAR(MAX)            ,
    @p_KeyValue         VARCHAR(10)             ,
    @p_DataType         VARCHAR(100)    = NULL  ,
    @p_PrecisionScale   VARCHAR(100)    = NULL

)
RETURNS VARCHAR(MAX)
AS
BEGIN
    -- Declare the return variable here
    DECLARE @l_Return       VARCHAR(MAX)
    DECLARE @l_CastToType   VARCHAR(4000)
    DECLARE @l_CastToString VARCHAR(MAX)

    set @l_CastToType = ' CAST( VALUE as ' + @p_DataType + @p_PrecisionScale + ') '
    set @l_CastToString = ' CAST( '+@p_Field+' as VARCHAR(MAX)) '

    -- Add the T-SQL statements to compute the return value here
    SELECT @l_Return =    
        CASE 
            --EQUAL
            --ex: vcBurnUID = (select  value FROM #temp where keyid = 1)
            WHEN @p_Command = 1 
                THEN @p_Field + ' = (select  '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')' 

            --BEGIN WITH
            --ex:vcInvoiceNumber LIKE (select value+'%' FROM #temp where keyid = 2)
            WHEN @p_Command = 2 
                THEN @l_CastToString +' LIKE (select value+'+ QUOTENAME('%','''') +' FROM #temp where keyid = ' + @p_KeyValue + ')'

            --END WITH
            --ex:vcInvoiceNumber LIKE (select '%'+value FROM #temp where keyid = 2)
            WHEN @p_Command = 4 
                THEN @l_CastToString +' LIKE (select '+ QUOTENAME('%','''') +'+value FROM #temp where keyid = ' + @p_KeyValue + ')'

            --END WITH
            --ex:vcInvoiceNumber LIKE (select '%'+value+'%' FROM #temp where keyid = 2)
            WHEN @p_Command = 8 
                THEN @l_CastToString +' LIKE (select '+ QUOTENAME('%','''') +'+value+'+ QUOTENAME('%','''') +' FROM #temp where keyid = ' + @p_KeyValue + ')'

            --greater than
            --ex: iSerialNumber > (select  CAST(value as INT) FROM #temp where keyid = 1)
            WHEN @p_Command = 16 
                THEN @p_Field +' > (select  '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'

            --greater than equal
            --ex: iSerialNumber >= (select  CAST(value as INT) FROM #temp where keyid = 1)
            WHEN @p_Command = 32 
                THEN @p_Field +' >= (select  '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'

            --Less than
            --ex: iSerialNumber < (select  CAST(value as INT) FROM #temp where keyid = 1)
            WHEN @p_Command = 64 
                THEN @p_Field +' < (select  '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'

            --less than equal
            --ex: iSerialNumber <= (select  CAST(value as INT) FROM #temp where keyid = 1)
            WHEN @p_Command = 128 
                THEN @p_Field +' <= (select  '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'

            --less than equal
            --ex: iSerialNumber != (select  CAST(value as INT) FROM #temp where keyid = 1)
            WHEN @p_Command = 256 
                THEN @p_Field +' != (select  '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'

            --default to an empty string
            ELSE ''

        END 

        if @l_Return != '' and LEN(@p_WhereClause) > 1
        begin
            set @l_Return =  ' AND ' + @l_Return
        end

    -- Return the result of the function
    RETURN @l_Return

END
4

0 回答 0