NETCORE Ocelot网关下的API统一JWT鉴权

本文章主要整理并分享 netcore2.2 版本 Ocelot网关下,各个api使用JWT验证实现统一鉴权验证(代码如有雷同请多包涵)

项目所用到的NuGet包版本:  net core (2.2.0)  , Ocelot(13.5.2) 

如果对授权认证还有什么不明白的地方 ,安利一个专题系列:https://www.cnblogs.com/Benjamin-JunJie/p/13669414.html

园内很多blog都有介绍JWT的功能这里就不详细说明了,为什么用到JWT,主要出于的原因是想减少服务器的开销,将token的压力部分转移到客户端,

至于jwt的几个缺点:1.一旦颁发给前端无法销毁 2.无法实现refresh

此项目案例中都会进行处理规避。

下图描述了客户端请求某个服务是如何运行:

NETCORE Ocelot网关下的API统一JWT鉴权_第1张图片

项目目录结构如下:

NETCORE Ocelot网关下的API统一JWT鉴权_第2张图片

AuthenticationAPI:提供授权API(JWT令牌颁发),刷新TOKEN API(自定义机制 弥补JWT 无法 Refresh Token的缺陷 )

CastleAPIGateWay:Ocelot网关(注入JWT 验证)

TemplateAPI:业务api (注入OcelotJwt策略)

Castle.Infrastructure  ----> Ocelot.JWTAuthorizePolicy : 基础架构层提供 Token的生成,netcore gateway,业务api的管道注入,权限校验的逻辑代码

目录

1.基础架构层各个类的作用

2.API的Startup配置以及,配置文件样例

3. action写法 调用时使用方法

4.Refresh Token的实现


1.基础架构层各个类的作用

ClientType.cs :客户端登陆的类型枚举

此文件暂定三种登录类型 saas网站登录,c端小程序,B端App 

public enum AuthClientType
    {
        /// 
        /// SAAS后台
        /// 
        SaasPlatForm = 1,

        /// 
        /// C端小程序
        /// 
        ToCWehcatApp = 2,

        /// 
        /// B端App
        /// 
        ToBApp = 3
    }

JwtToken.cs :构建jwt token令牌(包含refresh token的构建)

public class JwtToken
    {
        /// 
        /// 获取基于JWT的Token
        /// 
        /// 
        /// 
        /// 
        /// 1.token   2 refreshToken
        /// 
        public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement,AuthClientType clientType)
        {
            double expiresIn = 2 * 60 * 60 * 1000;
            double refreshTokenExpiresIn = 24 * 60 * 60 * 1000;
            switch (clientType)
            {
                case AuthClientType.SaasPlatForm://SAAS后台 
                    refreshTokenExpiresIn = 8 * 60 * 60 * 1000; //设置成8小时  
                    break;
                case AuthClientType.ToCWehcatApp://小程序
                    refreshTokenExpiresIn = 7 * 24 * 60 * 60 * 1000; //设置成7天  用于小程序客户端之类的,能确保一周永久在线
                    break;
                default:
                    refreshTokenExpiresIn = 7 * 24 * 60 * 60 * 1000; 
                    break;
            }

            var now = DateTime.Now;
            var jwt_token = new JwtSecurityToken(
            issuer: permissionRequirement.Issuer,
            audience: permissionRequirement.Audience,
            claims: claims,
            notBefore: now,
            expires: now.Add(TimeSpan.FromMilliseconds(expiresIn)),
            //expires: now.Add(TimeSpan.FromMilliseconds(5 * 60 * 1000)),
            signingCredentials: permissionRequirement.SigningCredentials
            );

            //用于refresh使用的 jwt
            var jwt_refreshtoken = new JwtSecurityToken(
            issuer: permissionRequirement.Issuer,
            audience: permissionRequirement.Audience,
            claims: claims,
            notBefore: now,
            expires: now.Add(TimeSpan.FromMilliseconds(refreshTokenExpiresIn)),
            //expires: now.Add(TimeSpan.FromMilliseconds(1000 * 60 * 10)),
            signingCredentials: permissionRequirement.SigningCredentials
            );

            var encodedJwt_Token = new JwtSecurityTokenHandler().WriteToken(jwt_token);
            var encodedJwt_RefreshToken = new JwtSecurityTokenHandler().WriteToken(jwt_refreshtoken);
              
            var responseTokenJson = new
            { 
                access_token = encodedJwt_Token,
                expires = now.Add(TimeSpan.FromMilliseconds(expiresIn)),
                expires_in = expiresIn,
                //expires = now.Add(TimeSpan.FromMilliseconds(5 * 60 * 1000)),
                //expires_in = 5 * 60 * 1000,
                token_type = "Bearer",
                refresh_access_token = new
                {
                    refresh_token = encodedJwt_RefreshToken,
                    expires = now.Add(TimeSpan.FromMilliseconds(refreshTokenExpiresIn)),
                    expires_in = refreshTokenExpiresIn
                    //expires = now.Add(TimeSpan.FromMilliseconds(1000 * 60 * 10)),
                    //expires_in = 1000 * 60 * 10
                }
            };

            return responseTokenJson;
        }
    }

