web-dev-qa-db-fra.com

Injecter des informations de contexte de connexion à l'aide d'une table persistante

Je travaille sur une application comportant plusieurs modules hérités qui dépendent fortement de la procédure stockée (aucun orme, de sorte que toutes les extratures et la persistance des données ne sont effectuées via des procédures stockées).

La sécurité des modules hérités s'appuie sur SUSER_NAME() pour obtenir l'utilisateur actuel et appliquer des règles de sécurité.

Je migre pour utiliser un connecteur ORM (Entity Framework) et SQL utilisera un utilisateur générique pour vous connecter à la base de données (SQL Server), de sorte que je dois fournir un nom d'utilisateur actuel à de nombreuses procédures.

Afin d'éviter les modifications de code .NET, j'ai pensé à "injecter" l'utilisateur actuel dans le contexte lorsqu'une nouvelle connexion est faite:

CREATE TABLE dbo.ConnectionContextInfo 
(
    ConnectionContextInfoId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_ConnectionContextInfo PRIMARY KEY,
    Created DATETIME2 NOT NULL CONSTRAINT DF_ConnectionContextInfo DEFAULT(GETDATE()),
    SPID INT NOT NULL,
    AttributeName VARCHAR(32) NOT NULL, 
    AttributeValue VARCHAR(250) NULL,
    CONSTRAINT UQ_ConnectionContextInfo_Info UNIQUE(SPID, AttributeName)
)
GO

Lorsqu'une connexion est ouverte (ou réutilisée, comme étant utilisé un pool de connexion), la commande suivante est utilisée:

exec sp_executesql N'
    DELETE FROM dbo.ConnectionContextInfo WHERE SPID = @@SPID AND AttributeName = @UsernameAttribute;
    INSERT INTO dbo.ConnectionContextInfo (SPID, AttributeName, AttributeValue) VALUES (@@SPID, @UsernameAttribute, @Username);
',N'@UsernameAttribute nvarchar(8),@Username nvarchar(16)',@UsernameAttribute=N'Username',@Username=N'domain\username'
go

(0 CPU, ~ 15 lit, <6 ms)

Une fonction scalaire permet d'obtenir facilement l'utilisateur actuel:

alter FUNCTION dbo.getCurrentUser()
RETURNS VARCHAR(250)
AS
BEGIN
    DECLARE @ret VARCHAR(250) = (SELECT AttributeValue FROM ConnectionContextInfo where SPID = @@SPID AND AttributeName = 'Username')
    -- fallback to session current, if no data is found on current SPID (i.e. call outside of the actual application)
    RETURN ISNULL(@ret, SUSER_NAME())
END
GO

Y a-t-il des mises en garde (robustesse, performance, etc.) dans cette approche d'une perspective de niveau de données?

Merci.

4
Alexei

En termes de performances, vous encourrez les frais généraux du DELETE et INSERT chaque fois que la connexion est ouverte. Vous pouvez également utiliser la connexion intégrée contextuelle_info à cette fin. L'exemple ci-dessous stocke les informations dans une structure de 48 octets fixe.

EXEC sp_executesql N'
    DECLARE @ContextInfo binary(48);
    SET @ContextInfo = CAST(CAST(@UsernameAttribute AS nchar(8)) + CAST(@Username AS nchar(16)) AS binary(48));
',N'@UsernameAttribute nvarchar(8),@Username nvarchar(16)',@UsernameAttribute=N'Username',@Username=N'domain\username'
GO


CREATE FUNCTION dbo.getCurrentUser()
RETURNS VARCHAR(250)
AS
BEGIN
DECLARE
      @ContextInfo binary(48) = CONTEXT_INFO()
    , @Username nvarchar(16);
    SET @Username = RTRIM(CAST(SUBSTRING(@ContextInfo, 17, 32) AS nvarchar(16)));

    RETURN ISNULL(@Username, SUSER_NAME());

END
GO

De plus, sp_set_session_context et session_context () sont disponibles dans la base de données SQL Server 2016 et Azure SQL. C'est une méthode beaucoup plus propre si elle est disponible pour vous.

4
Dan Guzman

Je peux voir trois problèmes mineurs et un problème majeur avec cette approche:

Problèmes mineurs:

  1. Votre instruction DELETE dans votre requête ad hoc utilise le prédicat suivant:

    AttributeName = @UsernameAttribute
    

    Au lieu de cela, vous ne devriez que filtrer uniquement SPID = @@SPID Depuis que vous ne voudriez pas de valeurs obsolètes à partir de la dernière instance de cette Spid suspendu, mélangée avec votre ou vos valeurs actuelles.

  2. Dans votre table ConnectionContextInfo table, les deux Attribute% Les colonnes sont définies comme VARCHAR, mais dans la requête ad hoc, vous avez les paramètres définis comme NVARCHAR et même préfixer les chaînes avec N. Vous devez mettre à jour la table de sorte qu'il soit également défini comme NVARCHAR.

  3. Les données obsolètes des valeurs de SPID supérieures qui sont insérées pendant les temps d'utilisation de crête s'attarderont pendant un certain temps car il n'y a pas DELETE appelé jusqu'à la prochaine fois que SPID est utilisé, ce qui pourrait ne jamais être utilisé. Vous pouvez soit créer un travail d'agent SQL Server à exécuter une fois par jour et DELETE lignes créées il y a plus de x jours.

problème majeur (et une solution):

Basé sur le Commentaire Vous avez fait la réponse de @ Dan, vous ne pouvez pas utiliser CONTEXT_INFO En raison de la taille limitant votre capacité à ajouter plus d'attributs à l'avenir, surtout si vous utilisez NVARCHAR.

Heureusement, vous n'avez pas besoin d'une table permanente. Vous pouvez simplifier cela un peu en utilisant une table temporaire globale. Cela éliminerait la nécessité de DELETE antérieures des lignes antérieures et d'un travail d'agent SQL Nettoyez des enregistrements obsolètes, OR Pré-allouer un tas de lignes mais nécessitent toujours de mettre à jour tout Les rangées correspondant à la cordon vider la chaîne ou NULL par connexion.

Lors de l'établissement de la connexion, il vous suffit de créer la table. Ensuite, insérez les paires de la clé/de la valeur que vous aimez. La table sera automatiquement chutée lorsque la connexion se ferme (connexions non poolées et réalisées) ou lorsque la prochaine session de réutilisation de la connexion exécute sa première instruction et son interne sp_reset_connection Le processus est exécuté (connexions groupées).

