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=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<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");

自此就完成了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM