本文章主要整理并分享 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
此项目案例中都会进行处理规避。
下图描述了客户端请求某个服务是如何运行:
项目目录结构如下:
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的实现
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);
}
}
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"
}
需要验证时:
[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:
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未授权
正确的使用方法 Hearders里加入key Authorization value值为 bearer+空格+ jwttoken:
5050端口是内部api的地址,我们再看一下 使用网关调用:
网关地址 5000 :
[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 这个自己看代码理解。
如果您认为这篇文章还不错或者有所收获,您可以点击左下角的【点赞】【收藏】,或给作者【赞赏】,这将是我继续写作,分享的最大动力!