Maintenant, vous pouvez vous demander:

  • La table TEMP sera-t-elle nettoyée lorsque la procédure stockée (ou la requête ad hoc) se termine ?

    S'il s'agissait d'une table temporaire locale (c'est-à-dire #Name), alors oui, il serait nettoyé lorsque le processus/sous-processus a été créé à la fin et ne sera pas disponible dans le contexte parent. Mais les tables temporaires mondiales (c'est-à-dire ##Name) Survivre à la fin du processus qu'ils ont été créés et sont disponibles dans le contexte parent.

  • Parce que les tables TEMP sont globales, elles ne peuvent pas partager le même nom.

    Correct, en utilisant un nom de table standard dans le CREATE TABLE La déclaration ne fonctionnera pas car plusieurs sessions s'affrontent les unes avec les autres. Nous avons juste besoin d'un moyen de différencier le nom de la table en utilisant quelque chose à la disposition de la session, unique à la session, mais non passé de l'application car le code ne fonctionnerait que de l'application et l'objectif est de ne pas changer le code de l'application. Par conséquent, juste ajouter le @@SPID Valeur à un préfixe connue, fixe, puis vous pouvez déduire le nom de la table à tout moment de cette session.

    Quelque chose comme:

    CREATE PROCEDURE dbo.InitializeSessionContext
    AS
    SET NOCOUNT ON;
    
    DECLARE @Query NVARCHAR(MAX),
            @Template NVARCHAR(MAX) = N'
    
    CREATE TABLE ##SessionContext{{SPID}}
    (
      AttributeName NVARCHAR(32) NOT NULL, 
      AttributeValue NVARCHAR(250) NULL,
      Created DATETIME2 NOT NULL CONSTRAINT DF_SessionContext{{SPID}} DEFAULT(GETDATE())
    );';
    
    SET @Query = REPLACE(@Template, N'{{SPID}}', @@SPID);
    
    EXEC(@Query);
    GO
    
  • Contrairement à l'UDF montré dans la question, cette approche nécessite à la fois un SQL dynamique et accéder aux tables temporaires, qui ne sont pas autorisés.

    Correct, aucun de ceux-ci ne peut être fait dans des fonctions T-SQL, mais ils peuvent être effectués de deux autres manières:

    • Utilisez une procédure stockée T-SQL pour réussir la valeur de retour via un paramètre OUTPUT ou
    • Créez un SQLCLR Scalar UDF. Utilisez la connexion contextuelle en cours (I.E. "Context Connection = true" et l'assemblage peut être marqué comme WITH PERMISSION_SET = SAFE. SQLCLR UDFS peut faire à la fois des tables temporaires locales dynamiques et d'accès.
1
Solomon Rutzky

Je suggérerais de pré-allouer des lignes dans la table de contexte d'une manière qui aide à minimiser la conflit de la page.

C'est l'une des difficultés à quelques reprises que je recommanderai en fait d'utiliser un fichier GUID en tant que clé de clustering de table. Cette touche agira en tant que randomizer pour l'emplacement de la page pour tout SPID donné pour réduire Page Convention.

CREATE TABLE dbo.ConnectionContextInfo 
(
    SlotID UNIQUEIDENTIFIER 
        CONSTRAINT PK_ConnectionContextInfo 
        PRIMARY KEY CLUSTERED
        DEFAULT (NEWID())
    , SPID INT NOT NULL
    , AttributeName VARCHAR(128) NOT NULL 
    , AttributeValue VARCHAR(255) NULL
    , CONSTRAINT UQ_ConnectionContextInfo_Info 
           UNIQUE(SPID, AttributeName)
);
GO
CREATE INDEX IX_ConnectionContextInfo_Lookups
ON dbo.ConnectionContextInfo(SPID, AttributeName);
GO

Cela pré-remplira la table avec les lignes requises, une combinaison de spid/attribute.

;WITH Numbers AS 
(
    SELECT TOP(32767) 
        rn = ROW_NUMBER() OVER (ORDER BY o1.object_id)
    FROM sys.objects o1
        , sys.objects o2
        , sys.objects o3
)
, Attributes AS
(
    SELECT AttrName = N'UserName'
    UNION ALL
    SELECT AttrName = N'SomeOtherAttribute'
)
INSERT INTO dbo.ConnectionContextInfo (SPID, AttributeName) 
SELECT rn
    , AttrName
FROM Numbers
    , Attributes;

Si vous avez vraiment besoin d'utiliser sp_executesql, Je le ferais comme ça:

EXEC sys.sp_executesql N'UPDATE dbo.ConnectionContextInfo 
SET AttributeValue = @AttributeValue
WHERE SPID = @@SPID
    AND AttributeName = @AttributeName;'
    , N'@AttributeName nvarchar(128), @AttributeValue nvarchar(128)'
    , @AttributeName = N'Username'
    , @AttributeValue = N'domain\username';
GO

Les résultats pour mon SPID:

SELECT * 
    , plc.*
FROM dbo.ConnectionContextInfo
CROSS APPLY sys.fn_PhysLocCracker(%%PhysLoc%%) plc 
WHERE SPID = @@SPID;

enter image description here

À la place d'utiliser sp_executesql, Je recommanderais d'utiliser une procédure stockée pour effectuer la mise à jour afin que vous puissiez facilement inclure une manipulation des erreurs et peut mettre à jour librement ce code sur le côté serveur sans avoir un impact sur le client. Par exemple:

IF OBJECT_ID('dbo.UpdateConnectionContextInfo') IS NOT NULL
DROP PROCEDURE UpdateConnectionContextInfo;
GO
CREATE PROCEDURE dbo.UpdateConnectionContextInfo
(
    @AttributeName NVARCHAR(128)
    , @AttributeValue NVARCHAR(255)
)
AS
BEGIN
    SET NOCOUNT ON;
    UPDATE dbo.ConnectionContextInfo 
    SET AttributeValue = @AttributeValue
    WHERE SPID = @@SPID
        AND AttributeName = @AttributeName
    RETURN @@ROWCOUNT;
END
GO

Cela définira @retval à 0 si le @AttributeName passé est invalide:

DECLARE @RetVal INT;
EXEC @RetVal = dbo.UpdateConnectionContextInfo 
    @AttributeName = 'UserName', @AttributeValue = 'SomeUser';
SELECT @RetVal;
1
Max Vernon