https://alonso.network/rss 09月30日 19:07
数据库访问安全策略
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

在Web应用安全领域,高效且安全地管理数据库访问至关重要。一种不常见的方法是使用单个专用的应用数据库登录来模拟单个应用用户,利用数据库内置的安全功能。我在SQL Server环境中实现了这种方法,分享了其优缺点,并与常见替代方案进行了比较,最后提供了一个以安全为中心的代码审查清单。

💡 简化连接管理:应用通过__web_user身份管理单个数据库连接或连接池,避免了为每个应用用户管理单独数据库连接的复杂性和开销。

🔒 减少密码暴露:该方法无需在应用层存储或发送单个用户数据库密码(例如,在会话存储或客户端存储中),显著降低了应用服务器被入侵时凭证被大规模泄露的风险。

🛡️ 利用数据库安全功能:允许使用服务器内的细粒度权限控制。通过为应用用户创建数据库用户(而非登录),并将它们分配给具有特定权限的角色(例如,authenticated user角色),可以控制每个用户在数据库层面的操作权限。

🔐 鼓励最小权限原则:可以通过仅授予authenticated user角色访问特定存储过程和视图的权限来执行最小权限原则,禁止直接访问表或敏感数据库功能(如CREATE USER, DROP USER, ALTER PERMISSIONS)。

🌐 集中数据库授权:数据库级别的安全策略(如存储过程和视图的权限)可以集中管理在数据库内,而不是完全依赖应用级别的授权逻辑。

In the realm of web application security, managing database access efficiently and securely is paramount. An uncommon approach involves using a single, dedicated application database login to impersonate individual application users, using the database's built-in security features. I've implemented this approach in SQL Server environments, and here's what I've learned about its strengths and weaknesses, comparing it to common alternatives, and concluding with a security-focused code review checklist.

The Impersonation-Based Authentication Flow

In this model, your web application utilizes a single, privileged server login, let's call it __web_user. Instead of connecting as each individual user, __web_user establishes a single connection to the database. Upon a successful application-level authentication, __web_user then uses the server's impersonation feature (e.g., EXECUTE AS USER = 'ApplicationDbUser') to temporarily assume the identity and permissions of the specific application database user associated with the authenticated individual.

Strengths

Simplified Connection Management: Your application manages a single database connection or a pool of connections under the __web_user identity, avoiding the complexities and overhead of managing individual database connections for each application user.

Reduced Password Exposure: This approach means you don't need to store or send individual user database passwords within the application layer (e.g., in session stores or client-side storage), significantly reducing the risk of a widespread credential compromise if the application server is breached.

Using Database Security Features: It allows you to use the fine-grained permission controls within the server. By creating database users (not logins) for your application users and assigning them to roles with specific permissions (e.g., the authenticated user role), you can control what each user can do at the database level.

Encourages Principle of Least Privilege: You can enforce the principle of least privilege by only granting the AuthenticatedUser role access to specific stored procedures and views, prohibiting direct access to tables or sensitive database features (like CREATE USERDROP USERALTER PERMISSIONS).

Centralized Database Authorization: Database-level security policies (like permissions on stored procedures and views) can be centrally managed within the database, rather than being solely dependent on application-level authorization logic.

Weaknesses

Reliance on Stored Procedure Security: The primary security boundary shifts from direct table access to the security of your stored procedures and views. If any of these objects are vulnerable to SQL injection (despite your precautions) or contain logical flaws, an attacker could potentially escalate privileges or access unauthorized data. This is always a risk regardless of your approach.

Compromise of __web_user: If the __web_user login itself is compromised, an attacker could potentially impersonate any application user, allowing them to perform actions within the bounds of those users' permissions. This is dramatically better —or rather, less-bad— than giving table-level access or full db_owner to your app user.

Potential for Information Leakage: Even without direct table access, vulnerabilities in stored procedures or views could still allow an attacker to extract sensitive data that the impersonated user has access to.

Auditing Complexity: While possible, make sure your auditing mechanisms clearly attribute actions to the originating __web_user and the impersonated AuthenticatedUser for effective forensics.

Alternatives and Why They Are Generally Worse

Individual Database Logins for Each Application User (uncommon)

