博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
IdentityServer4同时使用多个GrantType进行授权和IdentityModel.Client部分源码解析
阅读量:5328 次
发布时间:2019-06-14

本文共 8385 字,大约阅读时间需要 27 分钟。

首先,介绍一下问题。

由于项目中用户分了三个角色:管理员、代理、会员。其中,代理又分为一级代理、二级代理等,会员也可以相互之间进行推荐。

将用户表分为了两个,管理员和代理都属于后台,在同一张表,会员单独属于一张表。(别问我为什么不在同一张表按类型区分,俺不知道,俺也不敢问。我只是进去用新架构进行重新开发,基于原有的数据库。。)

同时后台账户不能请求会员的接口,会员也不能请求后台的接口。 他们是相互独立的两个服务。

因为要做成前后端分离,所以采用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
()//后台账户登录 .AddExtensionGrantValidator
()//会员账户登录 //.AddAppAuthRedirectUriValidator
() .AddProfileService
();

使用不同的模式,不同的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
(operatorT); if (result.Result == ResultType.Success) { var user = JsonConvert.DeserializeObject
(result.Data.ToString()); List
list = new List
(); 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
{ new Secret("secret".Sha256()) }, AllowedGrantTypes= new List
{ "customuserservice" }, AccessTokenType= AccessTokenType.Jwt, RequireConsent=false, AccessTokenLifetime=900, AllowOfflineAccess=true, AlwaysIncludeUserClaimsInIdToken=true, AbsoluteRefreshTokenLifetime=86400, 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
(); 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
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");

自此就完成了。

 

转载于:https://www.cnblogs.com/FateHuli/p/10943289.html

你可能感兴趣的文章
京东静态网页练习记录
查看>>
Filebeat Config 参数详解:
查看>>
使用DBCP连接池对连接进行管理
查看>>
【洛谷】【堆+模拟】P2278 操作系统
查看>>
hdu3307 欧拉函数
查看>>
Spring Bean InitializingBean和DisposableBean实例
查看>>
Solr4.8.0源码分析(5)之查询流程分析总述
查看>>
[Windows Server]安装系统显示“缺少计算机所需的介质驱动程序”解决方案
查看>>
[容斥][dp][快速幂] Jzoj P5862 孤独
查看>>
Lucene 学习之二:数值类型的索引和范围查询分析
查看>>
软件开发工作模型
查看>>
20165301 2017-2018-2 《Java程序设计》第九周学习总结
查看>>
Java基础之字符串匹配大全
查看>>
SGA和PGA的分配原则及更改大小
查看>>
面向对象
查看>>
lintcode83- Single Number II- midium
查看>>
移动端 响应式、自适应、适配 实现方法分析(和其他基础知识拓展)
查看>>
selenium-窗口切换
查看>>
selenium-滚动
查看>>
read from and write to file
查看>>