.net core使用Ocelot+Identity Server统一网关验证
源码下载地址:下载
项目结构如下图:
在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文件:
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文件修改如下:
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自带的基于角色授权需要像下图那样写死角色的名称,当角色权限发生变化时,需要修改并重新发布站点,很不方便。
所以我自定义了一个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上加上该特性即可。