一.前言

大家好,许久没有更新博客了,最近从重庆来到了成都,换了个工作环境,前面都比较忙没有什么时间,这次趁着清明假期有时间,又可以分享一些知识给大家。在QQ群里有许多人都问过IdentityServer4怎么用Role(角色)来控制权限呢?还有关于Claim这个是什么呢?下面我带大家一起来揭开它的神秘面纱!

二.Claim详解

我们用过IdentityServer4或者熟悉ASP.NET Core认证的都应该知道有Claim这个东西,Claim我们通过在线翻译有以下解释:

(1)百度翻译

(2)谷歌翻译

这里我理解为声明,我们每个用户都有多个Claim,每个Claim声明了用户的某个信息比如:Role=Admin,UserID=1000等等,这里Role,UserID每个都是用户的Claim,都是表示用户信息的单元 ,我们不妨把它称为用户信息单元

建议阅读杨总的Claim相关的解析 http://www.cnblogs.com/savorboard/p/aspnetcore-identity.html

三.测试环境中添加角色Claim

这里我们使用IdentityServer4的QuickStart中的第二个Demo:ResourceOwnerPassword来进行演示(代码地址放在文末),所以项目的创建配置就不在这里演示了。

这里我们需要自定义IdentityServer4(后文简称id4)的验证逻辑,然后在验证完毕之后,将我们自己需要的Claim加入验证结果。便可以向API资源服务进行传递。id4定义了IResourceOwnerPasswordValidator接口,我们实现这个接口就行了。

Id4为我们提供了非常方便的In-Memory测试支持,那我们在In-Memory测试中是否可以实现自定义添加角色Claim呢,答案当时是可以的。

1.首先我们需要在定义TestUser测试用户时,定义用户Claims属性,意思就是为我们的测试用户添加额外的身份信息单元,这里我们添加角色身份信息单元:

new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim>(){new Claim(JwtClaimTypes.Role,"superadmin") }
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password",
Claims = new List<Claim>(){new Claim(JwtClaimTypes.Role,"admin") }
}

JwtClaimTypes是一个静态类在IdentityModel程序集下,里面定义了我们的jwt token的一些常用的Claim,JwtClaimTypes.Role是一个常量字符串public const string Role = "role";如果JwtClaimTypes定义的Claim类型没有我们需要的,那我们直接写字符串即可。

2.分别启动 QuickstartIdentityServer、Api、ResourceOwnerClient 查看 运行结果:

可以看见我们定义的API资源通过HttpContext.User.Claims并没有获取到我们为测试用户添加的Role Claim,那是因为我们为API资源做配置。

3.配置API资源需要的Claim

在QuickstartIdentityServer项目下的Config类的GetApiResources做出如下修改:

public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
// new ApiResource("api1", "My API")
new ApiResource("api1", "My API",new List<string>(){JwtClaimTypes.Role})
};
}

我们添加了一个Role Claim,现在再次运行(需要重新QuickstartIdentityServer方可生效)查看结果。

可以看到,我们的API服务已经成功获取到了Role Claim。

这里有个疑问,为什么需要为APIResource配置Role Claim,我们的API Resource才能获取到呢,我们查看ApiResource的源码:

public ApiResource(string name, string displayName, IEnumerable<string> claimTypes)
{
if (name.IsMissing()) throw new ArgumentNullException(nameof(name)); Name = name;
DisplayName = displayName; Scopes.Add(new Scope(name, displayName)); if (!claimTypes.IsNullOrEmpty())
{
foreach (var type in claimTypes)
{
UserClaims.Add(type);
}
}
}

从上面的代码可以分析出,我们自定义的Claim添加到了一个名为UserClaims的属性中,查看这个属性:

/// <summary>
/// List of accociated user claims that should be included when this resource is requested.
/// </summary>
public ICollection<string> UserClaims { get; set; } = new HashSet<string>();

根据注释我们便知道了原因:请求此资源时应包含的相关用户身份单元信息列表。

四.通过角色控制API访问权限

我们在API项目下的IdentityController做出如下更改

[Route("[controller]")]

public class IdentityController : ControllerBase
{
[Authorize(Roles = "superadmin")]
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in HttpContext.User.Claims select new { c.Type, c.Value });
} [Authorize(Roles = "admin")]
[Route("{id}")]
[HttpGet]
public string Get(int id)
{
return id.ToString();
}
}

我们定义了两个API通过Authorize特性赋予了不同的权限(我们的测试用户只添加了一个角色,通过访问具有不同角色的API来验证是否能通过角色来控制)

