ASP.NET Core 6 使用IdentityServer实现简单的Policy与Role授权
.Net的OpenId Connect和OAuth 2.0协议实现的框架最新版是Duende.IdentityServer,用于公司要收费(最便宜1500USD一年可以授权管理5个客户端,每增加一个客户端就要加300USD),用于个人免费(年收入低于100W美元的公司也可以申请免费使用)。这里我们为了省去一些麻烦就使用之前的免费版本(IdentityServer4)来做演示,两者的代码差异不大,你用IdentityServer4也可以升级为Duende.IdentityServer,升级参考链接IdentityServer4 v4.1 to Duende IdentityServer v6。
首先创建一个Web项目,我这里创建一个空的Web项目,项目名称为IdentityServer用于管理验证和授权。
接下来添加IdentityServer4的Nuget包,然后去GitHub把官方演示程序整个下载下来Github链接,我们把wwwroot、Quickstart、Views三个文件夹复制到我们的项目里,然后在改写一下Program文件的代码。
只下载需要的那三个文件夹的代码如下:
// 在IdentityServer项目所在文件夹打开PowerShell执行如下命令
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/main/getmain.ps1'))
这里多了一个Config文件,内容如下:
using IdentityServer4.Models;
namespace IdentityServer
{
public class Config
{
public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope>
{
};
public static IEnumerable<Client> Clients => new List<Client>
{
};
public static IEnumerable<IdentityResource> IdentityResources => new List<IdentityResource>
{
};
}
}
接下来我们再添加一个webApp项目并添加Microsoft.AspNetCore.Authentication.OpenIdConnect的Nuget包,改写Program文件代码如下:
配置项里的Authority就是需要跳转到OpenId Connect的授权服务地址,ClientId可以理解为用户名,Client Secret可以理解为密码。
我们在WebApp项目的HomeController打个[Authorize],然后浏览器打开WebApp的地址会跳转到IdentityServer项目的错误页面。
这个错误表示还没给IdentityServer项目配置Client,我们来配置一下,修改Config文件,代码如下:
using IdentityServer4;
using IdentityServer4.Models;
namespace IdentityServer
{
public class Config
{
public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope>
{
};
public static IEnumerable<Client> Clients => new List<Client>
{
new Client
{
ClientId = "webapp",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:44381/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44381/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
}
}
};
public static IEnumerable<IdentityResource> IdentityResources => new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
}
这时在浏览WebApp项目就会跳转到登录页面,用户名密码都是bob,登录后就会跳转到首页。bob这个用户是写死在TestUsers这个文件的可以自行修改。
接下来我们修改一下WebApp项目的Home/Index文件,修改后如下
@using Microsoft.AspNetCore.Authentication
@{
ViewData["Title"] = "Home Page";
}
<div style="word-wrap:break-word;">
<h2>Cliams</h2>
<dl>
@foreach (var claim in User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
<h2>Properties</h2>
<dl>
@foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
{
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
}
</dl>
</div>
在新建一个Account控制器,增加一个Action,退出登录用,代码如下
using Microsoft.AspNetCore.Mvc;
namespace Example.WebApp.Controllers
{
public class AccountController : Controller
{
public IActionResult Logout()
{
return SignOut("Cookies", "oidc");
}
}
}
在修改一下_Layout页面新增一个退出功能:
现在我们登录会看到WebApp首页多了一些数据,但是Claim有些名称显示的太长了看着不友好,我们修改代码调整一下:
在WebApp项目的Program文件添加JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()这句代码Claim就会显示短名称了。需要引用System.IdentityModel.Tokens.Jwt命名空间。
using System.IdentityModel.Tokens.Jwt;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
.......
最简单的示例代码已经写完了,现在的验证场景有些单一,假设现在有个报表页面,只能给中国用户展示,我们继续来改下代码,WebApp的Program文件改动如下
IdentityServer的Config文件改动如下
记得TestUsers里的bob用户要加上new Claim("country", "China"),另一个用户不用加,以便验证权限。
WebApp项目HomeController新增如下代码
[Authorize(Policy = "Area.China")]
public IActionResult Report()
{
return Content("<h1>很棒的业绩报表</h1>", "text/html; charset=utf-8");
}
AccountController新增
public IActionResult AccessDenied(string returnUrl)
{
return Content("<h1>您没有权限</h1>", "text/html; charset=utf-8");
}
现在我们测试一下授权策略是否生效,用bob用户登录后访问/Home/Report
用Alice用户登录访问/Home/Report
顺便说一下,如果要在页面使用类似[Authorize(Policy = "Area.China")]的方法判断权限需要用以下这种方式
var authorizationService = HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var isPass = authorizationService.AuthorizeAsync(User, "Area.China").Result.Succeeded;
好了,功能已经实现,使用起来还是非常灵活,这里授权除了可以用Policy模式也可以用原生的Role模式,只需要把Role Claim映射一下就可以用User.IsInRole("Admin")这种自带方法判断是否有权限访问某个功能了。
实现Role模式鉴权核心代码
改造一下WebApp项目的Home/Index文件,加几行代码
@if (User.IsInRole("Visitor"))
{
<h1>你只是个访客</h1>
}
@if(User.IsInRole("Admin"))
{
<h1>是普通管理员</h1>
}
@if (User.IsInRole("SuperAdmin"))
{
<h1>最吊的超级管理员</h1>
}
最后给bob用户增加两个Role Claim:new Claim("role","Admin"),new Claim("role","SuperAdmin")。给Alice用户增加一个new Claim("role","Visitor"),在config里加上相应配置
public static IEnumerable<Client> Clients => new List<Client>
{
new Client
{
ClientId = "webapp",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:44381/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44381/signout-callback-oidc" },
//RequireConsent = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"country",
"role"
}
}
};
public static IEnumerable<IdentityResource> IdentityResources => new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource("country", "国家", new List<string> { "country" }),
new IdentityResource("role", "角色", new List<string> { "role" })
};
用bob登录 Home/Index显示如下:
用alice登录
展示完毕。
PS:刚开始添加多个角色的时候用原生的User.IsInRole方法返回的结果始终是false,当时想用自定义扩展方法解决这个问题,有点不甘心,后来花了一个多小时解决了使用User.IsInRole返回false的问题。