jwt不自带refresh功能,自定义的refresh原理为:构建一个更长时效的jwt 令牌作为jwt令牌的一个属性 返回给前端。

OcelotJwtBearerExtension:Ocelot下JwtBearer扩展 提供两个静态方法用来网管层的注入和业务api层的注入

/// 
    /// Ocelot下JwtBearer扩展
    /// 
    public static class OcelotJwtBearerExtension
    {
        /// 
        /// 注入Ocelot下JwtBearer,在ocelot网关的Startup的ConfigureServices中调用
        /// 
        /// IServiceCollection
        /// 发行人
        /// 订阅人
        /// 密钥
        /// 默认架构
        /// 是否https
        /// 
        public static AuthenticationBuilder AddOcelotJwtBearer(this IServiceCollection services, string issuer, string audience, string secret, string defaultScheme, bool isHttps = false)
        {
            var keyByteArray = Encoding.ASCII.GetBytes(secret);
            var signingKey = new SymmetricSecurityKey(keyByteArray);
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,
                ValidateIssuer = true,
                ValidIssuer = issuer,//发行人
                ValidateAudience = true,
                ValidAudience = audience,//订阅人
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero,
                RequireExpirationTime = true,
            };
            return services.AddAuthentication(options =>
            {
                options.DefaultScheme = defaultScheme;
            })
            .AddJwtBearer(defaultScheme, opt =>
            {
                //不使用https
                opt.RequireHttpsMetadata = isHttps;
                opt.TokenValidationParameters = tokenValidationParameters;
            });
        }

        /// 
        /// 注入Ocelot jwt策略,在业务API应用中的Startup的ConfigureServices调用
        /// 
        /// IServiceCollection
        /// 发行人
        /// 订阅人
        /// 密钥
        /// 默认架构
        /// 自定义策略名称 
        /// 是否开启jwt验证
        /// 是否https
        /// 
        public static AuthenticationBuilder AddOcelotPolicyJwtBearer(this IServiceCollection services, 
            string issuer, string audience, string secret, string defaultScheme, string policyName,string openJWT, bool isHttps = false)
        {

            var keyByteArray = Encoding.UTF8.GetBytes(secret);
            var signingKey = new SymmetricSecurityKey(keyByteArray);
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,
                ValidateIssuer = true,
                ValidIssuer = issuer,//发行人
                ValidateAudience = true,
                ValidAudience = audience,//订阅人
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero,
                RequireExpirationTime = true,

            };
            var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); 
            var permissionRequirement = new PermissionRequirement( 
            issuer,
            audience,
            signingCredentials ,
            openJWT
            );
            //注入授权Handler
            services.AddSingleton();
            services.AddSingleton(permissionRequirement);
            return services.AddAuthorization(options =>
            {
                options.AddPolicy(policyName,
                policy => policy.Requirements.Add(permissionRequirement));

            })
            .AddAuthentication(options =>
            {
                options.DefaultScheme = defaultScheme;
            })
            .AddJwtBearer(defaultScheme, o =>
            {
                //不使用https
                o.RequireHttpsMetadata = isHttps;
                o.TokenValidationParameters = tokenValidationParameters;
            });
        }
        /// 
        /// 注入Token生成器参数,在token生成项目的Startup的ConfigureServices中使用
        /// 
        /// IServiceCollection
        /// 发行人
        /// 订阅人
        /// 密钥 
        /// 
        public static IServiceCollection AddJTokenBuild(this IServiceCollection services, string issuer, string audience, string secret )
        {
            var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secret)), SecurityAlgorithms.HmacSha256); 
            var permissionRequirement = new PermissionRequirement( 
            issuer,
            audience,
            signingCredentials,
            "True"
            );
            return services.AddSingleton(permissionRequirement);

        }

    }

