您的位置:首页 > 路由器知识路由器知识

2024BlazorServer权限控制完全指南:从入门到实战避坑(5000字超详细)

2026-02-17人已围观

2024 Blazor Server权限控制完全指南:从入门到实战避坑(5000字超详细)

大家好啊!今天咱们接着聊Blazor Server开发的第五期内容。之前有小伙伴反馈说,用微软自带的身份验证标签总出问题,在最新版的.NET Core里简直就是个"报错制造机"。别担心,今天咱们就彻底抛弃那些不灵活的方式,手把手教你用"全 DIY 模式"实现权限验证——就像给自己的房子装了套自定义门禁系统,想开哪扇门,全由你说了算!

为什么要自己动手搞权限控制?

微软自带的身份验证标签(就是那些带`[Authorize]`的玩意儿)就像小区统一安装的门禁,虽然能用但不够灵活。比如你想让张三要进101室,李四只能进202室,这套系统就很难搞定。而微软后来推荐的"策略授权"呢?又有点像让你填一堆表格申请开门,对咱们小项目来说还是太麻烦。

举个生活例子:这就好比你去饭店吃饭,自带标签的验证方式是"会员才能进",策略授权是"VIP会员才能坐靠窗位置",而咱们今天要做的是"张三只能进包间A,李四只能进大厅,王五能进所有地方"——完全定制化的权限管理!

OnNavigateAsync:路由跳转的"守门神"

在Blazor Server的`App.razor`文件里,有个叫`Router`的组件,它就像小区门口的保安亭。微软给这个保安亭留了个"特殊通道"——`OnNavigateAsync`方法,每次有人想换个页面(路由跳转),这个方法就会自动"醒过来"检查一下。咱们今天就把权限验证的"岗哨"设在这儿!

第一步:给Router组件装上"门禁系统"

打开你的`App.razor`,找到``标签,给它加上`OnNavigateAsync`属性,就像这样:

```razor

```

这个`OnNavigateAsync`就相当于给保安亭装了个新的安检设备,以后所有"访客"(页面跳转请求)都得先过这道关。

第二步:编写安检规则(实现OnNavigateAsync方法)

接下来咱们要写这个`OnNavigateAsync`方法具体怎么工作。先看完整代码,后面我会一句句解释:

```csharp

private async Task OnNavigateAsync(NavigationContext context)

{

// 白名单路径,这些页面不需要登录就能访问

var whiteListPaths = new List { "login", "register", "forgot-password" };

// 获取当前要访问的路径,比如"/counter"会变成"counter"

var targetPath = context.Path.TrimStart('/').ToLower();

// 1. 检查是否是白名单路径

if (whiteListPaths.Contains(targetPath))

{

// 白名单路径直接放行

return;

}

// 2. 检查用户是否已登录

var userSession = await _sessionService.GetUserSessionAsync();

if (userSession == null || string.IsNullOrEmpty(userSession.UserId))

{

// 未登录,跳转到登录页,同时记录当前想访问的页面

NavigationManager.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(context.Path)}");

return;

}

// 3. 检查用户是否有权限访问目标路径

var hasPermission = await _permissionService.CheckPathPermissionAsync(

userId: userSession.UserId,

path: targetPath,

roleId: userSession.RoleId // 注意:实际项目中不建议直接存RoleId,后面会讲原因

);

if (!hasPermission)

{

// 没有权限,跳转到无权限提示页

NavigationManager.NavigateTo("/access-denied");

return;

}

// 所有检查通过,正常跳转

}

```

这段代码到底在干什么?

咱们把它想象成去电影院看电影的流程:

1. 白名单检查:就像电影院的大厅永远开放,不需要门票就能进。这里的`login`、`register`等页面就是"大厅",谁都能看。

2. 登录检查:相当于检票口查票,没票(未登录)就不能进放映厅,得先去售票处(登录页)买票。

3. 权限检查:就像不同场次的电影需要不同的票,你买了《复仇者联盟》的票就不能进《蜘蛛侠》的放映厅。这里就是检查用户有没有访问目标页面的"票"(权限)。

关键细节:路径处理要小心!

这里有个特别容易踩坑的地方:路径的格式问题。比如用户访问`/Login`和`/login`其实是同一个页面,但代码区分大小写!所以咱们用`ToLower()`把路径统一转成小写。

