Blazor Server Cookie认证下服务层安全获取当前登录用户方案
问题背景
在Blazor Server项目中使用Cookie认证,需要在HotelRoomService.cs的DeleteHotelRoomAsync方法中获取当前登录用户ID,写入数据库审计日志。遇到的问题:
AuthenticationStateProvider实例为空,无法直接使用- 使用
IHttpContextAccessor不符合微软官方建议,但能获取到用户ID
推荐解决方案
方案1:从Blazor组件层传递用户ID到服务层(最推荐)
Blazor组件可以正常通过AuthenticationStateProvider获取用户身份,在调用服务方法时直接传入用户ID,避免服务层依赖请求上下文,保持服务的独立性和可测试性。
组件端代码:
@inject AuthenticationStateProvider AuthStateProvider @inject HotelRoomService HotelRoomService // 调用删除方法的逻辑 private async Task HandleDelete(int roomId) { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var userId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!string.IsNullOrEmpty(userId)) { await HotelRoomService.DeleteHotelRoomAsync(roomId, userId); } }
服务层修改后的DeleteHotelRoomAsync方法:
public async Task<int> DeleteHotelRoomAsync(int roomId, string userId) { var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId); if (roomDetails == null) { return 0; } _dbContext.HotelRooms.Remove(roomDetails); // 写入审计日志 _dbContext.DbLog.Add(new DbLog { UserId = userId, RoomId = roomId, OperationTime = DateTime.UtcNow, OperationType = "DeleteRoom" }); return await _dbContext.SaveChangesAsync(); }
方案2:使用Scoped服务封装用户上下文
创建一个Scoped的用户上下文服务,在根组件(如MainLayout)中初始化用户信息,服务层通过注入该上下文获取用户ID。
1. 创建UserContext服务:
public class UserContext { public string UserId { get; set; } }
2. 注册服务:
在Program.cs中添加:
builder.Services.AddScoped<UserContext>();
3. 在根组件中初始化用户信息:
@inject AuthenticationStateProvider AuthStateProvider @inject UserContext UserContext @code { protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); UserContext.UserId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; } }
4. 服务层使用UserContext:
private readonly AppDbContext _dbContext; private readonly UserContext _userContext; public HotelRoomService(AppDbContext dbContext, UserContext userContext) { _dbContext = dbContext; _userContext = userContext; } public async Task<int> DeleteHotelRoomAsync(int roomId) { var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId); if (roomDetails == null) { return 0; } _dbContext.HotelRooms.Remove(roomDetails); // 使用UserContext中的用户ID写入日志 if (!string.IsNullOrEmpty(_userContext.UserId)) { _dbContext.DbLog.Add(new DbLog { UserId = _userContext.UserId, RoomId = roomId }); } return await _dbContext.SaveChangesAsync(); }
方案3:安全使用IHttpContextAccessor(不推荐但可行)
如果必须在服务层直接获取,需确保只在请求范围内使用,并正确校验用户身份和Claim,避免直接取第一个Claim的错误写法。
1. 注册IHttpContextAccessor:
builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped<IGetUserId, GetUserId>();
2. 实现IGetUserId接口:
public interface IGetUserId { string Get(); } public class GetUserId : IGetUserId { private readonly IHttpContextAccessor _contextAccessor; public GetUserId(IHttpContextAccessor contextAccessor) { _contextAccessor = contextAccessor; } public string Get() { var httpContext = _contextAccessor.HttpContext; if (httpContext == null || !httpContext.User.Identity.IsAuthenticated) { return null; // 或根据业务需求抛出未授权异常 } // 正确获取用户ID Claim,替换为你实际使用的Claim类型 return httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; } }
3. 服务层使用:
private readonly IGetUserId _getUserId; public HotelRoomService(AppDbContext dbContext, IGetUserId getUserId) { _dbContext = dbContext; _getUserId = getUserId; } public async Task<int> DeleteHotelRoomAsync(int roomId) { var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId); if (roomDetails == null) { return 0; } _dbContext.HotelRooms.Remove(roomDetails); var userId = _getUserId.Get(); if (!string.IsNullOrEmpty(userId)) { _dbContext.DbLog.Add(new DbLog { UserId = userId, RoomId = roomId }); } return await _dbContext.SaveChangesAsync(); }
方案对比
- 方案1:符合微软设计原则,服务层无请求上下文依赖,可测试性强,是最优选择。
- 方案2:适合需要在多个服务中复用用户信息的场景,需确保组件初始化逻辑覆盖所有用户路径。
- 方案3:仅适合特殊场景使用,需注意
HttpContext在Blazor Server的SignalR连接中可能不存在,存在线程安全风险。
内容的提问来源于stack exchange,提问作者Sadabadi
相关产品推荐
相关产品推荐

