刷新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过滤失效。
排查步骤
- 验证静态类值的正确性:在
RefreshTokenAsync方法末尾、DoExecuteSQL方法中添加日志,输出LoggedInUser.UserId,确认刷新Token后该值是否被正确更新。 - 替换静态类为请求级存储:将
LoggedInUser静态类替换为HttpContext.Items或Scoped依赖注入服务存储用户ID,从JWT的ClaimTypes.NameIdentifier中直接提取用户ID,避免静态类的线程安全问题。 - 检查Azure连接池行为:Azure SQL的连接池可能复用连接,但代码中每次查询都新建连接,重点需确认用户ID是否在每次查询时都正确传入
sp_set_session_context。 - 验证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
相关产品推荐
相关产品推荐