This involves creating a separate server login (and corresponding database user) for each application user, and the application connects to the database using those specific credentials.

Why it's Worse:

    Credential Management and Exposure: This requires storing and retrieving individual user database credentials within the application layer (e.g., in session stores, configuration files), significantly increasing the risk of widespread credential compromise if the application server or its memory is breached.Scalability Issues: Managing thousands or millions of individual server logins becomes a substantial operational and administrative burden.Connection Overhead: Establishing and managing numerous individual database connections can lead to performance bottlenecks and resource exhaustion.

A Single Application Login with Full Database Access (typical)

The application connects as a single database user with extensive privileges (e.g., db_owner or even sysadmin). All authorization is then handled solely within the application code. This is a common paradigm, and "migrations" (DDL) are even handled by the application layer.

Why it's Worse:

    Single Point of Failure (Catastrophic): If the single database login is compromised, or a SQL injection is exploited, an attacker gains full control over the entire database, bypassing all application-level authorization and potentially leading to complete data loss or theft.Violation of Least Privilege: This directly contradicts the principle of least privilege, making the system highly vulnerable.Difficulty in Auditing: Attributing actions to specific application users becomes challenging as all actions appear to originate from the single privileged database login. You'd have to create your own audit trail mechanisms.

If you want a real life example of the implications of giving your applications full DML and DDL access:

MOVEit transfer data breaches Deep Dive | ORX News Deep Dive

Thousands of firms suffer data breaches via zero-day flaws in MOVEit file transfer software. Download this free ORX News Deep Dive for more information.

Implementation Example

Implementation time is where things get tricky. My first attempt at this was quite a headbanger (not the good kind) since it is an unorthodox way of building. There were a lot of concepts I had to shake off, such as the idea that "user" is now under the control of SQL Server, not my app. This actually led to a more philosophical understanding of auth systems and an app's relationship to the database.

For those who need to see, smell, and touch, let me provide a simplified implementation example. I've implemented this using SQL Server in the past, so I will be using TSQL syntax. I will use Python for the sake of brevity. Though I have more experience implementing similar systems in TypeScript in the past, python expresses itself in a less verbose manner, which is perfect since this is more about the workflow than the shape of data.

SQL Implementation

Create Application Login + App Users

-- Application login used by the web appCREATE LOGIN __web_user WITH PASSWORD = 'StrongPassword!123';CREATE USER __web_user FOR LOGIN __web_user;-- Application database users (no logins)CREATE USER alice WITHOUT LOGIN;CREATE USER bob WITHOUT LOGIN;-- Shared role for impersonated usersCREATE ROLE AuthenticatedUser;EXEC sp_addrolemember 'AuthenticatedUser', 'alice';EXEC sp_addrolemember 'AuthenticatedUser', 'bob';-- Grant impersonation rights only to __web_userGRANT IMPERSONATE ON USER::alice TO __web_user;GRANT IMPERSONATE ON USER::bob TO __web_user;

Create tables with sensitive data

-- Master customer tableCREATE TABLE Customer (    DbUserId INT PRIMARY KEY,  -- maps to USER_ID()    FirstName NVARCHAR(100),    LastName NVARCHAR(100));-- Credit cards (one-to-many per customer)CREATE TABLE CreditCard (    DbUserId INT,    Last4 CHAR(4),    Name NVARCHAR(100),    PRIMARY KEY (DbUserId, Last4),    FOREIGN KEY (DbUserId) REFERENCES Customer(DbUserId));-- Payments (many per card)CREATE TABLE Payment (    DbUserId INT,    CreditCard CHAR(4),    PaymentNo INT,    Amount MONEY,    PRIMARY KEY (DbUserId, CreditCard, PaymentNo),    FOREIGN KEY (DbUserId, CreditCard) REFERENCES CreditCard(DbUserId, Last4));

Create Views Filtered by Current User

-- View for authenticated users to see their credit cardsCREATE VIEW MyCreditCards_V ASSELECT DbUserId, Last4, NameFROM CreditCardWHERE DbUserId = USER_ID(); -- Scoped by current user-- View for authenticated users to see their paymentsCREATE VIEW MyPayments_V ASSELECT DbUserId, CreditCard, PaymentNo, AmountFROM PaymentWHERE DbUserId = USER_ID(); -- Scoped by current user-- Grant SELECT on views to authenticated usersGRANT SELECT ON MyCreditCards_V TO AuthenticatedUser;GRANT SELECT ON MyPayments_V TO AuthenticatedUser;