另外,URL里的斜杠`/`也要处理干净。比如`/counter`要变成`counter`,不然白名单里写的`counter`就匹配不上了。这就像你快递地址写"北京市朝阳区"和"北京市 朝阳区"(多了个空格),快递员可能就找不到地方了。

用户会话管理:咱们的"会员卡系统"

上面代码里用到了`_sessionService.GetUserSessionAsync()`,这就像电影院的会员卡系统,记录着你的会员信息。在Blazor Server里,我们通常用`ISession`来存储这些信息,就像把会员卡信息存在电影院的系统里。

怎么存储用户会话信息?

登录成功后,我们可以把用户信息存到Session里:

```csharp

public async Task SetUserSessionAsync(UserSession session)

{

// 序列化成JSON字符串

var sessionJson = JsonSerializer.Serialize(session);

// 存到Session里,键名可以叫"UserSession"

_httpContextAccessor.HttpContext.Session.SetString("UserSession", sessionJson);

await Task.CompletedTask;

}

public async Task GetUserSessionAsync()

{

// 从Session里取出来

var sessionJson = _httpContextAccessor.HttpContext.Session.GetString("UserSession");

if (string.IsNullOrEmpty(sessionJson))

{

return null; // 没找到就是没登录

}

// 反序列化成对象

return JsonSerializer.Deserialize(sessionJson);

}

```

这里的`UserSession`类可以包含用户ID、用户名、角色ID等基本信息。注意:原文提到"实战中建议还是不要直接放RoleId进去",这是因为一个用户可能有多个角色(比如既是管理员又是普通用户),直接存RoleId太局限了。更好的做法是存用户ID,需要时再去数据库查该用户的所有角色。

权限检查:咱们的"权限数据库"

接下来是最核心的部分:怎么判断用户有没有访问某个页面的权限?这就需要一个"权限数据库",记录着哪个角色可以访问哪些路径。

数据库表设计建议

至少需要三张表:

1. Roles(角色表):比如"管理员"、"普通用户"、"游客"

2. Permissions(权限表):记录所有可访问的路径,比如"/dashboard"、"/users"

3. RolePermissions(角色权限关联表):记录哪个角色可以访问哪个路径

权限检查代码实现

```csharp

public async Task CheckPathPermissionAsync(string userId, string path, string roleId = null)

{

// 实际项目中应该先查缓存,这里为了演示直接查数据库

List userRoles;

if (string.IsNullOrEmpty(roleId))

{

// 如果没传roleId,就通过userId查该用户的所有角色

userRoles = await _dbContext.UserRoles

.Where(ur => ur.UserId == userId)

.Select(ur => ur.RoleId)

.ToListAsync();

}

else

{

// 如果传了roleId,就用这个roleId(简化版)

userRoles = new List { roleId };

}

// 查这些角色有哪些权限路径

var allowedPaths = await _dbContext.RolePermissions

.Where(rp => userRoles.Contains(rp.RoleId))

.Join(

_dbContext.Permissions,

rp => rp.PermissionId,

p => p.Id,

(rp, p) => p.Path.ToLower() // 路径统一转小写

)

.ToListAsync();

// 检查目标路径是否在允许的路径列表中

return allowedPaths.Contains(path);

}

```

重要性能提示:就像你不会每次进小区都让保安查户口本,我们也不应该每次页面跳转都查数据库。实际项目中一定要把权限数据缓存起来!可以用`IDistributedCache`或者内存缓存,用户登录时加载一次,退出时清除。

数据库初始化:给权限系统"录入信息"

咱们需要一些默认数据来测试权限系统,就像刚建好的电影院需要先录入电影场次信息。可以用EF Core的种子数据功能:

