You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

刷新Token后,基于session_context的行级安全(RLS)失效问题排查

多租户RLS刷新Token后数据为空问题排查

问题背景与现象

维护多租户数据库,采用行级安全(RLS)过滤所有表数据,每次查询需通过sp_set_session_context设置用户ID上下文。API已实现JWT认证与刷新Token机制,所有请求均受保护。

问题表现:

  • 用户首次使用JWT请求API时,RLS正常生效,仅返回该用户专属数据;
  • 刷新Token后使用新Token请求,接口返回200状态,但结果为空;
  • JWT验证通过,推测是session_context未正确设置导致RLS过滤失效。

相关代码

1. .NET中设置session_context的代码

using (SqlCommand command = new SqlCommand("EXEC sp_set_session_context @key=N'UserId', @value=@UserId", conn))
{
    command.Parameters.AddWithValue("@UserId", LoggedInUser.UserId);
    command.ExecuteNonQuery();
}

2. 刷新Token相关代码

public async Task<AuthResult> RefreshTokenAsync(string refreshtoken)
{
    var refreshToken = await context.RefreshTokens.FirstOrDefaultAsync(r => r.Token == refreshtoken);
    // 刷新Token有效性校验(如过期检查等)
    var getUser = await userManager.FindByIdAsync(refreshToken.UserId);
    var getUserRole = await userManager.GetRolesAsync(getUser);
    var userSession = new UserSession(getUser.Id, getUser.Name, getUser.Email, getUserRole.First());
    context.RefreshTokens.Remove(refreshToken);
    return await GenerateJwtTokenAsync(userSession, deviceid);
}

public async Task<AuthResult> GenerateJwtTokenAsync(UserSession user)
{
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Name),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(ClaimTypes.NameIdentifier, user.Id),
        new Claim(ClaimTypes.Email, user.Email),
        new Claim(ClaimTypes.Role,user.Role)
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: config["Jwt:Issuer"],
        audience: config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.Now.AddMinutes(5),
        signingCredentials: creds);

    var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);

    var refreshToken = new RefreshToken
    {
        Token = Guid.NewGuid().ToString(),
        JwtId = token.Id,
        IsRevoked = false,
        UserId = user.Id,
        AddedDate = DateTime.UtcNow,
        ExpiryDate = DateTime.UtcNow.AddMonths(1)
    };

    await context.RefreshTokens.AddAsync(refreshToken);
    await context.SaveChangesAsync();

    return new AuthResult
    {
        Token = jwtToken,
        RefreshToken = refreshToken.Token,
        Success = true
    };
}

3. 登录时设置UserId的代码(仅此处设置)

private async Task<AuthResult> LoginAccountAsync(Login login)
{
    var getUser = await userManager.FindByEmailAsync(login.Email);

    var getUserRole = await userManager.GetRolesAsync(getUser);
    var userSession = new UserSession(getUser.Id, getUser.Name, getUser.Email, getUserRole.First());
    ApplicationUser getUser = response.User;

    bool checkUserPassword = await userManager.CheckPasswordAsync(getUser, login.Password);
    // 用户合法性校验
    LoggedInUser.UserId = getUser.Id; // 赋值给静态类LoggedInUser
    return await GenerateJwtTokenAsync(userSession);
}

4. 查询数据库的完整代码

private static DataTable DoExecuteSQL(SqlCommand cmd, bool loadtable)
{
    DataTable dt = new();

    using (SqlConnection conn = new(ConnectionString))
    {
        conn.Open();
        if (LoggedInUser.UserId != null)
        {
            using (SqlCommand command = new SqlCommand("EXEC sp_set_session_context @key=N'UserId', @value=@UserId", conn))
            {
                command.Parameters.AddWithValue("@UserId", LoggedInUser.UserId);
                command.ExecuteNonQuery();
            }
        }
        
        cmd.Connection = conn;
        try
        {
            SqlDataReader dr = cmd.ExecuteReader();
            if (loadtable)
            {
                dt.Load(dr);
            }
        }
        catch (SqlException ex)
        {
            string msg = ParseConstraintMsg(ex.Message);
            throw new Exception(msg);
        }
        catch (InvalidCastException ex)
        {
            throw new Exception(cmd.CommandText + ": " + ex.Message, ex);
        }
    }

    return dt;
}

调试情况

  • 查询后手动验证session_context值正确,直接在SQL中设置相同上下文可返回正确结果;
  • 本地测试数据库无此问题,仅Azure测试数据库出现异常。

问题定位与排查方案

核心问题推测

LoggedInUser是静态类,仅在登录时赋值。刷新Token流程中未更新该静态类的UserId,而ASP.NET Core多线程环境下,静态类实例会被不同请求共享,导致后续查询时使用的是旧的用户ID(甚至可能为空),最终引发RLS过滤失效。

排查步骤

  1. 验证静态类值的正确性:在RefreshTokenAsync方法末尾、DoExecuteSQL方法中添加日志,输出LoggedInUser.UserId,确认刷新Token后该值是否被正确更新。
  2. 替换静态类为请求级存储:将LoggedInUser静态类替换为HttpContext.Items或Scoped依赖注入服务存储用户ID,从JWT的ClaimTypes.NameIdentifier中直接提取用户ID,避免静态类的线程安全问题。
  3. 检查Azure连接池行为:Azure SQL的连接池可能复用连接,但代码中每次查询都新建连接,重点需确认用户ID是否在每次查询时都正确传入sp_set_session_context。
  4. 验证RLS策略执行:在Azure SQL中启用Query Store,查看业务查询的执行计划,确认RLS过滤条件是否被正确应用;或执行SELECT SESSION_CONTEXT(N'UserId')在查询前后验证上下文值。

在数据库中查看所有执行的查询

方法1:使用Azure SQL Query Store

在Azure Portal进入目标SQL数据库,打开「Query Performance Insight」,可查看最近执行的所有SQL语句,包括sp_set_session_context调用和业务查询,对比两者的UserId参数是否一致。

方法2:创建扩展事件会话捕获查询

-- 创建扩展事件会话
CREATE EVENT SESSION [CaptureAllQueries] ON SERVER 
ADD EVENT sqlserver.sql_statement_completed(
    ACTION(sqlserver.session_id, sqlserver.sql_text))
ADD TARGET package0.event_file(SET filename=N'CaptureAllQueries.xel')
WITH (STARTUP_STATE=OFF)
GO

-- 启动会话
ALTER EVENT SESSION [CaptureAllQueries] ON SERVER STATE=START

查询捕获到的SQL语句:

SELECT 
    event_data.value('(event/@name)[1]', 'varchar(50)') AS event_name,
    event_data.value('(event/@timestamp)[1]', 'datetime2') AS timestamp,
    event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS sql_text
FROM 
    (SELECT CAST(event_data AS XML) AS event_data
     FROM sys.fn_xe_file_target_read_file('CaptureAllQueries*.xel', NULL, NULL, NULL)) AS x

方法3:使用SSMS Profiler

通过SSMS连接到Azure SQL数据库,启动SQL Server Profiler,选择「T-SQL」事件类,捕获所有执行的SQL语句。

内容的提问来源于stack exchange,提问作者Shmiel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 02:24:53