此类中用到了一些 身份验证和管道注入,以及core天然自带的AddSingleton依赖注入 ,有兴趣的同学可以搜索一下群内其他大神的博客了解学习一下,之后有空我也会整理分享。

PermissionRequirement:授权环境 包含所有token认证都会用到的 发行人,订阅人,签名验证,以及自己项目需要用到的一些属性

public class PermissionRequirement : IAuthorizationRequirement
    { 
        /// 
        /// 请求路径
        /// 
        public string LoginPath { get; set; } = "/api/Auth/Login";
        /// 
        /// 发行人
        /// 
        public string Issuer { get; set; }
        /// 
        /// 订阅人
        /// 
        public string Audience { get; set; } 
        /// 
        /// 签名验证
        /// 
        public SigningCredentials SigningCredentials { get; set; }

        /// 
        /// 构造
        /// 
        /// 无权限action
        /// 用户权限集合

        /// 
        /// 构造
        /// 
        /// 拒约请求的url 
        /// 声明类型
        /// 发行人
        /// 订阅人
        /// 签名验证实体
        public PermissionRequirement(  string issuer, string audience, SigningCredentials signingCredentials ,string openJWT)
        { 
            Issuer = issuer;
            Audience = audience; 
            SigningCredentials = signingCredentials;
            OpenJWT = openJWT;
        }

        /// 
        /// 是否启用jwt验证
        /// 
        public string OpenJWT { get; set; }
    }

PermissionHandler:权限授权Handler 重写授权处理方法加入自己业务自定义的判断

/// 
    /// 权限授权Handler
    /// 
    public class PermissionHandler : AuthorizationHandler
    {
        /// 
        /// 验证方案提供对象
        /// 
        public IAuthenticationSchemeProvider Schemes { get; set; }

        /// 
        /// 构造
        /// 
        /// 
        public PermissionHandler(IAuthenticationSchemeProvider schemes)
        {
            Schemes = schemes;
        }

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
        {
            //调试时可跳过验证
            if (requirement.OpenJWT != "True")
            {
                context.Succeed(requirement);
                return;
            }

            //从AuthorizationHandlerContext转成HttpContext,以便取出表求信息
            var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext;
            //请求Url
            var questUrl = httpContext.Request.Path.Value.ToLower();
            //判断请求是否停止
            var handlers = httpContext.RequestServices.GetRequiredService();
            foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
            {
                var handler = await handlers.GetHandlerAsync(httpContext, scheme.Name) as IAuthenticationRequestHandler;
                if (handler != null && await handler.HandleRequestAsync())
                {
                    context.Fail();
                    return;
                }
            }
            //判断请求是否拥有凭据,即有没有登录
            var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
            if (defaultAuthenticate != null)
            {
                var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name);
                //result?.Principal不为空即登录成功
                if (result?.Principal != null)
                {
                    httpContext.User = result.Principal;

                    //var jwtToken = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
                    //var playload = JsonConvert.DeserializeObject(EncrypHelper.DeCodeBase64("utf-8", jwtToken.Split('.')[1]));
                    //string identity = playload["identity"];
                    //string clientType = playload["clientType"];
                    //string username = playload["userName"];
                    //double exp = playload["exp"];
                    string userid = "";
                    string clientType = "";
                    string username = "";
                    double exp = 0;
                    string isAdmin = "";
                    foreach (var a in httpContext.User.Claims)
                    {
                        switch (a.Type)
                        {
                            case "userid":
                                userid = a.Value;
                                break;
                            case "clientType":
                                clientType = a.Value;
                                break;
                            case "userName":
                                username = a.Value;
                                break;
                            case "exp":
                                exp = Convert.ToDouble(a.Value);
                                break;
                            case "isAdmin":
                                isAdmin = a.Value;
                                break;
                            default:
                                break;
                        }
                    }


                    System.DateTime startTime = Convert.ToDateTime(new System.DateTime(1970, 1, 1)); // 当地时区
                    DateTime expDate = startTime.AddSeconds(exp).AddHours(8);

                    if (string.IsNullOrEmpty(userid) || string.IsNullOrEmpty(clientType))
                    {
                        context.Fail();
                        return;
                    }

                    //根据clientType判断是否需要校验url权限  saas平台的需要校验url权限 
                    //todo 根据自己的权限逻辑编写验证代码  

                    //判断过期时间  
                    if (expDate >= DateTime.Now)
                    {
                        context.Succeed(requirement);
                    }
                    else
                    {
                        context.Fail();
                    }
                    return;
                }
            }
            //判断没有登录时,是否访问登录的url,并且是Post请求,并且是form表单提交类型,否则为失败
            if (!questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) && (!httpContext.Request.Method.Equals("POST")
            || !httpContext.Request.HasFormContentType))
            {
                context.Fail();
                return;
            }
            context.Succeed(requirement);
        }


    }