```csharp

modelBuilder.Entity().HasData(

new Permission { Id = 1, Name = "仪表盘访问权限", Path = "dashboard" },

new Permission { Id = 2, Name = "用户管理权限", Path = "users" },

new Permission { Id = 3, Name = "角色管理权限", Path = "roles" },

new Permission { Id = 4, Name = "普通页面访问权限", Path = "counter" }

);

modelBuilder.Entity().HasData(

new Role { Id = 1, Name = "管理员" },

new Role { Id = 2, Name = "普通用户" }

);

modelBuilder.Entity().HasData(

new RolePermission { RoleId = 1, PermissionId = 1 }, // 管理员有仪表盘权限

new RolePermission { RoleId = 1, PermissionId = 2 }, // 管理员有用户管理权限

new RolePermission { RoleId = 1, PermissionId = 3 }, // 管理员有角色管理权限

new RolePermission { RoleId = 1, PermissionId = 4 }, // 管理员有普通页面权限

new RolePermission { RoleId = 2, PermissionId = 1 }, // 普通用户有仪表盘权限

new RolePermission { RoleId = 2, PermissionId = 4 } // 普通用户有普通页面权限

);

```

如果之前已经建过数据库,记得先删除`Migrations`文件夹和数据库文件,然后重新执行`Add-Migration`和`Update-Database`命令。这就像你修改了电影院的场次安排,需要重新打印新的场次表。

新手避坑清单:这些错误千万别犯!

1. 路径大小写不统一:记住始终用`ToLower()`统一路径格式,不然`/Counter`和`/counter`会被当成两个不同页面。

2. Session配置问题:如果发现Session总是取不到值,检查`Program.cs`里有没有加`AddSession()`和`UseSession()`。这就像你办了会员卡但没激活,刷不了卡。

3. 直接在OnNavigateAsync里用同步代码:这个方法必须是异步的,里面所有数据库操作、网络请求都要加`await`。

4. 把敏感信息存在Session里:Session是存在服务器内存的,别存密码、Token这些敏感信息,只存用户ID、用户名这种基本信息。

5. 忘了处理returnUrl:用户未登录访问受保护页面时,要记住他本来想去哪,登录后自动跳过去。就像你在超市没带会员卡,结账时服务员会先帮你记账,等你拿来卡再一起结算。

6. 数据库查询没加缓存:每个页面跳转都查数据库,就像你每次进家门都让物业查一遍房产证,数据库压力会很大!

7. RoleId的局限性:一个用户通常有多个角色,直接存RoleId就像只给你一张电影票,却想看多部电影,肯定不行。

常见问题解决:遇到这些情况不用慌!

问题1:OnNavigateAsync方法不执行怎么办?

可能原因:

- 没在`Router`组件上正确设置`OnNavigateAsync`属性

- Blazor Server版本太旧(需要.NET 5+才支持这个方法)

- 页面跳转用的是`NavigationManager.NavigateTo`的重载方法,第二个参数设为`true`导致整页刷新

解决办法:

检查`App.razor`里的`Router`标签是否正确:

```razor

```

确保使用的是正常跳转:`NavigationManager.NavigateTo("/target")`(不带第二个参数或设为`false`)

问题2:Session总是为空怎么回事?

可能原因:

- 没在`Program.cs`中配置Session

- 用了`IIS Express`调试但没启用Session

- Blazor Server的`ServerPrerendering`导致的问题

解决办法:

在`Program.cs`中添加Session配置:

```csharp

var builder = WebApplication.CreateBuilder(args);

// 添加Session服务

builder.Services.AddSession(options =>

{

options.IdleTimeout = TimeSpan.FromMinutes(30);

options.Cookie.HttpOnly = true;

options.Cookie.IsEssential = true;

});

var app = builder.Build();

// 使用Session中间件(注意顺序,要放在UseRouting之后,UseEndpoints之前)

app.UseSession();

```

如果启用了预渲染,需要在`_Host.cshtml`中添加:

```html

```

问题3:权限检查总是返回false?

可能原因:

- 数据库里没有对应的权限数据

- 路径格式不匹配(比如数据库存的是"dashboard",实际传的是"/dashboard")

- 用户角色没关联正确的权限

解决办法:

1. 检查数据库的`Permissions`表和`RolePermissions`表是否有数据

2. 调试时输出`targetPath`和`allowedPaths`,看是否真的匹配

3. 确认用户登录后获取的角色ID是否正确

问题4:导航到登录页后,returnUrl参数是乱码?

解决办法:使用`Uri.EscapeDataString()`编码URL:

```csharp

NavigationManager.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(context.Path)}");

```

在登录页解析时用`Uri.UnescapeDataString()`解码:

```csharp

var returnUrl = Uri.UnescapeDataString(NavigationManager.GetQueryParameter("returnUrl"));

```

问题5:频繁访问数据库导致性能下降?

解决办法:实现权限缓存。这里提供一个简单的内存缓存实现:

```csharp

public class PermissionService : IPermissionService

{

private readonly IDistributedCache _cache;

private readonly ApplicationDbContext _dbContext;

private const string CacheKeyPrefix = "Perm_";

public PermissionService(IDistributedCache cache, ApplicationDbContext dbContext)

{

_cache = cache;

_dbContext = dbContext;

}

public async Task CheckPathPermissionAsync(string userId, string path)

{

var cacheKey = $"{CacheKeyPrefix}{userId}";

// 先查缓存

var cachedPermissions = await _cache.GetStringAsync(cacheKey);

List allowedPaths;

if (string.IsNullOrEmpty(cachedPermissions))

{

// 缓存没有,查数据库

allowedPaths = await GetPermissionsFromDatabase(userId);

// 存入缓存,设置30分钟过期

await _cache.SetStringAsync(

cacheKey,

JsonSerializer.Serialize(allowedPaths),

new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }

);

}

else

{

// 从缓存读取

allowedPaths = JsonSerializer.Deserialize>(cachedPermissions);

}

return allowedPaths.Contains(path.ToLower());

}

private async Task> GetPermissionsFromDatabase(string userId)

{

// 这里是原来的数据库查询逻辑

return await _dbContext.UserRoles

.Where(ur => ur.UserId == userId)

.Join(

_dbContext.RolePermissions,

ur => ur.RoleId,

rp => rp.RoleId,

(ur, rp) => rp.PermissionId

)

.Join(

_dbContext.Permissions,

pid => pid,

p => p.Id,

(pid, p) => p.Path.ToLower()

)

.ToListAsync();

}

}

```

10个实用小技巧:让你的权限系统更上一层楼

1. 动态权限刷新:用户权限变更后,主动清除缓存,避免用户需要重新登录才能生效。

2. 通配符路径匹配:支持`/users/`这样的路径格式,表示允许访问`/users`下的所有子页面,就像电影院的"通票"。

3. 权限粒度控制:不仅控制"能不能访问页面",还能控制"能不能点击按钮"、"能不能看到某个表格列"。

4. 权限日志:记录谁访问了什么页面,什么时候被拒绝访问,方便排查问题和审计。

5. 默认权限:给新注册用户自动分配"普通用户"角色,避免新用户什么都访问不了。

6. 权限组管理:把多个权限打包成"权限组",比如"用户管理组"包含"查看用户"、"添加用户"、"编辑用户"等权限,方便批量分配。

7. 前端菜单动态生成:根据用户权限自动生成导航菜单,没有权限的菜单项直接不显示。

8. AJAX请求也需要权限验证:别只在页面跳转时验证,API接口也要加权限检查,防止有人直接调用API。

9. 角色继承:比如"超级管理员"自动拥有"管理员"的所有权限,不用重复设置。

10. 定期权限审计:定期检查用户实际拥有的权限和应该拥有的权限是否一致,避免权限"泄露"。

长期使用体验分享

用这种自定义权限控制方式已经快两年了,最大的感受就是"自由"!想怎么控制权限就怎么控制,完全不用迁就框架的限制。不过也有几点心得:

1. 缓存是必须的:一开始没做缓存,数据库压力大到吓人,加了缓存后性能立刻上去了。

2. 权限设计要提前规划:一开始图省事把权限设计得很简单,后来用户多了、角色复杂了,改起来特别麻烦。建议一开始就考虑多角色、细粒度权限。

3. 前端也要做控制:就算后端控制了权限,前端也要隐藏没权限的菜单和按钮,不然用户看着能点却点不了,体验很差。

4. 测试要全面:新权限上线前,一定要测试各种角色的访问情况,特别是"边界情况"——比如一个用户同时有多个角色时权限会不会冲突。

话说回来,Blazor Server的这种权限控制方式虽然需要自己写更多代码,但换来的灵活性是完全值得的。就像自己动手装修房子,虽然麻烦点,但最终能装出完全符合自己需求的家。希望这篇教程能帮你摆脱那些不灵活的权限控制方式,打造出真正属于自己的权限系统!

你在Blazor开发中还遇到过哪些权限相关的问题?欢迎在评论区交流讨论!

随机图文