Create Stored Procedures

For the __web_user

-- Only __web_user can run thisCREATE PROCEDURE CreateCustomer_trx    @UserName NVARCHAR(128),    @FirstName NVARCHAR(100),    @LastName NVARCHAR(100)ASBEGIN    BEGIN TRANSACTION;        INSERT INTO Customer (DbUserId, FirstName, LastName)        VALUES (USER_ID(@UserName), @FirstName, @LastName);    COMMIT TRANSACTION;END;GRANT EXECUTE ON CreateCustomer_trx TO __web_user;

For AuthenticatedUser

-- Credit card addition - scoped by impersonated userCREATE PROCEDURE AddCreditCard_trx    @Last4 CHAR(4),    @Name NVARCHAR(100)ASBEGIN    IF EXISTS (SELECT 1 FROM CreditCard WHERE Last4 = @Last4 AND DbUserId = USER_ID())        RAISERROR('Credit card already exists', 16, 1);        GOTO EXIT_ERROR;        -- Insert credit card    BEGIN TRANSACTION;        INSERT INTO CreditCard (DbUserId, Last4, Name)        VALUES (USER_ID(), @Last4, @Name);    COMMIT TRANSACTION;        RETURN 0;    EXIT_ERROR:        RETURN 1;END;-- Payment processing - scoped by impersonated userCREATE PROCEDURE MakePayment_trx    @CreditCard CHAR(4),    @PaymentNo INT,    @Amount MONEYASBEGIN    IF NOT EXISTS (SELECT 1 FROM CreditCard WHERE Last4 = @CreditCard AND DbUserId = USER_ID())        RAISERROR('Credit card not found', 16, 1);        GOTO EXIT_ERROR;    BEGIN TRANSACTION;        INSERT INTO Payment (DbUserId, CreditCard, PaymentNo, Amount)        VALUES (USER_ID(), @CreditCard, @PaymentNo, @Amount);    COMMIT TRANSACTION;        RETURN 0;    EXIT_ERROR:        RETURN 1;END;-- Add permissionsGRANT EXECUTE ON AddCreditCard_trx TO AuthenticatedUser;GRANT EXECUTE ON MakePayment_trx TO AuthenticatedUser;

How to use this flow

In the app layer:

-- Step 1: Create new user via app-level loginEXEC CreateCustomer_trx @UserName = 'alice', @FirstName = 'Alice', @LastName = 'Jones';-- Step 2: Authenticated flowEXECUTE AS USER = 'alice';    EXEC AddCreditCard_trx @Last4 = '1234', @Name = 'Alice Visa';    EXEC MakePayment_trx @CreditCard = '1234', @PaymentNo = 1, @Amount = 49.99;REVERT;

Language-level implementation

At the language level, you take the extra steps to validate your data before it even gets to your database. I’ll demonstrate validation at the API class layer, assuming an RPC style class implementation. This follows a clean separation of concerns, where developers extract data-storage logic to be re-usable across different ports—HTTP, CLI, Workers, etc.

Because you don't know how your API will be used, it is best you validate both at the edge (e.g. user-facing layers) as well as within class methods.