2.API的Startup配置以及,配置文件样例

AuthenticationAPI  startup:

services.AddJTokenBuild(audienceConfig["Issuer"], audienceConfig["Audience"], audienceConfig["Secret"]);
            services.AddOcelotPolicyJwtBearer(audienceConfig["Issuer"], audienceConfig["Audience"],
                audienceConfig["Secret"], "BenBearer", "AuthJWT", audienceConfig["OpenJWT"]);

GateWayAPI 配置文件:

{
      "DownstreamPathTemplate": "/{url}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5050
        }
      ],
      "UpstreamPathTemplate": "/TemplateAPI/{url}",
      "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ],
      "ReRouteIsCaseSensitive": true,
      "HttpHandlerOptions": {
        "UseTracing": true
      },
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "BenBearer",
        "AllowedScopes": []
      }
    }

TemplateAPI startup类:

//读取配置文件,注入OcelotJwt策略
            var audienceConfig = Configuration.GetSection("Audience");
            services.AddOcelotPolicyJwtBearer(audienceConfig["Issuer"], audienceConfig["Audience"],
                audienceConfig["Secret"], "BenBearer", "AuthJWT", audienceConfig["OpenJWT"]);

各个api的json配置文件 需要添加 Audience 节点:

"Audience": {
    "Secret": "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
    "Issuer": "Benjamin",
    "Audience": "everone",
    "OpenJWT": "True"
  }

3. action写法 调用时使用方法

需要验证时:

[Authorize("AuthJWT")]
[HttpGet]
public ActionResult GetByAuth()
{
     return ReturnOperterResult(200, "GetByAuth Success!", null, null);
}

注意:policy的name 必须和注入时命名的一样

跳过验证直接打上 [AllowAnonymous] 标签

授权API项目中 AccountLogin 方法用来获取 Token

[AllowAnonymous]
        [HttpPost]
        public ActionResult AccountLogin([FromBody]dynamic obj)
        {
            
            AuthService ser = new AuthService();
            JsonApiResult result = ser.AccessAuthorization(obj);


            if (result.StatusCode != 200)
            {
                return ReturnOperterResult(result.StatusCode, result.Msg, null, result.Msg);
            }
            else
            {
                string username = ((AccessInfo)result.Data).UserName;
                string userid = ((AccessInfo)result.Data).UserId.ToString();
                string OrgId = ((AccessInfo)result.Data).OrgId.ToString();
                string OrgName = ((AccessInfo)result.Data).OrgName.ToString();
                string IsAdmin = ((AccessInfo)result.Data).IsAdmin.ToString();
                string ClientType = ((AccessInfo)result.Data).ClientType.ToString();
                //如果是基于用户的授权策略,这里要添加用户;如果是基于角色的授权策略,这里要添加角色
                var claims = new Claim[] {
                    new Claim("userName", username),
                    new Claim("clientType",ClientType.ToString()),
                    new Claim("userid",userid),
                    new Claim("jti",Guid.NewGuid().ToString()),
                    new Claim("orgId",OrgId),
                    new Claim("orgName",OrgName),
                    new Claim("isAdmin",IsAdmin)
                };
                //用户标识
                var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
                identity.AddClaims(claims);
                var token = JwtToken.BuildJwtToken(claims, _requirement, (AuthClientType)(Convert.ToInt32(ClientType)));
                return ReturnOperterResult(200, "认证成功",
                    new
                    {
                        Token = token,
                        UserId = Convert.ToInt32(userid),
                        UserName = username,
                        OrgId = Convert.ToInt32(OrgId),
                        OrgName = OrgName,
                        IsAdmin = Convert.ToBoolean(IsAdmin),
                        ClientType = Convert.ToInt32(ClientType)
                    });
            }
        }