我们在ResourceOwnerClient项目下,Program类最后添加如下代码:

response = await client.GetAsync("http://localhost:5001/identity/1");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
Console.WriteLine("没有权限访问 http://localhost:5001/identity/1");
}
else
{
var content = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(content);
}

这里我们请求第二个API的代码,正常情况应该会没有权限访问的(我们使用的用户只具有superadmin角色,而第二个API需要admin角色),运行一下:

可以看到提示我们第二个,无权访问,正常。

五.如何使用已有用户数据自定义Claim

我们前面的过程都是使用的TestUser来进行测试的,那么我们正式使用时肯定是使用自己定义的用户(从数据库中获取),这里我们可以实现IResourceOwnerPasswordValidator接口,来定义我们自己的验证逻辑。

/// <summary>
/// 自定义 Resource owner password 验证器
/// </summary>
public class CustomResourceOwnerPasswordValidator: IResourceOwnerPasswordValidator
{
/// <summary>
/// 这里为了演示我们还是使用TestUser作为数据源,
/// 正常使用此处应当传入一个 用户仓储 等可以从
/// 数据库或其他介质获取我们用户数据的对象
/// </summary>
private readonly TestUserStore _users;
private readonly ISystemClock _clock; public CustomResourceOwnerPasswordValidator(TestUserStore users, ISystemClock clock)
{
_users = users;
_clock = clock;
} /// <summary>
/// 验证
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
//此处使用context.UserName, context.Password 用户名和密码来与数据库的数据做校验
if (_users.ValidateCredentials(context.UserName, context.Password))
{
var user = _users.FindByUsername(context.UserName); //验证通过返回结果
//subjectId 为用户唯一标识 一般为用户id
//authenticationMethod 描述自定义授权类型的认证方法
//authTime 授权时间
//claims 需要返回的用户身份信息单元 此处应该根据我们从数据库读取到的用户信息 添加Claims 如果是从数据库中读取角色信息,那么我们应该在此处添加 此处只返回必要的Claim
context.Result = new GrantValidationResult(
user.SubjectId ?? throw new ArgumentException("Subject ID not set", nameof(user.SubjectId)),
OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime,
user.Claims);
}
else
{
//验证失败
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential");
}
return Task.CompletedTask;
}

在Startup类里配置一下我们自定义的验证器:

实现了IResourceOwnerPasswordValidator还不够,我们还需要实现IProfileService接口,他是专门用来装载我们需要的Claim信息的,比如在token创建期间和请求用户信息终结点是会调用它的GetProfileDataAsync方法来根据请求需要的Claim类型,来为我们装载信息,下面是一个简单实现:

这里特别说明一下:本节讲的是“如何使用已有用户数据自定义Claim”,实现 IResourceOwnerPasswordValidator 是为了对接已有的用户数据,然后才是实现 IProfileService 以添加自定义 claim,这两步共同完成的是 “使用已有用户数据自定义Claim”,并不是自定义 Claim 就非得把两个都实现。

public class CustomProfileService: IProfileService
{
/// <summary>
/// The logger
/// </summary>
protected readonly ILogger Logger; /// <summary>
/// The users
/// </summary>
protected readonly TestUserStore Users; /// <summary>
/// Initializes a new instance of the <see cref="TestUserProfileService"/> class.
/// </summary>
/// <param name="users">The users.</param>
/// <param name="logger">The logger.</param>
public CustomProfileService(TestUserStore users, ILogger<TestUserProfileService> logger)
{
Users = users;
Logger = logger;
} /// <summary>
/// 只要有关用户的身份信息单元被请求(例如在令牌创建期间或通过用户信息终点),就会调用此方法
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
public virtual Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.LogProfileRequest(Logger); //判断是否有请求Claim信息
if (context.RequestedClaimTypes.Any())
{
//根据用户唯一标识查找用户信息
var user = Users.FindBySubjectId(context.Subject.GetSubjectId());
if (user != null)
{
//调用此方法以后内部会进行过滤,只将用户请求的Claim加入到 context.IssuedClaims 集合中 这样我们的请求方便能正常获取到所需Claim context.AddRequestedClaims(user.Claims);
}
} context.LogIssuedClaims(Logger); return Task.CompletedTask;
} /// <summary>
/// 验证用户是否有效 例如:token创建或者验证
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
public virtual Task IsActiveAsync(IsActiveContext context)
{
Logger.LogDebug("IsActive called from: {caller}", context.Caller); var user = Users.FindBySubjectId(context.Subject.GetSubjectId());
context.IsActive = user?.IsActive == true; return Task.CompletedTask;
}

同样在Startup类里启用我们自定义的ProfileServiceAddProfileService<CustomProfileService>()

值得注意的是如果我们直接将用户的所有Claim加入 context.IssuedClaims集合,那么用户所有的Claim都将会无差别返回给请求方。比如默认情况下请求用户终结点(http://Identityserver4地址/connect/userinfo)只会返回sub(用户唯一标识)信息,如果我们在此处直接 context.IssuedClaims=User.Claims,那么所有Claim都将被返回,而不会根据请求的Claim来进行筛选,这样做虽然省事,但是损失了我们精确控制的能力,所以不推荐。

上述说明配图:

如果直接 context.IssuedClaims=User.Claims,那么返回结果如下:

         /// <summary>
/// 只要有关用户的身份信息单元被请求(例如在令牌创建期间或通过用户信息终点),就会调用此方法
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
public virtual Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = Users.FindBySubjectId(context.Subject.GetSubjectId());
if (user != null)
context.IssuedClaims .AddRange(user.Claims); return Task.CompletedTask;
}

用户的所有Claim都将被返回。这样降低了我们控制的能力,我们可以通过下面的方法来实现同样的效果,但却不会丢失控制的能力。

(1).自定义身份资源资源

身份资源的说明:身份资源也是数据,如用户ID,姓名或用户的电子邮件地址。 身份资源具有唯一的名称,您可以为其分配任意身份信息单元(比如姓名、性别、身份证号和有效期等都是身份证的身份信息单元)类型。 这些身份信息单元将被包含在用户的身份标识(Id Token)中。 客户端将使用scope参数来请求访问身份资源。

public static IEnumerable<IdentityResource> GetIdentityResourceResources()
{
var customProfile = new IdentityResource(
name: "custom.profile",
displayName: "Custom profile",
claimTypes: new[] { "role"}); return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
customProfile
};
}

(2).配置Scope

通过上面的代码,我们自定义了一个名为“customProfile“的身份资源,他包含了"role" Claim(可以包含多个Claim),然后我们还需要配置Scope,我们才能访问到:

new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" ,IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,"custom.profile"}
}

我们在Client对象的AllowedScopes属性里加入了我们刚刚定义的身份资源,下载访问用户信息终结点将会得到和上面一样的结果。

六. Client Claims

新增于2018.12.14

在定义 Client 资源的时候发现,Client也有一个Claims属性,根据注释得知,在此属性上设置的值将会被直接添加到AccessToken,代码如下:

new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes =
{
"api1", IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
Claims = new List<Claim>
{
new Claim(JwtClaimTypes.Role, "admin")
}
};

只用在客户端资源这里设置就行,其他地方不用设置,然后请求AccessToken就会被带入。

值得注意的是Client这里设置的Claims默认都会被带一个client_前缀。如果像前文一样使用 [Authorize(Roles ="admin")] 是行的,因为 [Authorize(Roles ="admin")] 使用的Claim是role而不是client_role

七.总结

写这篇文章,简单分析了一下相关的源码,如果因为有本文描述不清楚或者不明白的地方建议阅读一下源码,或者加下方QQ群在群内提问。如果我们的根据角色的权限认证没有生效,请检查是否正确获取到了角色的用户信息单元。我们需要接入已有用户体系,只需实现IProfileServiceIResourceOwnerPasswordValidator接口即可,并且在Startup配置Service时不再需要AddTestUsers,因为将使用我们自己的用户信息。

Demo地址:https://github.com/stulzq/IdentityServer4.Samples/tree/master/Practice/01_RoleAndClaim

