IdentityServer4同时使用多个GrantType进行授权和IdentityModel.Client部分源码解析
首先,介绍一下问题。
由于项目中用户分了三个角色:管理员、代理、会员。其中,代理又分为一级代理、二级代理等,会员也可以相互之间进行推荐。
将用户表分为了两个,管理员和代理都属于后台,在同一张表,会员单独属于一张表。(别问我为什么不在同一张表按类型区分,俺不知道,俺也不敢问。我只是进去用新架构进行重新开发,基于原有的数据库。。)
同时后台账户不能请求会员的接口,会员也不能请求后台的接口。 他们是相互独立的两个服务。
因为要做成前后端分离,所以采用IdentityServer4进行接口授权。
oauth2 有四种授权模式:
- 密码模式(resource owner password credentials)
- 授权码模式(authorization code)
- 简化模式(implicit)
- 客户端模式(client credentials)
这篇重点不是介绍四种模式差异,有不清楚的请自行看相关资料。
我想到的有两种方案可以解决;
1.后台登陆和前台登陆都采用authorization code模式进行登陆,只是传参时加一个loginType来区分是会员还是后台账户,
在scopes里面定义所有的apiResource,当然因为登陆统一了,所以登陆时请求的scope也要根据loginType来区分(当然你也可以根据loginType来生成role角色权限,在不同的服务里面带上相应的role权限即可);
不然会员账户的access_token也可以请求后台,后台同时也可以请求会员的功能了。
这样在登陆的就能根据类型来判断应该查询哪张表。
但是这种一听就很绕,代码可读性差、后期维护难,假如突然又增加一个角色或者一张表呢。
不符合开放闭合原则。
2.就是增加新的授权模式,在IdentityServer4里面;
可以让我们使用自定义的授权码。这里我们可以好好利用了,
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryClients(MemoryConfigs.GetClients())
.AddInMemoryIdentityResources(MemoryConfigs.GetIdentityResources())
.AddInMemoryApiResources(MemoryConfigs.GetApiResources())
.AddResourceOwnerValidator<CustomPasswordOwnerUserServices>()//后台账户登录
.AddExtensionGrantValidator<CustomUserService>()//会员账户登录
//.AddAppAuthRedirectUriValidator<AuthorizationCodeService>()
.AddProfileService<CustomProfileService>();
使用不同的模式,不同的clientId,在请求时会自动进行相应的模式验证;下面是会员自定义的模式验证
public class CustomUserService : IExtensionGrantValidator
{
private readonly IHttpClientFactory _httpClientFactory; public CustomUserService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
} public string GrantType => "customuserservice"; public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var model = new userLoginDto
{
phoneNumber = context.Request.Raw["Phone"],
passWord = context.Request.Raw["PassWord"]
};
var client = _httpClientFactory.CreateClient("userApi");
var response = await client.PostAsJsonAsync("/api/userLogin/login", model);//调用服务接口进行密码验证
response.EnsureSuccessStatusCode();
if (response.IsSuccessStatusCode)
{
string operatorT = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<OperatorResult>(operatorT);
if (result.Result == ResultType.Success)
{
var user = JsonConvert.DeserializeObject<UserInfo>(result.Data.ToString());
List<Claim> list = new List<Claim>();
list.Add(new Claim("username", user.UserName ?? ""));
list.Add(new Claim("role", string.IsNullOrEmpty(user.Role) ? "" : user.Role));
list.Add(new Claim("realname", string.IsNullOrEmpty(user.RealName) ? "" : user.RealName));
list.Add(new Claim("company", string.IsNullOrEmpty(user.Company) ? "" : user.Company));
list.Add(new Claim("roleid", string.IsNullOrEmpty(user.RoleId) ? "" : user.RoleId));
context.Result = new GrantValidationResult(subject: user.Id.ToString(), authenticationMethod: GrantType, claims: list);
}
else
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, result.Message);
}
}
else
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "用户名密码错误"); await Task.CompletedTask;
}
}
下面是client配置
new Client(){
ClientId="userservices",
ClientName="用户服务",
ClientSecrets=new List<Secret> {
new Secret("secret".Sha256())
},
AllowedGrantTypes= new List<string>{
"customuserservice"
},
AccessTokenType= AccessTokenType.Jwt,
RequireConsent=false,
AccessTokenLifetime=,
AllowOfflineAccess=true,
AlwaysIncludeUserClaimsInIdToken=true,
AbsoluteRefreshTokenLifetime=,
AllowedScopes={
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.OfflineAccess,
"userservicesapi"
},
}
当然ApiResource也要添加
new ApiResource("userservicesapi","用户服务")
接下来,就是登录了
var client = _httpClientFactory.CreateClient();
var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = Configuration["AuthorityConfig"], Policy = new DiscoveryPolicy { RequireHttps = false } });
if (disco.IsError)
{
result.Message = disco.Error;
return Ok(result);
}
var formvalues = new Dictionary<string, string>();
formvalues.Add("scope", "profile openid offline_access userservicesapi");
formvalues.Add("Phone", loginDto.phoneNumber);
formvalues.Add("PassWord", loginDto.passWord);var content = new FormUrlEncodedContent(formvalues);
TokenRequest tokenRequest = new TokenRequest
{
GrantType = "customuserservice",
Address = disco.TokenEndpoint,
ClientId = "userservices",
ClientSecret = "secret",
Parameters = formvalues
};
var tokenResponse = await client.RequestTokenAsync(tokenRequest);//自定义的授权模式请求
这样基本就完成了。登录接口就不展示了,都是一些逻辑判断。
这只是会员的登陆,后台账户的登陆跟会员的类似。修改请求的clientId和scope就行了
可以发现全程没有loginType参数,即使后面要加,完全不需要修改源代码,只需要按需扩展即可。
后面,我们看看IdentityModel基于httpclient的扩展源码,以前的TokenClient已经被舍弃了。网上能找到完整的自定义grantType授权太少了。
拿密码模式举例
第一句的clone可以先不管,就是将参数重新组装;
中间4行代码增加的一些参数,就常见的GrantType,Scope和密码模式必须的UserName和Password;
然后调用client.RequestTokenAsync方法发起请求,
接下来看RequestTokenAsync方法;说明已经加在注释里面了
internal static async Task<TokenResponse> RequestTokenAsync(this HttpMessageInvoker client, Request request, CancellationToken cancellationToken = default)
{
if (!request.Parameters.TryGetValue(OidcConstants.TokenRequest.ClientId, out _))
{
if (request.ClientId.IsMissing())
{
throw new InvalidOperationException("client_id is missing");
}
} var httpRequest = new HttpRequestMessage(HttpMethod.Post, request.Address);//初始化post请求
httpRequest.Headers.Accept.Clear();
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); //根据style判断是在header里面添加client_id client_secret等参数 还是在body里面添加;默认是在body里面添加
ClientCredentialsHelper.PopulateClientCredentials(request, httpRequest);
//下面的就是常见的httpClient post请求
httpRequest.Content = new FormUrlEncodedContent(request.Parameters); HttpResponseMessage response;
try
{
response = await client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
return new TokenResponse(ex);
} string content = null;
if (response.Content != null)
{
content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
//直接通过JObject转化的json实例化成TokenResponse类
if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.BadRequest)
{
return new TokenResponse(content);
}
else
{
return new TokenResponse(response.StatusCode, response.ReasonPhrase, content);
}
}
可以发现其实就是简单的post请求,只是封装了参数而已。
然后看我们的自定义RequestTokenAsync源码
可以发现只加了一个grantType,当然client_id和client_secret都在TokenReques继承的基类Request里面了。
如果有scope的话,自定义的模式请求就需要自己添加参数
formvalues.Add("scope", "profile openid offline_access userservicesapi");
自此就完成了。
IdentityServer4同时使用多个GrantType进行授权和IdentityModel.Client部分源码解析的更多相关文章
- identityserver4源码解析_2_元数据接口
目录 identityserver4源码解析_1_项目结构 identityserver4源码解析_2_元数据接口 identityserver4源码解析_3_认证接口 identityserver4 ...
- identityserver4源码解析_3_认证接口
目录 identityserver4源码解析_1_项目结构 identityserver4源码解析_2_元数据接口 identityserver4源码解析_3_认证接口 identityserver4 ...
- IdentityServer4源码解析_4_令牌发放接口
目录 identityserver4源码解析_1_项目结构 identityserver4源码解析_2_元数据接口 identityserver4源码解析_3_认证接口 identityserver4 ...
- 简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析
简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析 虽然经常用 OAuth 2.0,但是原理却不曾了解,印象里觉得很简单,请求跳来跳去,今天看完相关介绍,就来捋一捋 ...
- IdentityServer4源码解析_1_项目结构
目录 IdentityServer4源码解析_1_项目结构 IdentityServer4源码解析_2_元数据接口 IdentityServer4源码解析_3_认证接口 IdentityServer4 ...
- IdentityServer4源码解析_5_查询用户信息接口
协议简析 UserInfo接口是OAuth2.0中规定的需要认证访问的接口,可以返回认证用户的声明信息.请求UserInfo接口需要使用通行令牌.响应报文通常是json数据格式,包含了一组claim键 ...
- 【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于IdentityServer ...
- OAuth2.0授权和SSO授权
一. OAuth2.0授权和SSO授 1. OAuth2.0 --> 网页 --> 当前程序内授权 --> 输入账号密码 --> (自己需要获取到令牌, 自己处理逻辑) 授权成 ...
- ASP.NET MVC - 安全、身份认证、角色授权和ASP.NET Identity
ASP.NET MVC - 安全.身份认证.角色授权和ASP.NET Identity ASP.NET MVC内置的认证特性 AuthorizeAttribute特性(System.Web.Mvc)( ...
随机推荐
- redis不支持多个数据库实例但是支持多个字典
Redis多个数据库 注意:Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念. Redis是一个字典结构的存储服务器,而实际上一个Redi ...
- java匿名内部类new(){}
匿名内部类:顾名思义,没有名字的内部类.表面上看起来它们似乎有名字,实际那不是它们的名字.当程序中使用匿名内部类时,在定义匿名内部类的地方往往直接创建该类的一个对象.匿名内部类的声明格式如下:new ...
- 阶段5 3.微服务项目【学成在线】_day09 课程预览 Eureka Feign_04-Eureka注册中心-将服务注册到Eureka Server
cms相当于客户端 配置客户端的信息 后面加逗号分隔开 50102表示向两台eureka服务上报服务,如果有一台死掉了 那么还可以上另外的一台去注册服务 直接把ip注册到eureka 启动类加注解 重 ...
- 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_19-认证接口开发-接口开发-controller
补充controller方法 判断是否有值传过来. 私有方法存储cookie httpOnly设置为false的话 浏览器就拿到这个cookie 拿到Response cookie在配置文件内的配置 ...
- PAT 甲级 1046 Shortest Distance (20 分)(前缀和,想了一会儿)
1046 Shortest Distance (20 分) The task is really simple: given N exits on a highway which forms a ...
- [LeetCode] 167. Fraction to Recurring Decimal 分数转循环小数
Given two integers representing the numerator and denominator of a fraction, return the fraction in ...
- iOS-宏定义
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnoAAAPCCAYAAADvRHWgAAAAAXNSR0IArs4c6QAAAZ1pVFh0WE1MOm ...
- Spring 本地事务
主要原理: 事务的顶层接口: PlatformTransactionManager提供了3个方法,在申明式事务中这3个方法都是通过aop 调用的: 1.getTransaction: 进入相关需要被拦 ...
- 使用JavaScript实现量化策略并发执行
本文代码和文章发在FMZ发明者比特币量化交易平台上: 使用JavaScript实现量化策略并发执行--封装Go函数 - 发明者量化 https://www.fmz.com/digest-topic/3 ...
- 三节课MINI计划第三周
第一部分 微信读书的产品分析 一.任务背景 二.做什么 三.TASK8 业务流程图 四.TASK9 五.周会 六.周报