ocelot 自定义认证和授权

C#

浏览数:223

2019-5-11

AD:资源代下载服务

ocelot 自定义认证和授权

Intro

最近又重新启动了网关项目,服务越来越多,每个服务都有一个地址,这无论是对于前端还是后端开发调试都是比较麻烦的,前端需要定义很多 baseUrl,而后端需要没有代码调试的时候需要对每个服务的地址都收藏着或者记在哪里,用的时候要先找到地址,甚是麻烦,有了网关之后,所有的 API 就有了统一的入口,对于前端来说就不需要维护那么多的 baseUrl,只需要网关的地址即可,对于后端来说也是同样的。

Ocelot 简介

Ocelot是一个用.NET Core实现并且开源的API网关,它功能强大,包括了:路由、请求聚合、服务发现、认证、鉴权、限流熔断等功能,这些功能只都只需要简单的配置即可完成。

自定义认证授权

自定义认证授权思想,这里的示例是一个基于用户角色授权的示例:

  1. 基于 url 以及 请求 Method 查询需要的权限
  2. 如果不需要用户登录就可以访问,就直接往下游服务转发
  3. 如果需要权限,判断当前登录用户的角色是否可以以当前 Method 访问当前路径
  4. 如果可以访问就转发到下游服务,如果没有权限访问根据用户是否登录,已登录返回 403 Forbidden,未登录返回 401 Unauthorized

Ocelot 的 认证授权不能满足我的需要,于是就自己扩展了一个 Ocelot 的中间件

示例代码

    public class ApiPermission
    {
        public string AllowedRoles { get; set; }

        public string PathPattern { get; set; }

        public string Method { get; set; }
    }

    public class UrlBasedAuthenticationMiddleware : Ocelot.Middleware.OcelotMiddleware
    {
        private readonly IConfiguration _configuration;
        private readonly IMemoryCache _memoryCache;
        private readonly OcelotRequestDelegate _next;

        public UrlBasedAuthenticationMiddleware(OcelotRequestDelegate next, IConfiguration configuration, IMemoryCache memoryCache, IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger<UrlBasedAuthenticationMiddleware>())
        {
            _next = next;

            _configuration = configuration;
            _memoryCache = memoryCache;
        }

        public async Task Invoke(DownstreamContext context)
        {
            var permissions = await _memoryCache.GetOrCreateAsync("ApiPermissions", async entry =>
           {
               using (var conn = new SqlConnection(_configuration.GetConnectionString("ApiPermissions")))
               {
                   entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
                   return (await conn.QueryAsync<ApiPermission>("SELECT * FROM dbo.ApiPermissions")).ToArray();
               }
           });

            var result = await context.HttpContext.AuthenticateAsync(context.DownstreamReRoute.AuthenticationOptions.AuthenticationProviderKey);
            context.HttpContext.User = result.Principal;

            var user = context.HttpContext.User;
            var request = context.HttpContext.Request;

            var permission = permissions.FirstOrDefault(p =>
                request.Path.Value.Equals(p.PathPattern, StringComparison.OrdinalIgnoreCase) && p.Method.ToUpper() == request.Method.ToUpper());

            if (permission == null)// 完全匹配不到,再根据正则匹配
            {
                permission =
                    permissions.FirstOrDefault(p =>
                        Regex.IsMatch(request.Path.Value, p.PathPattern, RegexOptions.IgnoreCase) && p.Method.ToUpper() == request.Method.ToUpper());
            }

            if (!user.Identity.IsAuthenticated)
            {
                if (permission != null && string.IsNullOrWhiteSpace(permission.AllowedRoles)) //默认需要登录才能访问
                {
                    //context.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "Anonymous") }, context.DownstreamReRoute.AuthenticationOptions.AuthenticationProviderKey));
                }
                else
                {
                    SetPipelineError(context, new UnauthenticatedError("unauthorized, need login"));
                    return;
                }
            }
            else
            {
                if (!string.IsNullOrWhiteSpace(permission?.AllowedRoles) &&
                    !permission.AllowedRoles.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Any(r => user.IsInRole(r)))
                {
                    SetPipelineError(context, new UnauthorisedError("forbidden, have no permission"));
                    return;
                }
            }

            await _next.Invoke(context);
        }
    }

认证授权之后

经过上面的认证授权之后,就可以往下游转发请求了,下游的服务有的可能会需要判断用户的角色或者需要根据用户的 userId 或者 Name 或者 邮箱去检查某些数据的权限,这里就需要把在网关完成认证之后,得到的用户信息传递给下游服务,这里我选择的是通过请求头把用户信息从网关服务传递到下游服务, Ocelot 可以把 Claims 中的信息转换到 Header ,详细参考Ocelot文档,但是实现有个bug,如果有多个值他只会取第一个,详见issue,可以自己扩展一个 ocelot 的中间件替换掉原有的中间件。

传递到下游服务之后,下游服务在需要用户信息的地方就可以从请求头中获取用户信息,如果下游服务比较复杂,不方便改动的话可以实现一个自定义的请求头认证,可以参考我的这一篇文章

Memo

如果有什么问题或建议,欢迎提出一起交流

作者:WeihanLi