首先,介紹一下問題。
由於項目中用戶分了三個角色:管理員、代理、會員。其中,代理又分為一級代理、二級代理等,會員也可以相互之間進行推薦。
將用戶表分為了兩個,管理員和代理都屬於后台,在同一張表,會員單獨屬於一張表。(別問我為什么不在同一張表按類型區分,俺不知道,俺也不敢問。我只是進去用新架構進行重新開發,基於原有的數據庫。。)
同時后台賬戶不能請求會員的接口,會員也不能請求后台的接口。 他們是相互獨立的兩個服務。
因為要做成前后端分離,所以采用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");
自此就完成了。