注意: Claim中的 jti,官方建议用jti来识别每次颁发的令牌,我们也可以记录jti用来做令牌的回收,具体代码根据业务来实现,这样就避免了 token一旦颁发给客户端后无法追溯并回收。

参数[FromBody]dynamic obj 这是成动态类型,clientType为必须项,这样更通用,后台系统可自主添加 用户名 密码参数,小程序登录只需要appid即可。

接下来我们用postman 来操作一下:

颁发token:

NETCORE Ocelot网关下的API统一JWT鉴权_第3张图片

TemplateAPI 中新建两个api 

[Authorize("AuthJWT")]
[HttpGet]
public ActionResult GetByAuth()
{
     return ReturnOperterResult(200, "GetByAuth Success!", null, null);
}

[AllowAnonymous]
[HttpGet]
public ActionResult GetWithOutAuth1()
{
     return ReturnOperterResult(200, "GetWithOutAuth1 Success!", null, null);
}

不带token执行一下 GetByAuth方法:返回401未授权 

NETCORE Ocelot网关下的API统一JWT鉴权_第4张图片

正确的使用方法  Hearders里加入key Authorization value值为 bearer+空格+ jwttoken:

NETCORE Ocelot网关下的API统一JWT鉴权_第5张图片

5050端口是内部api的地址,我们再看一下 使用网关调用:

网关地址 5000 :

NETCORE Ocelot网关下的API统一JWT鉴权_第6张图片

 

4.Refresh Token的实现

[Authorize("AuthJWT")]
        [HttpPost]
        public ActionResult RefreshToken()
        {
            HttpContext _context = this.HttpContext; 

            string userid = "";
            string clientType = "";
            string userName = "";
            string OrgId = "";
            string OrgName = "";
            string IsAdmin = "";

            foreach (var a in _context.User.Claims)
            {
                if (a.Type == "userid")
                {
                    Console.WriteLine(a.Value);
                }
                switch (a.Type)
                {
                    case "userid":
                        userid = a.Value;
                        break;
                    case "clientType":
                        clientType = a.Value;
                        break;
                    case "userName":
                        userName = a.Value;
                        break;
                    case "orgId":
                        OrgId = a.Value;
                        break;
                    case "orgName":
                        OrgName = a.Value;
                        break;
                    case "isAdmin":
                        IsAdmin = a.Value;
                        break;
                    default:
                        break;
                }
            }


            var claims = new Claim[] {
                    new Claim("userName", userName),
                    new Claim("clientType",clientType),
                    new Claim("userid",userid),
                    new Claim("jti",Guid.NewGuid().ToString()),
                    new Claim("orgId",OrgId),
                    new Claim("orgName",OrgName),
                    new Claim("isAdmin",IsAdmin)
                };


            //用户标识
            var _identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
            _identity.AddClaims(claims);
            var token = JwtToken.BuildJwtToken(claims, _requirement, (AuthClientType)(Convert.ToInt32(clientType)));
            return ReturnOperterResult(200, "刷新成功",
                new
                {
                    token,
                    UserId = Convert.ToInt32(userid),
                    UserName = userName,
                    OrgId = Convert.ToInt32(OrgId),
                    OrgName = OrgName,
                    IsAdmin = Convert.ToBoolean(IsAdmin),
                    ClientType = Convert.ToInt32(clientType)
                }, null);
        }

实现原理:利用 jwt 验证 jwt 这个自己看代码理解。

如果您认为这篇文章还不错或者有所收获,您可以点击左下角的【点赞】【收藏】,或给作者【赞赏】,这将是我继续写作,分享的最大动力!

 

你可能感兴趣的:(.net,core,jwt,ocelot)