# ---------------------------# Pydantic models# ---------------------------class CreateCustomerInput(BaseModel):    user_name: constr(max_length=128)    first_name: constr(min_length=1)    last_name: constr(min_length=1)class AddCreditCardInput(BaseModel):    user_name: constr(max_length=128)    last4: constr(min_length=4, max_length=4)    name: constr(min_length=1)class MakePaymentInput(BaseModel):    user_name: constr(max_length=128)    credit_card: constr(min_length=4, max_length=4)    payment_no: conint(ge=1)    amount: PositiveFloat# ---------------------------# ImpersonationDB API Class# ---------------------------class ImpersonationDB:    def __init__(self, conn_str: str):        self.conn_str = conn_str    def _get_conn(self):        return pyodbc.connect(self.conn_str, autocommit=False)    def create_customer(self, data: Union[CreateCustomerInput, Dict]):        validated = CreateCustomerInput.parse_obj(data)        with self._get_conn() as conn:            with conn.cursor() as cur:                cur.execute(                    "EXEC CreateCustomer_trx ?, ?, ?",                    validated.user_name, validated.first_name, validated.last_name                )            conn.commit()    def add_credit_card(self, data: Union[AddCreditCardInput, Dict]):        validated = AddCreditCardInput.parse_obj(data)        with self._get_conn() as conn:            with conn.cursor() as cur:                cur.execute("EXECUTE AS USER = ?", validated.user_name)                cur.execute("EXEC AddCreditCard_trx ?, ?", validated.last4, validated.name)                cur.execute("REVERT")            conn.commit()    def make_payment(self, data: Union[MakePaymentInput, Dict]):        validated = MakePaymentInput.parse_obj(data)        with self._get_conn() as conn:            with conn.cursor() as cur:                cur.execute("EXECUTE AS USER = ?", validated.user_name)                cur.execute(                    "EXEC MakePayment_trx ?, ?, ?",                    validated.credit_card, validated.payment_no, validated.amount                )                cur.execute("REVERT")            conn.commit()    def get_credit_cards(self, user_name: str) -> List[Dict]:        with self._get_conn() as conn:            with conn.cursor() as cur:                cur.execute("EXECUTE AS USER = ?", user_name)                cur.execute("SELECT DbUserId, Last4, Name FROM MyCreditCards_V")                rows = [dict(zip([col[0] for col in cur.description], row)) for row in cur.fetchall()]                cur.execute("REVERT")        return rows    def get_payments(self, user_name: str) -> List[Dict]:        with self._get_conn() as conn:            with conn.cursor() as cur:                cur.execute("EXECUTE AS USER = ?", user_name)                cur.execute("SELECT DbUserId, CreditCard, PaymentNo, Amount FROM MyPayments_V")                rows = [dict(zip([col[0] for col in cur.description], row)) for row in cur.fetchall()]                cur.execute("REVERT")        return rows

This is obviously verbose, but I hope it gets the point across. You can do some meta-programming to reduce repetition and increase consistency. For example, in TypeScript, I'll take an object and convert it to EXEC MyProc @KeyName=?, @OtherKey=? with a meta utility. Then another utility that wraps IMPERSONATE and REVERT queries like above, so the function gets reduced and the need to call cur.execute("EXECUTE AS USER = ?", user_name) is abstracted away.

You would extract user_name from your HTTP session, JWT token, or job data. Obviously, depending on the scenario, you would create different logins for different apps.

You're probably asking yourself:

Wait a minute... If __web_user cannot create users, then how would I get my web app to actually create users? Wouldn't giving that level of access to my app user defeat the purpose?

Great question. This is where background workers come into play. Where your users are created doesn't matter—as long as it's away from your public-facing apps.

For security-sensitive tasks, offset the work to a privileged worker

First, your users need a way to sign up

CREATE PROCEDURE SignUp_trx    @UserName NVARCHAR(128)ASBEGIN    IF USER_ID(@UserName) IS NOT NULL      RAISERROR('User already exists', 16, 1);      GOTO EXIT_ERROR;        BEGIN TRANSACTION;        INSERT INTO AuthEvent (Type, DbUserName, RoleName, Status)        VALUES ('CreateUser', @UserName, NULL, 'pending');    COMMIT TRANSACTION;        RETURN 0;    EXIT_ERROR:        RETURN 1;END;-- Give your web user the ability to sign up usersGRANT EXECUTE ON SignUp_trx TO __web_user;

Then, you'll need to create your privileged user to perform the necessary operations far away from the public.

-- Background privileged worker loginCREATE LOGIN __privileged_worker WITH PASSWORD = 'PrivilegedOnly!456';CREATE USER __privileged_worker FOR LOGIN __privileged_worker;GRANT ALTER ANY LOGIN TO __privileged_worker;GRANT IMPERSONATE ANY LOGIN TO __privileged_worker WITH GRANT OPTION;

And create a background SQL queue to handle these requests:

-- AuthEvent TableCREATE TABLE AuthEvent (    Type NVARCHAR(50),              -- e.g. 'CreateUser', 'GrantRole', 'RevokeRole'    DbUserName NVARCHAR(128),    RoleName NVARCHAR(128),         -- Nullable for SignUp    Status NVARCHAR(50),            -- e.g. 'pending', 'completed', 'error'    EnqueuedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),    UpdatedAt DATETIME2 NULL,    PRIMARY KEY (EnqueuedAt, DbUserName));-- View for Worker PollingCREATE VIEW AuthEvent_V ASSELECT Type, DbUserName, RoleName, Status, EnqueuedAt, UpdatedAtFROM AuthEventWHERE Status = 'pending';-- Procedure to update the queueCREATE PROCEDURE ModifyAuthEvent_trx    @EnqueuedAt DATETIME2,    @LastUpdatedAt DATETIME2,    @DbUserName NVARCHAR(128),    @Status NVARCHAR(50)ASBEGIN    -- Validation block    IF NOT EXISTS (        SELECT 1         FROM AuthEvent         WHERE EnqueuedAt = @EnqueuedAt             AND DbUserName = @DbUserName            AND UpdatedAt = @LastUpdatedAt    )        RAISERROR('Auth event not found or modified elsewhere', 16, 1);        GOTO EXIT_ERROR;    BEGIN TRANSACTION;        UPDATE AuthEvent        SET Status = @Status,            UpdatedAt = SYSUTCDATETIME()        WHERE EnqueuedAt = @EnqueuedAt             AND DbUserName = @DbUserName            AND UpdatedAt = @LastUpdatedAt;        -- Success guarantees, scale guarantees        IF @@ROWCOUNT <> 1        BEGIN          RAISERROR('Auth event modified elsewhere', 16, 1)          ROLLBACK TRANSACTION;          GOTO EXIT_ERROR;        END        COMMIT TRANSACTION;        RETURN 0;    EXIT_ERROR:        RETURN 1;END;-- Privileged worker can read and act on the queueGRANT SELECT ON AuthEvent_V TO __privileged_worker;GRANT EXECUTE ON ModifyAuthEvent_trx TO __privileged_worker;

And now you can consume your SQL queue in a private subnet. This will scale horizontally thanks to the @LastUpdatedAt clause, guaranteeing that if 2 workers pick up a task, only the first one to modify will be successful, the 2nd will fail early.

class AuthWorker:    def __init__(self, db: ImpersonationDB):        self.db = db    def run(self):        while True:            event = self._fetch_next_event()            if not event:                time.sleep(1)                continue            logging.info(f"Found pending auth event: {event}")            try:                updated = self.db.modify_auth_event(                    enqueued_at=event["EnqueuedAt"],                    last_updated_at=event["UpdatedAt"],                    db_user_name=event["DbUserName"],                    new_status="processing"                )                if not updated:                    # Event was modified concurrently, skip silently                    continue                self._perform_action(event)                self.db.modify_auth_event(                    enqueued_at=event["EnqueuedAt"],                    last_updated_at=event["UpdatedAt"],                    db_user_name=event["DbUserName"],                    new_status="success"                )                logging.info(f"Auth event succeeded: {event}")            except Exception as e:                msg = str(e)                if "modified elsewhere" in msg.lower():                    # Skip silently — someone else picked it up                    continue                logging.exception(f"Error while processing auth event: {event}")                self.db.modify_auth_event(                    enqueued_at=event["EnqueuedAt"],                    last_updated_at=event["UpdatedAt"],                    db_user_name=event["DbUserName"],                    new_status="failed"                )    def _fetch_next_event(self) -> Optional[dict]:        rows = self.db.get_auth_events(limit=1)  # Expects SELECT * FROM AuthEvent_V        return rows[0] if rows else None    def _perform_action(self, event: dict):        kind = event["Type"]        user = event["DbUserName"]        role = event.get("RoleName")        if kind == "CreateUser":            self.db.create_user(user) # will also attach AuthenticatedUser role            self.db.grant_impersonation(user) # to __web_user        elif kind == "GrantRole":            self.db.add_user_to_role(user, role)        elif kind == "RevokeRole":            self.db.remove_user_from_role(user, role)        else:            raise ValueError(f"Unsupported AuthEvent type: {kind}")if __name__ == "__main__":    db = ImpersonationDB(CONN_STR)    worker = AuthWorker(db)    logging.info("Auth worker started. Polling for events...")    worker.run()