IdentityServer4实战 - 基于角色的权限控制及Claim详解的更多相关文章

  1. webapi框架搭建-安全机制(四)-可配置的基于角色的权限控制

    webapi框架搭建系列博客 在上一篇的webapi框架搭建-安全机制(三)-简单的基于角色的权限控制,某个角色拥有哪些接口的权限是用硬编码的方式写在接口上的,如RBAuthorize(Roles = ...

  2. webapi框架搭建-安全机制(三)-简单的基于角色的权限控制

    webapi框架搭建系列博客 上一篇已经完成了“身份验证”,如果只是想简单的实现基于角色的权限管理,我们基本上不用写代码,微软已经提供了authorize特性,直接用就行. Authorize特性的使 ...

  3. 图文详解基于角色的权限控制模型RBAC

    我们开发一个系统,必然面临权限控制的问题,即不同的用户具有不同的访问.操作.数据权限.形成理论的权限控制模型有:自主访问控制(DAC: Discretionary Access Control).强制 ...

  4. RBAC: K8s基于角色的权限控制

    文章目录 RBAC: K8s基于角色的权限控制 ServiceAccount.Role.RoleBinding Step 1:创建一个ServiceAccount,指定namespace Step 2 ...

  5. Django 基于角色的权限控制

    有一种场景, 要求为用户赋予一个角色, 基于角色(比如后管理员,总编, 编辑), 用户拥有相应的权限(比如管理员拥有所有权限, 总编可以增删改查, 编辑只能增改, 有些页面的按钮也只有某些角色才能查看 ...

  6. thinkphp基于角色的权限控制详解

    一.什么是RBAC 基于角色的访问控制(Role-Based Access Control)作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注. 在RBAC中,权限与角色相关联,用户通 ...

  7. [Kubernetes]基于角色的权限控制之RBAC

    Kubernetes中有很多种内置的编排对象,此外还可以自定义API资源类型和控制器的编写方式.那么,我能不能自己写一个编排对象呢?答案是肯定的.而这,也正是Kubernetes项目最具吸引力的地方. ...

  8. 【Kubernetes】基于角色的权限控制:RBAC

    Kubernetes中所有的API对象,都保存在Etcd里,对这些API对象的操作,一定都是通过访问kube-apiserver实现的,原因是需要APIServer来做授权工作. 在Kubernete ...

  9. RBAC - 基于角色的权限控制

    ThinkPHP中关于RBAC使用详解 自己的源码下载:百度网盘,thinkPHP文件夹下,RBAC文件夹. 重要的是,权限信息的写入函数等.在源码中能找到,Modules/Amin/Common/c ...

随机推荐

  1. IDEA 使用tomcat7-maven-plugin

    使用了这个插件就不需要配置tomcat了,直接用maven去run就行 配置方法:pom里添加:(之所以用tomcat7是因为如果直接用依赖下载很难下载到tomcat8-maven-plugin,详情 ...

  2. Several ports (8005, 8080, 8009)被占用

    启动Tomcat服务器报错: Several ports (8005, 8080, 8009) required by Tomcat v5.5 Server at localhost are alre ...

  3. Global.asax 中校验Session

    Application 相关的 Application_Init:在每一个HttpApplication实例初始化的时候执行. Application_Disposed:在每一个HttpApplica ...

  4. Xamarin改变移动开发的五个理由

    企业开发者不能简单的抛弃现有的桌面和Web应用,然而又不得不忙着创建各种各样的应用,没有太多的预算来开发移动版本,尤其是原生版本. 采用Xamarin,C#开发人员可以使用一份基础代码创建桌面版和移动 ...

  5. ISAPI和CGI限制中没有ASP.NET v4.0

    [服务器搭建]ISAPI和CGI限制中没有ASP.NET v4.0解决方式: 1.确保安装IIS时确实安装了ASP.NET,如果没有的话,勾上重新装一下,一般出现404.2时这么干 2.如果你是先装了 ...

  6. 基于Avocado 的 QData MySQL自动化测试.md

    qdata-mysql 自动化测试概要设计 │   ├── 1. 依赖环境 │   │   ├ │   │   └───── │   ├── 2. 配置文件 │   │   ├ │   │   └── ...

  7. 微信小程序调接口常见问题解决方法

    第一次调接口时遇见的bug. 注意:接口的域名不能使用 IP 地址或 localhost,且不能带端口号: 微信小程序如何调接口? wx.request({ url: 'http://miniapp/ ...

  8. [C#].Net Core下全局自定义身份过滤器使用AllowAnonymous属性

    假设一种情况:项目中需要做认证和权限控制,而且需要权限才能访问的控制器要远多于可以匿名访问的(类似AO系统那样,登陆了才能用). 那在每个控制器上加一个 [Authorize] 是能解决问题,反正正我 ...

  9. Java异常机制简介

    什么是异常? 异常一般是指程序在编译期没有问题,但是在程序运行期出现的错误,一个程序会因为出现异常而终止运行,也就是我们常说的挂掉,在多线程下,异常只会影响所在的线程,对其他线程没有影响. Java异 ...

  10. 从分布式一致性到共识机制(一)Paxos算法

    从分布式系统的CAP理论出发,关注分布式一致性,以及区块链的共识问题及解决. 区块链首先是一个大规模分布式系统,共识问题本质就是分布式系统的一致性问题,但是又有很大的不同.工程开发中,认为系统中存在故 ...