源碼下載地址:下載html
項目結構以下圖:redis
在Identity Server受權中,實現IResourceOwnerPasswordValidator接口:算法
public class IdentityValidator : IResourceOwnerPasswordValidator { private readonly UserManager<ApplicationUser> _userManager; private readonly IHttpContextAccessor _httpContextAccessor; public IdentityValidator( UserManager<ApplicationUser> userManager, IHttpContextAccessor httpContextAccessor) { _userManager = userManager; _httpContextAccessor = httpContextAccessor; } public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { string userName = context.UserName; string password = context.Password; var user = await _userManager.FindByNameAsync(userName); if (user == null) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidClient, "用戶不存在!"); return; } var checkResult = await _userManager.CheckPasswordAsync(user, password); if (checkResult) { context.Result = new GrantValidationResult( subject: user.Id, authenticationMethod: "custom", claims: _httpContextAccessor.HttpContext.User.Claims); } else { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "無效的客戶身份!"); } } }
單頁面應用中,使用implicit的受權模式,需添加oidc-client.js,調用API的關鍵代碼:數據庫
var config = { authority: "http://localhost:5000/", client_id: "JsClient", redirect_uri: "http://localhost:5500/callback.html", response_type: "id_token token", scope:"openid profile UserApi", post_logout_redirect_uri: "http://localhost:5500/index.html", }; var mgr = new Oidc.UserManager(config); mgr.getUser().then(function (user) { if (user) { log("User logged in", user.profile); } else { log("User not logged in"); } }); function login() { mgr.signinRedirect(); } //api調用以前需登陸 function api() { mgr.getUser().then(function (user) { if (user == null || user == undefined) { login(); } var url = "http://localhost:9000/api/Values"; var xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = function () { log(xhr.status, JSON.parse(xhr.responseText)); alert(xhr.responseText); } xhr.setRequestHeader("Authorization", "Bearer " + user.access_token); xhr.setRequestHeader("sub", user.profile.sub);//這裏拿到的是用戶ID,傳給API端進行角色權限驗證 xhr.send(); }); } function logout() { mgr.signoutRedirect(); }
統一網關經過Ocelot實現,添加Ocelot.json文件,並修改Program.cs文件:json
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, builder) => { builder .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) .AddJsonFile("Ocelot.json"); }) .UseUrls("http://+:9000") .UseStartup<Startup>() .Build();
StartUp.cs文件修改以下:api
public void ConfigureServices(IServiceCollection services) { services.AddOcelot(); var authenticationProviderKey = "qka_api"; services.AddAuthentication("Bearer") .AddIdentityServerAuthentication(authenticationProviderKey, options => { options.Authority = "http://localhost:5000"; options.RequireHttpsMetadata = false; options.ApiName = "UserApi"; }); services.AddCors(options => { options.AddPolicy("default", policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseCors("default"); app.UseOcelot().Wait(); }
Ocelot.js配置文件以下:緩存
{ "ReRoutes": [ { "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "ServiceName": "userapi", //consul中的userapi的service名稱 "LoadBalancer": "RoundRobin", //負載均衡算法 "UseServiceDiscovery": true, //啓用服務發現 "UpstreamPathTemplate": "/{url}", "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ], "AuthenticationOptions": { "AuthenticationProviderKey": "qka_api", "AllowedScopes": [] } }, { "DownstreamPathTemplate": "/{url}", "DownstreamScheme": "http", "ServiceName": "identityserverapi", //consul中的userapi的service名稱 "LoadBalancer": "RoundRobin", //負載均衡算法 "UseServiceDiscovery": true, //啓用服務發現 "UpstreamPathTemplate": "/{url}", "UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ], } ], "GlobalConfiguration": { "BaseUrl": "http://localhost:9000", "ServiceDiscoveryProvider": { "Host": "192.168.2.144",//consul的地址 "Port": 8500//consul的端口 } } }
asp.net core自帶的基於角色受權須要像下圖那樣寫死角色的名稱,當角色權限發生變化時,須要修改並從新發布站點,很不方便。app
因此我自定義了一個filter,實現角色受權驗證:負載均衡
public class UserPermissionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (context.Filters.Any(item => item is IAllowAnonymousFilter)) { return; } if (!(context.ActionDescriptor is ControllerActionDescriptor)) { return; } var attributeList = new List<object>(); attributeList.AddRange((context.ActionDescriptor as ControllerActionDescriptor).MethodInfo.GetCustomAttributes(true)); attributeList.AddRange((context.ActionDescriptor as ControllerActionDescriptor).MethodInfo.DeclaringType.GetCustomAttributes(true)); var authorizeAttributes = attributeList.OfType<UserPermissionFilterAttribute>().ToList(); if (!authorizeAttributes.Any()) { return; } var sub = context.HttpContext.Request.Headers["sub"]; string path = context.HttpContext.Request.Path.Value.ToLower(); string httpMethod = context.HttpContext.Request.Method.ToLower(); /*todo: 從數據庫中根據role獲取權限是否具備訪問當前path和method的權限, 因調用頻繁, 可考慮將角色權限緩存到redis中*/ bool isAuthorized = true; if (!isAuthorized) { context.Result = new UnauthorizedResult(); return; } } }
在須要受權的Action或Controller上加上該特性便可。asp.net