And your authentication flow would execute the signUp function and wait for the user to be made:

class ImpersonationDB:    # ...    def wait_for_user_creation(self, username: str, max_secs: int = 10) -> bool:        """        Polls USER_ID(username) every second for up to max_secs.        Returns True if the user exists in the database.        """        deadline = time.time() + max_secs            while time.time() < deadline:            with self._get_conn() as conn:                with conn.cursor() as cur:                    cur.execute("SELECT USER_ID(?)", username)                    result = cur.fetchone()                    if result and result[0] is not None:                        return True            time.sleep(1)            return False

Though the above is oversimplified, it works well and is surprisingly secure.

What else do you get when you make full use of the server's features?

    A dramatic reduction in logic at the language level means there's a lot less to implementNo need to implement language-level RBAC structuresRelational permissioning and scoping (using USER_ID() in views and procs)Data guarantees because you can validate against your DBReduced network tripsAudit TrailsRelational queuesScoped views (no more map, filter, reduce)

Security-Focused Code Review Checklist

To further mitigate risks associated with impersonation-based flow, integrate this checklist into your code review process, focusing on your stored procedures and views:

Input Validation

    Verify that all external inputs to stored procedures and views (parameters, external data) are validated for type, length, format, range, and allowed characters at the strictest possible point (UI, route, language-level API, and critically, within the stored procedure itself).Confirm that regular expressions used for validation are thoroughly tested for potential bypasses or vulnerabilities.Check for the use of Allow Lists (whitelisting) for input validation wherever possible, as they are inherently more secure than Block Lists (blacklisting).

SQL Injection Prevention

    Parameterized Queries: Ensure that all dynamically generated SQL within stored procedures uses parameterized queries (e.g., sp_executesql) for data values.Identifier Escaping: Verify that QUOTENAME() is correctly applied to all dynamically generated object names (table names, column names, etc.) to prevent structural injection attacks.Disallowed Characters: Review inputs for the presence of dangerous characters like ;'--/**/, and xp_ unless explicitly required and carefully handled.Contextual Escaping: Check that data is properly escaped based on its context when being passed to SQL, LDAP, OS, or third-party commands.

Stored Procedure Logic

    Least Privilege Principle: Scrutinize the permissions granted to the AuthenticatedUser role on stored procedures and views. Ensure they are the absolute minimum required.Logic Flaws: Look for potential logical flaws within stored procedures that, even with valid inputs, could lead to unintended consequences or data manipulation outside the expected scope.Error Handling: Ensure that stored procedures handle errors gracefully and do not expose sensitive information in error messages (scrub error outputs).

Impersonation and Permissions

    IMPERSONATE Scope: Verify that the IMPERSONATE permission on __web_user is restricted only to the necessary application database users (not sa or other high-privileged accounts).Role Assignments: Regularly audit the membership of the AuthenticatedUser role and other database roles.

Auditing and Logging

    Comprehensive Logging: Confirm that database activity monitoring captures both the __web_user login and the impersonated AuthenticatedUser for all relevant actions, especially data modifications and access to sensitive information.Alerting: Ensure that alerts are configured for suspicious activities, failed impersonation attempts, or unusual database access patterns.Secure Development Practices:
      Separation of Concerns: Maintain a clear separation between data and commands, and data and client-side scripts.Continuous Review: Integrate secure code reviews as a regular part of your development lifecycle, not just before release.

Conclusion

This isn’t just a matter of “good security hygiene.” database-level user access controls give you real benefits—especially when working with mature platforms like SQL Server, IBM DB2, or SAP HANA. By deferring to the database’s built-in mechanisms for authentication and authorization, you get two big wins: reduced risk and reduced application complexity.

You don’t have to reinvent role-based access control in your app layer, nor do you have to rent it. You don’t have to manually filter direct table access. Instead, you can lean on the database itself—creating views filtered by the current user, assigning privileges at the right level, and letting the engine enforce boundaries for you.

It’s simpler. It’s safer. It scales better than rolling your own—or worse, outsourcing it to a third-party auth provider that might get breached anyway.

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

数据库安全 Web应用安全 最小权限原则 数据库访问控制 SQL Server Impersonation
相关文章