从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图 - LamondLu - 博客园

mikel阅读(545)

来源: 从零开始实现ASP.NET Core MVC的插件式开发(一) – 使用ApplicationPart动态加载控制器和视图 – LamondLu – 博客园

系列文章

前言

如果你使用过一些开源CMS的话,肯定会用过其中的的插件化功能,用户可以通过启用或者上传插件包的方式动态添加一些功能,那么在ASP.NET Core MVC中如何实现插件化开发呢,下面我们来探究一下。

本系列只是笔者的一些尝试,并不表示一定正确,只是为了分享一些思路,大家可以一起讨论一下,后续会不断更新。

什么是ApplicationPart?

ASP.NET Core中进行插件话开发,就不得不说ApplicationPart。 ApplicationPartASP.NET Core一个重要组件,它是应用程序资源的一种抽象,通过它可以发现程序集中包含的控制器、视图组件、TagHelper和预编译Razor视图等MVC功能。

默认情况下,当一个ASP.NET Core MVC应用启动时,它只会尝试在当前应用启动的项目及引用的项目中加载控制器,如果想从未直接引用的程序集中加载控制器和预编译Razor视图,我们就需要借助ApplicationPart了。

而ASP.NET Core MVC中,有一个ApplicaitonPartManager类, 通过ApplicationPartManager我们可以来配置当前应用中使用哪一些ApplicationPart

例:

var assembly = Assembly.LoadFile("demo.dll");

var assemblyPart = new AssemblyPart(assembly);

var mvcBuilders = services.AddMvc();

mvcBuilders.ConfigureApplicationPartManager(apm =>
{
    apm.ApplicationParts.Add(assemblyPart);
});

下面呢,我们通过一个最简单的实例,给大家演示一下如何借助ApplicationPart,动态加载第三方程序集中的控制器和预编译视图。

创建项目

首先我们创建一个ASP.NET Core MVC的站点,命名为DynamicPluginsDemoSite

然后我们同时创建一个.NET Core Class Library项目,命名为DemoPlugin1, 同时对该项目引用

  • Microsoft.AspNetCore.App
  • Microsoft.AspNetCore.Razor
  • Microsoft.AspNetCore.Razor.Design

注意: 针对以上3个程序集,需要保证DynamicPluginsDemoSite和DemoPluigin1使用的相同的版本。

这里为了保证Razor视图的预编译,我们需要打开DemoPlugin1项目的工程文件DemoPlugin1.csproj。将项目使用的SDK从”Microsoft.NET.Sdk”改为”Microsoft.Net.Sdk.Razor”。

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <OutputPath>C:\Users\Lamond Lu\source\repos\DynamicPlugins\DynamicPluginsDemoSite\bin\Debug</OutputPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" />
  </ItemGroup>

</Project>

注:如果不做此修改,最后项目编译之后,不会产生预编译的Razor视图程序集。(这里如果有其他更优雅的修改方式,请大家留言, 我后续会先尝试先编写一个项目模板来避免这个重复操作)。

添加控制器和视图

下面我们开始编写我们的插件。

这里我们首先创建一个Plugin1Controller.cs.

	public class Plugin1Controller : Controller
    {
        public IActionResult HelloWorld()
        {
            return View();
        }
    }

然后我们添加一个对应的视图文件HelloWorld.cshtml。

@{

}

<h1>This is Demo Plugin1.</h1>

最终项目文件目录如下:

最后我们需要修改一个项目的输出目录,我们需要将项目编译的dll发送到DynamicPluginsDemoSite项目的Debug目录中。

以上我们就完成了第一个组件的所有修改,下面我们开始修改DynamicPluginsDemoSite项目。

如何动态加载插件中的控制器?

由于DynamicPluginsDemoSite项目不能直接引用DemoPlugin1, 所以当项目启动时,不能自主发现DemoPlugin1项目中的控制器,所以这里我们需要使用ApplicationPart将DemoPlugin1的程序集加载到当前的运行环境中。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });


    var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1.dll");
    var mvcBuilders = services.AddMvc();
    var controllerAssemblyPart = new AssemblyPart(assembly);

    mvcBuilders.ConfigureApplicationPartManager(apm =>
    {
        apm.ApplicationParts.Add(controllerAssemblyPart);
    });

    mvcBuilders.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

代码解释:

  • 由于前一步中, 我们将DemoPlugin1的程序集输出到了DynamicPluginsDemoSite的Debug目录中,所以这里我们可以使用Assembly.LoadFile方法将它加载。
  • 这里我们使用AssemblyPart类,将加载程序集封装成一个ApplicationPart.
  • mvcBuilders对象的ConfigureApplicationPartManager方法可以用来配置当前项目中使用的ApplicationPart

如何加载组件的预编译Razor视图?

加载完控制器之后,我们还需要加载插件的预编译Razor视图。这里和之前的稍有不同,我们需要使用CompileRazorAssemblyPart来封装加载的预编译Razor视图。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });


    var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1.dll");
    var assemblyView = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1.Views.dll");
    var viewAssemblyPart = new CompiledRazorAssemblyPart(assemblyView);

    var controllerAssemblyPart = new AssemblyPart(assembly);
    var mvcBuilders = services.AddMvc();

    mvcBuilders.ConfigureApplicationPartManager(apm =>
    {
        apm.ApplicationParts.Add(controllerAssemblyPart);
        apm.ApplicationParts.Add(viewAssemblyPart);
    });

    mvcBuilders.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

最终效果

现在我们启动DynamicPluginsDemoSite,在浏览器中输入/Plugin1/HelloWorld, 我们的插件就正常启用了。

注意:在启动DynamicPluginsDemoSite站点之前,请务必先编译DemoPlugin1项目,这样DemoPlugin1产生的程序集才会输出到DynamicPluginsDemoSite中。

总结

以上只是实现了一个最简单的MVC插件功能,要想完善整个项目,后续还有很多工作要做

  • 需要创建一个插件模板,来避免一些重复操作。
  • 需要将插件的模型和业务抽象出来。
  • 需要改用数据库来保存插件信息。
  • 需要支持实现插件的管理以及插件的升级。

后续我会慢慢实现以上功能,大家敬请期待。

Asp.Net Core 3.1学习-Web Api 中基于JWT的token验证及Swagger使用 (4) - 魏杨杨 - 博客园

mikel阅读(426)

来源: Asp.Net Core 3.1学习-Web Api 中基于JWT的token验证及Swagger使用 (4) – 魏杨杨 – 博客园

1、初始JWT

1.1、JWT原理

JWT(JSON Web Token)是目前最流行的跨域身份验证解决方案,他的优势就在于服务器不用存token便于分布式开发,给APP提供数据用于前后端分离的项目。登录产生的 token的项目完全可以独立与其他项目。当用户访问登录接口的时候会返回一个token,然后访问其他需要登录的接口都会带上这个token,后台进行验证如果token是有效的我们就认为用户是正常登录的,然后我们可以从token中取出来一些携带的信息进行操作。当然这些携带的信息都可以通过其他额外的字段进行传递,但是用token传递的话,不用其他额外加其他字段了。

JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

1.2、JWT结构

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbklEIjoiYWRtaW4iLCJuYmYiOjE1ODc4OTE2OTMsImV4cCI6MTU4NzkyNzY5MywiaXNzIjoiV1lZIiwiYXVkIjoiRXZlcnlUZXN0T25lIn0.-snenNVHrrKq9obN8FzKe0t99ok6FUm5pHv-P_eYc30

第一部分我们称它为头部(header):声明类型,这里是jwt;声明加密的算法 通常直接使用 HMAC SHA256

{
  'typ': 'JWT',
  'alg': 'HS256'
}

 

第二部分我们称其为载荷(payload, 类似于飞机上承载的物品):

iss:Token发布者

exp:过期时间 分钟

sub:主题

aud:Token接受者

nbf:在此之前不可用

iat:发布时间

jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

复制代码
{

"sub": "1234567890",

"name": "wyy",

"admin": true

}
复制代码

 

第三部分是签证(signature):这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

2、生成Token

2.1、建立项目

在VS2019中新建一个Core Api程序 Core选3.1 然后在项目上添加一个Jwt文件夹帮助类,新建接口ITokenHelper,类:TokenHelper继承ITokenHelper,类JWTConfig,类TnToken

JWTConfig:用来保存读取jwt相关配置

 

复制代码
/// <summary>
    /// 配置token生成信息
    /// </summary>
    public class JWTConfig
    {
        /// <summary>
        /// Token发布者
        /// </summary>
        public string Issuer { get; set; }
        /// <summary>
        /// oken接受者
        /// </summary>
        public string Audience { get; set; }
        /// <summary>
        /// 秘钥
        /// </summary>
        public string IssuerSigningKey { get; set; }
        /// <summary>
        /// 过期时间
        /// </summary>
        public int AccessTokenExpiresMinutes { get; set; }
    }
复制代码

TnToken:存放Token 跟过期时间的类

复制代码
/// <summary>
    /// 存放Token 跟过期时间的类
    /// </summary>
    public class TnToken
    {
        /// <summary>
        /// token
        /// </summary>
        public string TokenStr { get; set; }
        /// <summary>
        /// 过期时间
        /// </summary>
        public DateTime Expires { get; set; }
    }
复制代码

 

ITokenHelper接口:token工具类的接口,方便使用依赖注入,很简单提供两个常用的方法

复制代码
/// <summary>
    /// token工具类的接口,方便使用依赖注入,很简单提供两个常用的方法
    /// </summary>
    public interface ITokenHelper
    {
        /// <summary>
        /// 根据一个对象通过反射提供负载生成token
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="user"></param>
        /// <returns></returns>
        TnToken CreateToken<T>(T user) where T : class;
        /// <summary>
        /// 根据键值对提供负载生成token
        /// </summary>
        /// <param name="keyValuePairs"></param>
        /// <returns></returns>
        TnToken CreateToken(Dictionary<string, string> keyValuePairs);
    }
复制代码

 

TokenHelper:实现类

复制代码
/// <summary>
    /// Token生成类
    /// </summary>
    public class TokenHelper : ITokenHelper
    {
        private readonly IOptions<JWTConfig> _options;
        public TokenHelper(IOptions<JWTConfig> options)
        {
            _options = options;
        }

        /// <summary>
        /// 根据一个对象通过反射提供负载生成token
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="user"></param>
        /// <returns></returns>
        public TnToken CreateToken<T>(T user) where T : class
        {
            //携带的负载部分,类似一个键值对
            List<Claim> claims = new List<Claim>();
            //这里我们用反射把model数据提供给它
            foreach (var item in user.GetType().GetProperties())
            {
                object obj = item.GetValue(user);
                string value = "";
                if (obj != null)
                    value = obj.ToString();

                claims.Add(new Claim(item.Name, value));
            }
            //创建token
            return CreateToken(claims);
        }

        /// <summary>
        /// 根据键值对提供负载生成token
        /// </summary>
        /// <param name="keyValuePairs"></param>
        /// <returns></returns>
        public TnToken CreateToken(Dictionary<string, string> keyValuePairs)
        {
            //携带的负载部分,类似一个键值对
            List<Claim> claims = new List<Claim>();
            //这里我们通过键值对把数据提供给它
            foreach (var item in keyValuePairs)
            {
                claims.Add(new Claim(item.Key, item.Value));
            }
            //创建token
            return CreateTokenString(claims);
        }
        /// <summary>
        /// 生成token
        /// </summary>
        /// <param name="claims">List的 Claim对象</param>
        /// <returns></returns>
        private TnToken CreateTokenString(List<Claim> claims)
        {
            var now = DateTime.Now;
            var expires = now.Add(TimeSpan.FromMinutes(_options.Value.AccessTokenExpiresMinutes));
            var token = new JwtSecurityToken(
                issuer: _options.Value.Issuer,//Token发布者
                audience: _options.Value.Audience,//Token接受者
                claims: claims,//携带的负载
                notBefore: now,//当前时间token生成时间
                expires: expires,//过期时间
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256));
            return new TnToken { TokenStr = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires };
        }

    }
复制代码

 2.2、在Startup中去配置jwt相关:

ConfigureServices中:

复制代码
#region jwt配置
            services.AddTransient<ITokenHelper, TokenHelper>();
            //读取配置文件配置的jwt相关配置
            services.Configure<JWTConfig>(Configuration.GetSection("JWTConfig"));
            //启用JWT
            services.AddAuthentication(Options =>
            {
                Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).
            AddJwtBearer();#endregion
复制代码

 

JwtBearerDefaults.AuthenticationScheme与AddJwtBearer();下载两个依赖即可。或者NuGet安装

appsettings中简单配置一下jwt相关的信息:

 "JWTConfig": {
        "Issuer": "WYY", //Token发布者
        "Audience": "EveryTestOne", //Token接受者
        "IssuerSigningKey": "WYY&YL889455200Sily", //秘钥可以构建服务器认可的token;签名秘钥长度最少16
        "AccessTokenExpiresMinutes": "600" //过期时间 分钟
    },

 

Configure中去启用验证中间件:

//启用认证中间件 要写在授权UseAuthorization()的前面
app.UseAuthentication();

 2.3、一个简单的登录获取token

在Controllers文件夹里面新建一个api 名字LoginTest

复制代码
 [EnableCors("AllowCors")]
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class LoginTestController : ControllerBase
    {
        private readonly ITokenHelper tokenHelper = null;
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="_tokenHelper"></param>
        public LoginTestController(ITokenHelper _tokenHelper)
        {
            tokenHelper = _tokenHelper;
        }
        /// <summary>
        /// 登录测试
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
         [HttpPost]
        public ReturnModel Login([FromBody]UserDto user)
        {
            var ret = new ReturnModel();
            try
            {
                if (string.IsNullOrWhiteSpace(user.LoginID) || string.IsNullOrWhiteSpace(user.Password))
                {
                    ret.Code = 201;
                    ret.Msg = "用户名密码不能为空";
                    return ret;
                }
                //登录操作 我就没写了 || 假设登录成功
                if (1 == 1)
                {
                    Dictionary<string, string> keyValuePairs = new Dictionary<string, string>
                    {
                        { "loginID", user.LoginID }
                    };
                    ret.Code = 200;
                    ret.Msg = "登录成功";
                    ret.TnToken= tokenHelper.CreateToken(keyValuePairs);
                }
            }
            catch(Exception ex)
            {
                ret.Code = 500;
                ret.Msg = "登录失败:"+ex.Message;
            }
            return ret;
        }
    }
复制代码

 

UserDto接收类

复制代码
/// <summary>
    /// 登录类Dto
    /// </summary>
    public class UserDto
    {
        /// <summary>
        /// 用户名
        /// </summary>
        public string LoginID { get; set; }
        /// <summary>
        /// 密码
        /// </summary>
        public string Password { get; set; }
    }
复制代码

 

ReturnModel 只是我自己封装的一个统一的接口返回格式标准

复制代码
/// <summary>
    /// 返回类
    /// </summary>
    public class ReturnModel
    {
        /// <summary>
        /// 返回码
        /// </summary>
        public int Code { get; set; }
        /// <summary>
        /// 消息
        /// </summary>
        public string Msg { get; set; }
        /// <summary>
        /// 数据
        /// </summary>
        public object Data { get; set; }
        /// <summary>
        /// Token信息
        /// </summary>
        public TnToken TnToken { get; set; }
    }
复制代码

 

跨域上篇文章说了这里就不提了

2.4、前端获取token

我是用传统的MVC的一个启动页面

复制代码
<input type="hidden" id="tokenValue" name="tokenValue" value="" />
<br /><br /><br />
<span>Token:</span><div id="txtval"></div><br />
<span>有效期:</span><div id="txtvalTime"></div><br />

<div>
    <input type="button" value="获取Token" onclick="getToken()" /><br /><br /><br />
</div>
<script src="~/Scripts/jquery-3.3.1.js"></script>
<script type="text/javascript">
    //获取token
    function getToken() {
        var data = JSON.stringify({ LoginID: "admin", Password: "admin888" });
        $.ajax({
            type: "post",
            url: "https://localhost:44331/api/LoginTest/Login",
            dataType: "json",
            async: true,
            data: data,
            contentType: 'application/json',
            success: function (data) {
                console.log(data);
                $("#txtval").html(data.tnToken.tokenStr);
                $("#txtvalTime").html(new Date(data.tnToken.expires).Format("yyyy-MM-dd hh:mm"));
                $("#tokenValue").val(data.tnToken.tokenStr);

            },
            error: function (data) {
                console.log("错误" + data);
            }
        });
    }
    Date.prototype.Format = function (fmt) { //author: zhengsh 2016-9-5
        var o = {
            "M+": this.getMonth() + 1, //月份
            "d+": this.getDate(), //日
            "h+": this.getHours(), //小时
            "m+": this.getMinutes(), //分
            "s+": this.getSeconds(), //秒
            "q+": Math.floor((this.getMonth() + 3) / 3), //季度
            "S": this.getMilliseconds() //毫秒
        };
        if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
        for (var k in o)
            if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
        return fmt;
    }
</script>
复制代码

 

把Api启动起来 MVC也启动起来试试看

 

在JWT管网解码

3、验证前端传递的token

现在说说怎么来验证前台传递的jwt,其实很简单,最主要的就是验证token的有效性和是否过期。在接口ITokenHelper中添加验证的两个方法 。TokenHelper中实现

ITokenHelper中添加

复制代码
/// <summary>
        /// Token验证
        /// </summary>
        /// <param name="encodeJwt">token</param>
        /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值</param>
        /// <returns></returns>
        bool ValiToken(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad = null);
        /// <summary>
        /// 带返回状态的Token验证
        /// </summary>
        /// <param name="encodeJwt">token</param>
        /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值</param>
        /// <param name="action"></param>
        /// <returns></returns>
        TokenType ValiTokenState(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad, Action<Dictionary<string, string>> action);
复制代码

 

 

 

TokenHelper中添加

复制代码
/// <summary>
        /// 验证身份 验证签名的有效性
        /// </summary>
        /// <param name="encodeJwt"></param>
        /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值, </param>
        public bool ValiToken(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad = null)
        {
            var success = true;
            var jwtArr = encodeJwt.Split('.');
            if (jwtArr.Length < 3)//数据格式都不对直接pass
            {
                return false;
            }
            var header = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
            var payLoad = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[1]));
            //配置文件中取出来的签名秘钥
            var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(_options.Value.IssuerSigningKey));
            //验证签名是否正确(把用户传递的签名部分取出来和服务器生成的签名匹配即可)
            success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
            if (!success)
            {
                return success;//签名不正确直接返回
            }

            //其次验证是否在有效期内(也应该必须)
            var now = ToUnixEpochDate(DateTime.UtcNow);
            success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));

            //不需要自定义验证不传或者传递null即可
            if (validatePayLoad == null)
                return true;

            //再其次 进行自定义的验证
            success = success && validatePayLoad(payLoad);

            return success;
        }
        /// <summary>
        /// 时间转换
        /// </summary>
        /// <param name="date"></param>
        /// <returns></returns>
        private long ToUnixEpochDate(DateTime date)
        {
            return (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="encodeJwt"></param>
        /// <param name="validatePayLoad"></param>
        /// <param name="action"></param>
        /// <returns></returns>
        public TokenType ValiTokenState(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad, Action<Dictionary<string, string>> action)
        {
            var jwtArr = encodeJwt.Split('.');
            if (jwtArr.Length < 3)//数据格式都不对直接pass
            {
                return TokenType.Fail;
            }
            var header = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
            var payLoad = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[1]));
            var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(_options.Value.IssuerSigningKey));
            //验证签名是否正确(把用户传递的签名部分取出来和服务器生成的签名匹配即可)
            if (!string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1]))))))
            {
                return TokenType.Fail;
            }
            //其次验证是否在有效期内(必须验证)
            var now = ToUnixEpochDate(DateTime.UtcNow);
            if (!(now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString())))
            {
                return TokenType.Expired;
            }

            //不需要自定义验证不传或者传递null即可
            if (validatePayLoad == null)
            {
                action(payLoad);
                return TokenType.Ok;
            }
            //再其次 进行自定义的验证
            if (!validatePayLoad(payLoad))
            {
                return TokenType.Fail;
            }
            //可能需要获取jwt摘要里边的数据,封装一下方便使用
            action(payLoad);
            return TokenType.Ok;
        }
复制代码

 

其中TokenType是返回类型成功失败

public enum TokenType
    {
        Ok,
        Fail,
        Expired
    }

在api LoginTest中新增两个验证的方法

复制代码
/// <summary>
        /// 验证Token
        /// </summary>
        /// <param name="tokenStr">token</param>
        /// <returns></returns>
        [HttpGet]
        public ReturnModel ValiToken(string tokenStr)
        {
            var ret = new ReturnModel
            {
                TnToken = new TnToken()
            };
            bool isvilidate = tokenHelper.ValiToken(tokenStr);
            if(isvilidate)
            {
                ret.Code = 200;
                ret.Msg = "Token验证成功";
                ret.TnToken.TokenStr = tokenStr;
            }
            else
            {
                ret.Code = 500;
                ret.Msg = "Token验证失败";
                ret.TnToken.TokenStr = tokenStr;
            }
            return ret;
        }
        /// <summary>
        /// 验证Token 带返回状态
        /// </summary>
        /// <param name="tokenStr"></param>
        /// <returns></returns>
        [HttpGet]
        public ReturnModel ValiTokenState(string tokenStr)
        {
            var ret = new ReturnModel
            {
                TnToken = new TnToken()
            };
            string loginID = "";
            TokenType tokenType = tokenHelper.ValiTokenState(tokenStr, a => a["iss"] == "WYY" && a["aud"] == "EveryTestOne", action => { loginID = action["loginID"]; });
            if (tokenType == TokenType.Fail)
            {
                ret.Code = 202;
                ret.Msg = "token验证失败";
                return ret;
            }
            if (tokenType == TokenType.Expired)
            {
                ret.Code = 205;
                ret.Msg = "token已经过期";
                return ret;
            }

            //..............其他逻辑
            var data = new List<Dictionary<string, string>>();
            var bb = new Dictionary<string, string>
            {
                { "Wyy", "123456" }
            };
            data.Add(bb);
            ret.Code = 200;
            ret.Msg = "访问成功!";
            ret.Data =data ;
            return ret;
        }
复制代码

 

上面一个简单的验证和支持自定义验证的就写好了。下面带有状态的是让我们清楚的知道是什么状态请求登录的时候 或者请求数据的时候,是token过期还是说token没有获取到等等。

ValiTokenState第三个参数我还更了一个系统委托,是这样想的,处理可以验证token,还可以顺便取一个想要的数据,当然其实这样把相关逻辑混到一起也增加代码的耦合性,当时可以提高一点效率不用在重新解析一次数据,当然这个数据也可以通前台传递过来,所以怎么用还是看实际情况,这里只是封装一下提供这样一个方法,用的时候也可以用。

其前端请求代码

复制代码
 $.ajax({
            type: "post",
            url: "https://localhost:44331/api/LoginTest/ValiToken?tokenStr="+ $("#tokenValue").val(),
            dataType: "json",
            async: true,
            data: { token: $("#tokenValue").val() },
            contentType: 'application/json',
            success: function (data) {
                console.log(data);             
            },
            error: function (data) {
                console.log("错误" + data);
            }
        });
复制代码

4、Api中过滤器实现通用token验证

项目上新建一个文件夹Filter,在文件夹Filter里新建一个过滤器TokenFilter

复制代码
namespace JWTToken.Filter
{
    public class TokenFilter : Attribute, IActionFilter
    {
        private ITokenHelper tokenHelper;
        public TokenFilter(ITokenHelper _tokenHelper) //通过依赖注入得到数据访问层实例
        {
            tokenHelper = _tokenHelper;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {

        }
        public void OnActionExecuting(ActionExecutingContext context)
        {
            ReturnModel ret = new ReturnModel();
            //获取token
            object tokenobj = context.ActionArguments["token"];//前端地址栏参数传参
       //object tokenobj = context.HttpContext.Request.Headers["token"].ToString();//前端写在header里面获取的

            if (tokenobj == null)
            {
                ret.Code = 201;
                ret.Msg = "token不能为空";
                context.Result = new JsonResult(ret);
                return;
            }

            string token = tokenobj.ToString();

            string userId = "";
            //验证jwt,同时取出来jwt里边的用户ID
            TokenType tokenType = tokenHelper.ValiTokenState(token, a => a["iss"] == "WYY" && a["aud"] == "EveryTestOne", action => { userId = action["userId"]; });
            if (tokenType == TokenType.Fail)
            {
                ret.Code = 202;
                ret.Msg = "token验证失败";
                context.Result = new JsonResult(ret);
                return;
            }
            if (tokenType == TokenType.Expired)
            {
                ret.Code = 205;
                ret.Msg = "token已经过期";
                context.Result = new JsonResult(ret);
            }
            if (!string.IsNullOrEmpty(userId))
            {
                //给控制器传递参数(需要什么参数其实可以做成可以配置的,在过滤器里边加字段即可)
                //context.ActionArguments.Add("userId", Convert.ToInt32(userId));
            }
        }
    }
}
复制代码

context.ActionArguments。这是前段请求的时候地址栏带上的参数 token=xxx;这种类型的,不是请求的参数 不然会报错;

把过滤器在startup中注入一下:

 services.AddScoped<TokenFilter>();

需要验证token的地方,直接加上这个过滤器即可

 

前台试试 请求上图的GetList

复制代码
<input type="hidden" id="tokenValue" name="tokenValue" value="" />
<br /><br /><br />
<span>Token:</span><div id="txtval"></div><br />
<span>有效期:</span><div id="txtvalTime"></div><br />

<div>
    <input type="button" value="获取Token" onclick="getToken()" /><br /><br /><br />
</div>
<input type="button" value="获取List" onclick="getList()" /><br />
<script src="~/Scripts/jquery-3.3.1.js"></script>
<script type="text/javascript">
    //获取token
    function getToken() {
        var data = JSON.stringify({ LoginID: "admin", Password: "admin888" });
        $.ajax({
            type: "post",
            url: "https://localhost:44331/api/LoginTest/Login",
            dataType: "json",
            async: true,
            data: data,
            contentType: 'application/json',
            success: function (data) {
                console.log(data);
                $("#txtval").html(data.tnToken.tokenStr);
                $("#txtvalTime").html(new Date(data.tnToken.expires).Format("yyyy-MM-dd hh:mm"));
                $("#tokenValue").val(data.tnToken.tokenStr);

            },
            error: function (data) {
                console.log("错误" + data);
            }
        });
    }
    //获取list
    function getList() {
        var data = JSON.stringify();
        $.ajax({
            type: "post",
            url: "https://localhost:44331/api/Home/GetList?token="+ $("#tokenValue").val(),
            dataType: "json",
            async: true,
            data: { token: $("#tokenValue").val() },
            contentType: 'application/json',
            success: function (data) {
                console.log(data);
                $("#txtval").html(JSON.stringify(data));
                
             

            },
            error: function (data) {
                console.log("错误" + data);
            }
        });
    }
    Date.prototype.Format = function (fmt) { //author: zhengsh 2016-9-5
        var o = {
            "M+": this.getMonth() + 1, //月份
            "d+": this.getDate(), //日
            "h+": this.getHours(), //小时
            "m+": this.getMinutes(), //分
            "s+": this.getSeconds(), //秒
            "q+": Math.floor((this.getMonth() + 3) / 3), //季度
            "S": this.getMilliseconds() //毫秒
        };
        if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
        for (var k in o)
            if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
        return fmt;
    }
</script>
复制代码

现获取token 赋值在隐藏框里在请求

5、在Api中使用Swagger

5.1项目中添加Swagger的相关包

 

 

 

 5.2ConfigureServices、Configure 中添加

复制代码
#region Swagger
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo
                {
                    Version = "v1",
                    Title = "测试接口文档",
                    Description = "测试接口"
                });
                // 为 Swagger 设置xml文档注释路径
                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                c.IncludeXmlComments(xmlPath);
                c.DocInclusionPredicate((docName, description) => true);
                //添加对控制器的标签(描述)
                c.DocumentFilter<ApplyTagDescriptions>();//显示类名
                c.CustomSchemaIds(type => type.FullName);// 可以解决相同类名会报错的问题
                //c.OperationFilter<AuthTokenHeaderParameter>();
            });
            #endregion
复制代码
复制代码
 app.UseSwagger(c =>
            {
                c.RouteTemplate = "swagger/{documentName}/swagger.json";
            });
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Web App v1");
                c.RoutePrefix = "doc";//设置根节点访问
                //c.DocExpansion(DocExpansion.None);//折叠
                c.DefaultModelsExpandDepth(-1);//不显示Schemas
            });
复制代码

 5.3、项目属性修改

5.4、添加接口类的注释

 

看效果

6、总结

JWT个人的理解就是api配置文件的IssuerSigningKey作为秘钥来加密的,客户端登录后获取到token 地址栏请求传到后端 后端通过解码获取到IssuerSigningKey是否跟后台解析出来的一直来匹配。后端可以卸载锅炉器里面来接收这个token来验证从而限制能不能访问Api。前端可以自己封装一个请求把token穿进去的参数就可以避免每次输入Token,前端可以Session?

下了班写的仓促了 哈哈。欢迎补充。

原文链接:https://www.cnblogs.com/w5942066/p/12781397.html

【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能 - 金焰的世界 - 博客园

mikel阅读(385)

来源: 【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能 – 金焰的世界 – 博客园

一、背景

首先说声抱歉,可能是因为假期综合症(其实就是因为懒哈)的原因,已经很长时间没更新博客了,现在也调整的差不多了,准备还是以每周1-2篇的进度来更新博客,并完成本项目所有功能。

言归正传,本重构项目是在我根据实际需求重构,由于还未完全写完,所以也没进行压测,在2月份时,张善友老师给我留言说经过压测发现我重构的Ocelot网关功能性能较差,其中根本原因就是缓存模块,由于重构项目的缓存强依赖Redis缓存,造成性能瓶颈,发现问题后,我也第一时间进行测试,性能影响很大,经过跟张老师请教,可以使用二级缓存来解决性能问题,首先感谢张老师关注并指点迷津,于是就有了这篇文章,如何把现有缓存改成二级缓存并使用。

二、改造思路

为了解决redis的强依赖性,首先需要把缓存数据存储到本地,所有请求都优先从本地提取,如果提取不到再从redis提取,如果redis无数据,在从数据库中提取。提取流程如下:

MemoryCache > Redis > db

此种方式减少提取缓存的网络开销,也合理利用了分布式缓存,并最终减少数据库的访问开销。但是使用此种方案也面临了一个问题是如何保证集群环境时每个机器本地缓存数据的一致性,这时我们会想到redis的发布、订阅特性,在数据发生变动时更新redis数据并发布缓存更新通知,由每个集群机器订阅变更事件,然后处理本地缓存记录,最终达到集群缓存的缓存一致性。

但是此方式对于缓存变更非常频繁的业务不适用,比如限流策略(准备还是使用分布式redis缓存实现),但是可以扩展配置单机限流时使用本地缓存实现,如果谁有更好的实现方式,也麻烦告知下集群环境下限流的实现,不胜感激。

三、改造代码

首先需要分析下目前改造后的Ocelot网关在哪些业务中使用的缓存,然后把使用本地缓存的的业务重构,增加提取数据流程,最后提供网关外部缓存初始化接口,便于与业务系统进行集成。

1.重写缓存方法

找到问题的原因后,就可以重写缓存方法,增加二级缓存支持,默认使用本地的缓存,新建CzarMemoryCache类,来实现IOcelotCache<T>方法,实现代码如下。

using Czar.Gateway.Configuration;
using Czar.Gateway.RateLimit;
using Microsoft.Extensions.Caching.Memory;
using Ocelot.Cache;
using System;

namespace Czar.Gateway.Cache
{
    /// <summary>
    /// 金焰的世界
    /// 2019-03-03
    /// 使用二级缓存解决集群环境问题
    /// </summary>
    public class CzarMemoryCache<T> : IOcelotCache<T>
    {
        private readonly CzarOcelotConfiguration _options;
        private readonly IMemoryCache _cache;
        public CzarMemoryCache(CzarOcelotConfiguration options,IMemoryCache cache)
        {
            _options = options;
            _cache = cache;
        }
        public void Add(string key, T value, TimeSpan ttl, string region)
        {
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix,region, key);
            if (_options.ClusterEnvironment)
            {
                var msg = value.ToJson();
                if (typeof(T) == typeof(CachedResponse))
                {//带过期时间的缓存
                    _cache.Set(key, value, ttl); //添加本地缓存
                    RedisHelper.Set(key, msg); //加入redis缓存
                    RedisHelper.Publish(key, msg); //发布
                }
                else if (typeof(T) == typeof(CzarClientRateLimitCounter?))
                {//限流缓存,直接使用redis
                    RedisHelper.Set(key, value, (int)ttl.TotalSeconds);
                }
                else
                {//正常缓存,发布
                    _cache.Set(key, value, ttl); //添加本地缓存
                    RedisHelper.Set(key, msg); //加入redis缓存
                    RedisHelper.Publish(key, msg); //发布
                }
            }
            else
            {
                _cache.Set(key, value, ttl); //添加本地缓存
            }
        }

        public void AddAndDelete(string key, T value, TimeSpan ttl, string region)
        {
            Add(key, value, ttl, region);
        }

        public void ClearRegion(string region)
        {
            if (_options.ClusterEnvironment)
            {
                var keys = RedisHelper.Keys(region + "*");
                RedisHelper.Del(keys);
                foreach (var key in keys)
                {
                    RedisHelper.Publish(key, ""); //发布key值为空,处理时删除即可。
                }
            }
            else
            {
                _cache.Remove(region);
            }
        }

        public T Get(string key, string region)
        {
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            if(region== CzarCacheRegion.CzarClientRateLimitCounterRegion&& _options.ClusterEnvironment)
            {//限流且开启了集群支持,默认从redis取
                return RedisHelper.Get<T>(key);
            }
            var result = _cache.Get<T>(key);
            if (result == null&& _options.ClusterEnvironment)
            {
                result= RedisHelper.Get<T>(key);
                if (result != null)
                {
                    if (typeof(T) == typeof(CachedResponse))
                    {//查看redis过期时间
                        var second = RedisHelper.Ttl(key);
                        if (second > 0)
                        {
                            _cache.Set(key, result, TimeSpan.FromSeconds(second));
                        }
                    }
                    else
                    {
                        _cache.Set(key, result, TimeSpan.FromSeconds(_options.CzarCacheTime));
                    }
                }
            }
            return result;
        }
    }
}

上面就段代码实现了本地缓存和Redis缓存的支持,优先从本地提取,如果在集群环境使用,增加redis缓存支持,但是此种方式不适用缓存变更非常频繁场景,比如客户端限流的实现,所以在代码中把客户端限流的缓存直接使用redis缓存实现。

2.注入实现和订阅

有了实现代码后,发现还缺少添加缓存注入和配置信息修改。首先需要修改配置文件来满足是否开启集群判断,然后需要实现redis的不同部署方式能够通过配置文件配置进行管理,避免硬编码导致的不可用问题。

配置文件CzarOcelotConfiguration.cs修改代码如下:

namespace Czar.Gateway.Configuration
{
    /// <summary>
    /// 金焰的世界
    /// 2018-11-11
    /// 自定义配置信息
    /// </summary>
    public class CzarOcelotConfiguration
    {
        /// <summary>
        /// 数据库连接字符串,使用不同数据库时自行修改,默认实现了SQLSERVER
        /// </summary>
        public string DbConnectionStrings { get; set; }

        /// <summary>
        /// 金焰的世界
        /// 2018-11-12
        /// 是否启用定时器,默认不启动
        /// </summary>
        public bool EnableTimer { get; set; } = false;

        /// <summary>
        /// 金焰的世界
        /// 2018-11.12
        /// 定时器周期,单位(毫秒),默认30分总自动更新一次
        /// </summary>
        public int TimerDelay { get; set; } = 30 * 60 * 1000;

        /// <summary>
        /// 金焰的世界
        /// 2018-11-14
        /// Redis连接字符串
        /// </summary>
        public string RedisConnectionString { get; set; }

        /// <summary>
        /// 金焰的世界
        /// 2019-03-03
        /// 配置哨兵或分区时使用
        /// </summary>
        public string[] RedisSentinelOrPartitionConStr { get; set; }

        /// <summary>
        /// 金焰的世界
        /// 2019-03-03
        /// Redis部署方式,默认使用普通方式
        /// </summary>
        public RedisStoreMode RedisStoreMode { get; set; } = RedisStoreMode.Normal;

        /// <summary>
        /// 金焰的计界
        /// 2019-03-03
        /// 做集群缓存同步时使用,会订阅所有正则匹配的事件
        /// </summary>
        public string RedisOcelotKeyPrefix { get; set; } = "CzarOcelot";

        /// <summary>
        /// 金焰的世界
        /// 2019-03-03
        /// 是否启用集群环境,如果非集群环境直接本地缓存+数据库即可
        /// </summary>
        public bool ClusterEnvironment { get; set; } = false;

        /// <summary>
        /// 金焰的世界
        /// 2018-11-15
        /// 是否启用客户端授权,默认不开启
        /// </summary>
        public bool ClientAuthorization { get; set; } = false;

        /// <summary>
        /// 金焰的世界
        /// 2018-11-15
        /// 服务器缓存时间,默认30分钟
        /// </summary>
        public int CzarCacheTime { get; set; } = 1800;
        /// <summary>
        /// 金焰的世界
        /// 2018-11-15
        /// 客户端标识,默认 client_id
        /// </summary>
        public string ClientKey { get; set; } = "client_id";

        /// <summary>
        /// 金焰的世界
        /// 2018-11-18
        /// 是否开启自定义限流,默认不开启
        /// </summary>
        public bool ClientRateLimit { get; set; } = false;
    }
}

在配置文件中修改了redis相关配置,支持使用redis的普通模式、集群模式、哨兵模式、分区模式,配置方式可参考csrediscore开源项目。

然后修改ServiceCollectionExtensions.cs代码,注入相关实现和redis客户端。

			builder.Services.AddMemoryCache(); //添加本地缓存
            #region 启动Redis缓存,并支持普通模式 官方集群模式  哨兵模式 分区模式
            if (options.ClusterEnvironment)
            {
                //默认使用普通模式
                var csredis = new CSRedis.CSRedisClient(options.RedisConnectionString);
                switch (options.RedisStoreMode)
                {
                    case RedisStoreMode.Partition:
                        var NodesIndex = options.RedisSentinelOrPartitionConStr;
                        Func<string, string> nodeRule = null;
                        csredis = new CSRedis.CSRedisClient(nodeRule, options.RedisSentinelOrPartitionConStr);
                        break;
                    case RedisStoreMode.Sentinel:
                        csredis = new CSRedis.CSRedisClient(options.RedisConnectionString, options.RedisSentinelOrPartitionConStr);
                        break;
                }
                //初始化 RedisHelper
                RedisHelper.Initialization(csredis);
            }
            #endregion
            builder.Services.AddSingleton<IOcelotCache<FileConfiguration>, CzarMemoryCache<FileConfiguration>>();
            builder.Services.AddSingleton<IOcelotCache<InternalConfiguration>, CzarMemoryCache<InternalConfiguration>>();
            builder.Services.AddSingleton<IOcelotCache<CachedResponse>, CzarMemoryCache<CachedResponse>>();
            builder.Services.AddSingleton<IInternalConfigurationRepository, RedisInternalConfigurationRepository>();
            builder.Services.AddSingleton<IOcelotCache<ClientRoleModel>, CzarMemoryCache<ClientRoleModel>>();
            builder.Services.AddSingleton<IOcelotCache<RateLimitRuleModel>, CzarMemoryCache<RateLimitRuleModel>>();
            builder.Services.AddSingleton<IOcelotCache<RemoteInvokeMessage>, CzarMemoryCache<RemoteInvokeMessage>>();
            builder.Services.AddSingleton<IOcelotCache<CzarClientRateLimitCounter?>, CzarMemoryCache<CzarClientRateLimitCounter?>>();

现在需要实现redis订阅来更新本地的缓存信息,在项目启动时判断是否开启集群模式,如果开启就启动订阅,实现代码如下:

public static async Task<IApplicationBuilder> UseCzarOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)
{
    //重写创建配置方法
    var configuration = await CreateConfiguration(builder);
    ConfigureDiagnosticListener(builder);
    CacheChangeListener(builder);
    return CreateOcelotPipeline(builder, pipelineConfiguration);
}
/// <summary>
/// 金焰的世界
/// 2019-03-03
/// 添加缓存数据变更订阅
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
private static void CacheChangeListener(IApplicationBuilder builder)
{
    var config= builder.ApplicationServices.GetService<CzarOcelotConfiguration>();
    var _cache= builder.ApplicationServices.GetService<IMemoryCache>();
    if (config.ClusterEnvironment)
    {
        //订阅满足条件的所有事件
        RedisHelper.PSubscribe(new[] { config.RedisOcelotKeyPrefix + "*" }, message =>
              {
                  var key = message.Channel;
                  _cache.Remove(key); //直接移除,如果有请求从redis里取
                  //或者直接判断本地缓存是否存在,如果存在更新,可自行实现。
              });
    }
}

使用的是从配置文件提取的正则匹配的所有KEY都进行订阅,由于本地缓存增加了定时过期策略,所以为了实现方便,当发现redis数据发生变化,所有订阅端直接移除本地缓存即可,如果有新的请求直接从redis取,然后再次缓存,防止集群客户端缓存信息不一致。

为了区分不同的缓存实体,便于在原始数据发送变更时进行更新,定义CzarCacheRegion类。

namespace Czar.Gateway.Configuration
{
    /// <summary>
    /// 缓存所属区域
    /// </summary>
    public class CzarCacheRegion
    {
        /// <summary>
        /// 授权
        /// </summary>
        public const string AuthenticationRegion = "CacheClientAuthentication";

        /// <summary>
        /// 路由配置
        /// </summary>
        public const string FileConfigurationRegion = "CacheFileConfiguration";

        /// <summary>
        /// 内部配置
        /// </summary>
        public const string InternalConfigurationRegion = "CacheInternalConfiguration";

        /// <summary>
        /// 客户端权限
        /// </summary>
        public const string ClientRoleModelRegion = "CacheClientRoleModel";

        /// <summary>
        /// 限流规则
        /// </summary>
        public const string RateLimitRuleModelRegion = "CacheRateLimitRuleModel";

        /// <summary>
        /// Rpc远程调用
        /// </summary>
        public const string RemoteInvokeMessageRegion = "CacheRemoteInvokeMessage";

        /// <summary>
        /// 客户端限流
        /// </summary>
        public const string CzarClientRateLimitCounterRegion = "CacheCzarClientRateLimitCounter";
    }
}

现在只需要修改缓存的region为定义的值即可,唯一需要改动的代码就是把之前写死的代码改成如下代码即可。

var enablePrefix = CzarCacheRegion.AuthenticationRegion;

3.开发缓存变更接口

现在整个二级缓存基本完成,但是还遇到一个问题就是外部如何根据数据库变更数据时来修改缓存数据,这时就需要提供外部修改api来实现。

添加CzarCacheController.cs对外部提供缓存更新相关接口,详细代码如下:

using Czar.Gateway.Authentication;
using Czar.Gateway.Configuration;
using Czar.Gateway.RateLimit;
using Czar.Gateway.Rpc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Ocelot.Configuration;
using Ocelot.Configuration.Creator;
using Ocelot.Configuration.Repository;
using System;
using System.Threading.Tasks;

namespace Czar.Gateway.Cache
{
    /// <summary>
    /// 提供外部缓存处理接口
    /// </summary>
    [Authorize]
    [Route("CzarCache")]
    public class CzarCacheController : Controller
    {
        private readonly CzarOcelotConfiguration _options;
        private readonly IClientAuthenticationRepository _clientAuthenticationRepository;
        private IFileConfigurationRepository _fileConfigurationRepository;
        private IInternalConfigurationCreator _internalConfigurationCreator;
        private readonly IClientRateLimitRepository _clientRateLimitRepository;
        private readonly IRpcRepository _rpcRepository;
        private readonly IMemoryCache _cache;
        public CzarCacheController(IClientAuthenticationRepository clientAuthenticationRepository, CzarOcelotConfiguration options,
          IFileConfigurationRepository fileConfigurationRepository,
          IInternalConfigurationCreator internalConfigurationCreator,
          IClientRateLimitRepository clientRateLimitRepository,
          IRpcRepository rpcRepository,
          IMemoryCache cache)
        {
            _clientAuthenticationRepository = clientAuthenticationRepository;
            _options = options;
            _fileConfigurationRepository = fileConfigurationRepository;
            _internalConfigurationCreator = internalConfigurationCreator;
            _clientRateLimitRepository = clientRateLimitRepository;
            _rpcRepository = rpcRepository;
            _cache = cache;
        }

        /// <summary>
        /// 更新客户端地址访问授权接口
        /// </summary>
        /// <param name="clientid">客户端ID</param>
        /// <param name="path">请求模板</param>
        /// <returns></returns>
        [HttpPost]
        [Route("ClientRule")]
        public async Task UpdateClientRuleCache(string clientid, string path)
        {
            var region = CzarCacheRegion.AuthenticationRegion;
            var key = CzarOcelotHelper.ComputeCounterKey(region, clientid, "", path);
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _clientAuthenticationRepository.ClientAuthenticationAsync(clientid, path);
            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, data); //加入redis缓存
                RedisHelper.Publish(key, data.ToJson()); //发布事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 更新网关配置路由信息
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        [Route("InternalConfiguration")]
        public async Task UpdateInternalConfigurationCache()
        {
            var key = CzarCacheRegion.InternalConfigurationRegion;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, "", key);
            var fileconfig = await _fileConfigurationRepository.Get();
            var internalConfig = await _internalConfigurationCreator.Create(fileconfig.Data);
            var config = (InternalConfiguration)internalConfig.Data;
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, config); //加入redis缓存
                RedisHelper.Publish(key, config.ToJson()); //发布事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 删除路由配合的缓存信息
        /// </summary>
        /// <param name="region">区域</param>
        /// <param name="downurl">下端路由</param>
        /// <returns></returns>
        [HttpPost]
        [Route("Response")]
        public async Task DeleteResponseCache(string region,string downurl)
        {
            var key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, downurl);
            if (_options.ClusterEnvironment)
            {
                await RedisHelper.DelAsync(key);
                RedisHelper.Publish(key, "");//发布时间
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 更新客户端限流规则缓存
        /// </summary>
        /// <param name="clientid">客户端ID</param>
        /// <param name="path">路由模板</param>
        /// <returns></returns>
        [HttpPost]
        [Route("RateLimitRule")]
        public async Task UpdateRateLimitRuleCache(string clientid, string path)
        {
            var region = CzarCacheRegion.RateLimitRuleModelRegion;
            var key = clientid + path;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _clientRateLimitRepository.CheckClientRateLimitAsync(clientid, path);
            var data = new RateLimitRuleModel() { RateLimit = result.RateLimit, rateLimitOptions = result.rateLimitOptions };
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, data); //加入redis缓存
                RedisHelper.Publish(key, data.ToJson()); //发布事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 更新客户端是否开启限流缓存
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        [HttpPost]
        [Route("ClientRole")]
        public async Task UpdateClientRoleCache(string path)
        {
            var region = CzarCacheRegion.ClientRoleModelRegion;
            var key = path;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _clientRateLimitRepository.CheckReRouteRuleAsync(path);
            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, data); //加入redis缓存
                RedisHelper.Publish(key, data.ToJson()); //发布事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 更新呢客户端路由白名单缓存
        /// </summary>
        /// <param name="clientid"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        [HttpPost]
        [Route("ClientReRouteWhiteList")]
        public async Task UpdateClientReRouteWhiteListCache(string clientid, string path)
        {
            var region = CzarCacheRegion.ClientReRouteWhiteListRegion;
            var key = clientid + path;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _clientRateLimitRepository.CheckClientReRouteWhiteListAsync(clientid, path);
            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, data); //加入redis缓存
                RedisHelper.Publish(key, data.ToJson()); //发布事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        [HttpPost]
        [Route("Rpc")]
        public async Task UpdateRpcCache(string UpUrl)
        {
            var region = CzarCacheRegion.RemoteInvokeMessageRegion;
            var key = UpUrl;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _rpcRepository.GetRemoteMethodAsync(UpUrl);
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, result); //加入redis缓存
                RedisHelper.Publish(key, result.ToJson()); //发布事件
            }
            else
            {
                _cache.Remove(key);
            }
        }
    }
}

现在基本实现整个缓存的更新策略,只要配合后台管理界面,在相关缓存原始数据发送变更时,调用对应接口即可完成redis缓存的更新,并自动通知集群的所有本机清理缓存等待重新获取。

接口的调用方式参考之前我写的配置信息接口变更那篇即可。

四、性能测试

完成了改造后,我们拿改造前网关、改造后网关、原始Ocelot、直接调用API四个环境分别测试性能指标,由于测试环境有效,我直接使用本机环境,然后是Apache ab测试工具测试下相关性能(本测试不一定准确,只作为参考指标),测试的方式是使用100个并发请求10000次,测试结果分别如下。

1、改造前网关性能

2、改造后网关性能

3、Ocelot默认网关性能

4、直接调用API性能

本测试仅供参考,因为由于网关和服务端都在本机环境部署,所以使用网关和不使用网关性能差别非常小,如果分开部署可能性别差别会明显写,这不是本篇讨论的重点。

从测试中可以看到,重构的网关改造前和改造后性能有2倍多的提升,且与原生的Ocelot性能非常接近。

五、总结

本篇主要讲解了如何使用redis的发布订阅来实现二级缓存功能,并提供了缓存的更新相关接口供外部程序调用,避免出现集群环境下无法更新缓存数据导致提取数据不一致情况,但是针对每个客户端独立限流这块集群环境目前还是采用的redis的方式未使用本地缓存,如果有写的不对或有更好方式的,也希望多提宝贵意见。

本篇相关源码地址:https://github.com/jinyancao/czar.gateway

整合一个基于c#的RSA私钥加密公钥解密的Helper类,含源码 - 清风333 - 博客园

mikel阅读(592)

来源: 整合一个基于c#的RSA私钥加密公钥解密的Helper类,含源码 – 清风333 – 博客园

 

最近在搞单点登录的设计,在设计中需要一个Token令牌的加密传输,这个令牌在整个连接单点的各个站中起着连接认证作用,如果被仿造将会有不可预计的损失,但是这个Token是要可逆的.然后我就找.net中的各种加密,各种找。

因为是可逆的,所以像那种md5,sha之类的不可逆加密就没法用了,然后可逆的加密主要是分为对称加密盒非对称加密:

对称加密:用加密的钥匙来解密,比如DES,AES的加解密

非对称加密:一个钥匙加密,用另一个钥匙解密,这个主要就是RSA比较成熟(点我看科普)

当然这么看来非对称加密更加适合我这个需求,然后我又各种找RSA,.NET中自己实现了加密RSA加密类RSACryptoServiceProvider,但是这个用起来着实不爽,公钥和私钥是用xml来显示,太长太大,而且由于没有实现一些标准,只能用公钥加密,私钥解密(这个XML种私钥中可以看出公钥),但是事实上RSA的一对有效密钥公钥加密私钥解密  和私钥加密公钥解密均可  我想要的是后面的效果啊,结果又继续各种找,在找了好久看不到希望之际时,在csdn和博客园上看到了这两篇文章:

基于私钥加密公钥解密的RSA算法C#实现

C#使用RSA私钥加密公钥解密的改进,解决特定情况下解密后出现乱码的问题

这两个正好可以实现我的需求,但是上面的代码都不全,说的倒是很清楚了,就是说利用一个开源的大数组处理类Bigingegter类配合RSA的算法自己显示RSA的加解密,问题也解决的很到位了,单个文章中提供的资料都不好进行加解密,但是合起来就ok了,他们那个用的不爽,自己就在他们的基础上又封装了一个帮助类:

(我仅仅只是整合了他们的代码,方便自己用而已,嘿嘿,核心代码还是他们的)

按他们说的,先要产生密钥对,当然这个密钥对不是随便写的,是需要大质数  又素数啥啥啥的,不过RSACryptoServiceProvider这个类里面可以生成这些,还不错,先看下我生成的密钥对:

 

复制代码
/// <summary>
        /// RSA加密的密匙结构  公钥和私匙
        /// </summary>
        public struct RSAKey
        {
            public string PublicKey { get; set; }
            public string PrivateKey { get; set; }
        }

        #region 得到RSA的解谜的密匙对
        /// <summary>
        /// 得到RSA的解谜的密匙对
        /// </summary>
        /// <returns></returns>
        public static RSAKey GetRASKey()
        {
            RSACryptoServiceProvider.UseMachineKeyStore = true;
            //声明一个指定大小的RSA容器
            RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(DWKEYSIZE);
            //取得RSA容易里的各种参数
            RSAParameters p = rsaProvider.ExportParameters(true);

            return new RSAKey()
            {
                PublicKey = ComponentKey(p.Exponent,p.Modulus),
                PrivateKey = ComponentKey(p.D,p.Modulus)
            };
        }
        #endregion
#region 组合解析密匙
        /// <summary>
        /// 组合成密匙字符串
        /// </summary>
        /// <param name="b1"></param>
        /// <param name="b2"></param>
        /// <returns></returns>
        private static string ComponentKey(byte[] b1, byte[] b2)
        {
            List<byte> list = new List<byte>();
            //在前端加上第一个数组的长度值 这样今后可以根据这个值分别取出来两个数组
            list.Add((byte)b1.Length);
            list.AddRange(b1);
            list.AddRange(b2);
            byte[] b = list.ToArray<byte>();
            return Convert.ToBase64String(b);
        }

        /// <summary>
        /// 解析密匙
        /// </summary>
        /// <param name="key">密匙</param>
        /// <param name="b1">RSA的相应参数1</param>
        /// <param name="b2">RSA的相应参数2</param>
        private static void ResolveKey(string key, out byte[] b1, out byte[] b2)
        {
            //从base64字符串 解析成原来的字节数组
            byte[] b = Convert.FromBase64String(key);
            //初始化参数的数组长度
            b1=new byte[b[0]];
            b2=new byte[b.Length-b[0]-1];
            //将相应位置是值放进相应的数组
            for (int n = 1, i = 0, j = 0; n < b.Length; n++)
            {
                if (n <= b[0])
                {
                    b1[i++] = b[n];
                }
                else {
                    b2[j++] = b[n];
                }
            }
        }
        #endregion
复制代码

主要是对生成的byte数组拼接成字符串(毕竟还是字符串给别人比较方便):因为公钥和私钥都是两个byte一起用才能加解密,所以将两个byte数组拼接成一个byte,把并添加一个标志位来使得后期可以解开,最后以base64字符串来传

有了自己封装的密钥之后

再封装类似AES,DES这种简单的入参进行加减密(不然传BitIngteger真心累)

复制代码
#region 字符串加密解密 公开方法
        /// <summary>
        /// 字符串加密
        /// </summary>
        /// <param name="source">源字符串 明文</param>
        /// <param name="key">密匙</param>
        /// <returns>加密遇到错误将会返回原字符串</returns>
        public static string EncryptString(string source,string key)
        {
            string encryptString = string.Empty;
            byte[] d;
            byte[] n;
            try
            {
                if (!CheckSourceValidate(source))
                {
                    throw new Exception("source string too long");
                }
                //解析这个密钥
                ResolveKey(key, out d, out n);
                BigInteger biN = new BigInteger(n);
                BigInteger biD = new BigInteger(d);
                encryptString= EncryptString(source, biD, biN);
            }
            catch
            {
                encryptString = source;
            }
            return encryptString;
        }

        /// <summary>
        /// 字符串解密
        /// </summary>
        /// <param name="encryptString">密文</param>
        /// <param name="key">密钥</param>
        /// <returns>遇到解密失败将会返回原字符串</returns>
        public static string DecryptString(string encryptString, string key)
        {
            string source = string.Empty;
            byte[] e;
            byte[] n;
            try
            {
                //解析这个密钥
                ResolveKey(key, out e, out n);
                BigInteger biE = new BigInteger(e);
                BigInteger biN = new BigInteger(n);
                source = DecryptString(encryptString, biE, biN);
            }
            catch {
                source = encryptString;
            }
            return source;
        }
        #endregion

        #region 字符串加密解密 私有  实现加解密的实现方法
        /// <summary>
        /// 用指定的密匙加密 
        /// </summary>
        /// <param name="source">明文</param>
        /// <param name="d">可以是RSACryptoServiceProvider生成的D</param>
        /// <param name="n">可以是RSACryptoServiceProvider生成的Modulus</param>
        /// <returns>返回密文</returns>
        private static string EncryptString(string source, BigInteger d, BigInteger n)
        {
            int len = source.Length;
            int len1 = 0;
            int blockLen = 0;
            if ((len % 128) == 0)
                len1 = len / 128;
            else
                len1 = len / 128 + 1;
            string block = "";
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < len1; i++)
            {
                if (len >= 128)
                    blockLen = 128;
                else
                    blockLen = len;
                block = source.Substring(i * 128, blockLen);
                byte[] oText = System.Text.Encoding.Default.GetBytes(block);
                BigInteger biText = new BigInteger(oText);
                BigInteger biEnText = biText.modPow(d, n);
                string temp = biEnText.ToHexString();
                result.Append(temp).Append("@");
                len -= blockLen;
            }
            return result.ToString().TrimEnd('@');
        }

        /// <summary>
        /// 用指定的密匙加密 
        /// </summary>
        /// <param name="source">密文</param>
        /// <param name="e">可以是RSACryptoServiceProvider生成的Exponent</param>
        /// <param name="n">可以是RSACryptoServiceProvider生成的Modulus</param>
        /// <returns>返回明文</returns>
        private static string DecryptString(string encryptString, BigInteger e, BigInteger n)
        {
            StringBuilder result = new StringBuilder();
            string[] strarr1 = encryptString.Split(new char[] { '@' }, StringSplitOptions.RemoveEmptyEntries);
            for (int i = 0; i < strarr1.Length; i++)
            {
                string block = strarr1[i];
                BigInteger biText = new BigInteger(block, 16);
                BigInteger biEnText = biText.modPow(e, n);
                string temp = System.Text.Encoding.Default.GetString(biEnText.getBytes());
                result.Append(temp);
            }
            return result.ToString();
        }
        #endregion
复制代码

这样的话 用户用起来就很方便了  直接源码/加密码   +密钥就可以加解密了

使用方式如下

复制代码
string str = "{\"sc\":\"his51\",\"no\":\"1\",\"na\":\"管理员\"}{\"sc\":\"@his51\",\"no\":\"1\",\"na\":\"管理员\"}{\"sc\":\"his51\",\"no\":\"1\",\"na\":\"管员\"}{\"sc\":\"his522";
            RSAHelper.RSAKey keyPair = RSAHelper.GetRASKey();
            Console.WriteLine("公钥:" + keyPair.PublicKey + "\r\n");
            Console.WriteLine("私钥:" + keyPair.PrivateKey + "\r\n");
            string en = RSAHelper.EncryptString(str, keyPair.PrivateKey);
            Console.WriteLine("加密后:"+en + "\r\n");
            Console.WriteLine("解密:"+RSAHelper.DecryptString(en, keyPair.PublicKey) + "\r\n");
            Console.ReadKey();
复制代码

是不是简单又熟悉 ,下面来看一下效果

具体的代码解释就不说了 都有注释了,下面给个源码吧,不然片段的代码拼接起来出错概率很高的

猛击我去下载RSA私钥加密 公钥解密的源码

【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期 - 金焰的世界 - 博客园

mikel阅读(425)

来源: 【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期 – 金焰的世界 – 博客园

上一篇我介绍了JWT的生成验证及流程内容,相信大家也对JWT非常熟悉了,今天将从一个小众的需求出发,介绍如何强制令牌过期的思路和实现过程。

.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。

一、前言

众所周知,IdentityServer4 默认支持两种类型的 Token,一种是 Reference Token,一种是 JWT Token 。前者的特点是 Token 的有效与否是由 Token 颁发服务集中化控制的,颁发的时候会持久化 Token,然后每次验证都需要将 Token 传递到颁发服务进行验证,是一种中心化的验证方式。JWT Token的特点与前者相反,每个资源服务不需要每次都要都去颁发服务进行验证 Token 的有效性验证,上一篇也介绍了,该 Token 由三部分组成,其中最后一部分包含了一个签名,是在颁发的时候采用非对称加密算法进行数据的签名,保证了 Token 的不可篡改性,校验时与颁发服务的交互,仅仅是获取公钥用于验证签名,且该公钥获取以后可以自己缓存,持续使用,不用再去交互获得,除非数字证书发生变化。

二、Reference Token的用法

上一篇已经介绍了JWT Token的整个生成过程,为了演示强制过期策略,这里需要了解下Reference Token是如何生成和存储的,这样可以帮助掌握IdentityServer4所有的工作方式。

1、新增测试客户端

由于我们已有数据库,为了方便演示,我直接使用SQL脚本新增。

--新建客户端(AccessTokenType 0 JWT 1 Reference Token)
INSERT INTO Clients(AccessTokenType,AccessTokenLifetime,ClientId,ClientName,Enabled) VALUES(1,3600,'clientref','测试Ref客户端',1);

-- SELECT * FROM Clients WHERE ClientId='clientref'

--2、添加客户端密钥,密码为(secreta) sha256
INSERT INTO ClientSecrets VALUES(23,'',null,'SharedSecret','2tytAAysa0zaDuNthsfLdjeEtZSyWw8WzbzM8pfTGNI=');

--3、增加客户端授权权限
INSERT INTO ClientGrantTypes VALUES(23,'client_credentials');

--4、增加客户端能够访问scope
INSERT INTO ClientScopes VALUES(23,'mpc_gateway');

这里添加了认证类型为Reference Token客户端为clientref,并分配了客户端授权和能访问的scope,然后我们使用PostMan测试下客户端。

如上图所示,可以正确的返回access_token,且有标记的过期时间。

2、如何校验token的有效性?

IdentityServer4给已经提供了Token的校验地址http://xxxxxx/connect/introspect,可以通过访问此地址来校验Token的有效性,使用前需要了解传输的参数和校验方式。

在授权篇开始时我介绍了IdentityServer4的源码剖析,相信都掌握了看源码的方式,这里就不详细介绍了。

核心代码为IntrospectionEndpoint,标注出校验的核心代码,用到的几个校验方式已经注释出来了。

private async Task<IEndpointResult> ProcessIntrospectionRequestAsync(HttpContext context)
{
    _logger.LogDebug("Starting introspection request.");

    // 校验ApiResources信息,支持 basic 和 form两种方式,和授权时一样
    var apiResult = await _apiSecretValidator.ValidateAsync(context);
    if (apiResult.Resource == null)
    {
        _logger.LogError("API unauthorized to call introspection endpoint. aborting.");
        return new StatusCodeResult(HttpStatusCode.Unauthorized);
    }

    var body = await context.Request.ReadFormAsync();
    if (body == null)
    {
        _logger.LogError("Malformed request body. aborting.");
        await _events.RaiseAsync(new TokenIntrospectionFailureEvent(apiResult.Resource.Name, "Malformed request body"));

        return new StatusCodeResult(HttpStatusCode.BadRequest);
    }

    // 验证access_token的有效性,根据
    _logger.LogTrace("Calling into introspection request validator: {type}", _requestValidator.GetType().FullName);
    var validationResult = await _requestValidator.ValidateAsync(body.AsNameValueCollection(), apiResult.Resource);
    if (validationResult.IsError)
    {
        LogFailure(validationResult.Error, apiResult.Resource.Name);
        await _events.RaiseAsync(new TokenIntrospectionFailureEvent(apiResult.Resource.Name, validationResult.Error));

        return new BadRequestResult(validationResult.Error);
    }

    // response generation
    _logger.LogTrace("Calling into introspection response generator: {type}", _responseGenerator.GetType().FullName);
    var response = await _responseGenerator.ProcessAsync(validationResult);

    // render result
    LogSuccess(validationResult.IsActive, validationResult.Api.Name);
    return new IntrospectionResult(response);
}

//校验Token有效性核心代码
public async Task<TokenValidationResult> ValidateAccessTokenAsync(string token, string expectedScope = null)
{
    _logger.LogTrace("Start access token validation");

    _log.ExpectedScope = expectedScope;
    _log.ValidateLifetime = true;

    TokenValidationResult result;

    if (token.Contains("."))
    {//jwt
        if (token.Length > _options.InputLengthRestrictions.Jwt)
        {
            _logger.LogError("JWT too long");

            return new TokenValidationResult
            {
                IsError = true,
                Error = OidcConstants.ProtectedResourceErrors.InvalidToken,
                ErrorDescription = "Token too long"
            };
        }

        _log.AccessTokenType = AccessTokenType.Jwt.ToString();
        result = await ValidateJwtAsync(
            token,
            string.Format(Constants.AccessTokenAudience, _context.HttpContext.GetIdentityServerIssuerUri().EnsureTrailingSlash()),
            await _keys.GetValidationKeysAsync());
    }
    else
    {//Reference token
        if (token.Length > _options.InputLengthRestrictions.TokenHandle)
        {
            _logger.LogError("token handle too long");

            return new TokenValidationResult
            {
                IsError = true,
                Error = OidcConstants.ProtectedResourceErrors.InvalidToken,
                ErrorDescription = "Token too long"
            };
        }

        _log.AccessTokenType = AccessTokenType.Reference.ToString();
        result = await ValidateReferenceAccessTokenAsync(token);
    }

    _log.Claims = result.Claims.ToClaimsDictionary();

    if (result.IsError)
    {
        return result;
    }

    // make sure client is still active (if client_id claim is present)
    var clientClaim = result.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.ClientId);
    if (clientClaim != null)
    {
        var client = await _clients.FindEnabledClientByIdAsync(clientClaim.Value);
        if (client == null)
        {
            _logger.LogError("Client deleted or disabled: {clientId}", clientClaim.Value);

            result.IsError = true;
            result.Error = OidcConstants.ProtectedResourceErrors.InvalidToken;
            result.Claims = null;

            return result;
        }
    }

    // make sure user is still active (if sub claim is present)
    var subClaim = result.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Subject);
    if (subClaim != null)
    {
        var principal = Principal.Create("tokenvalidator", result.Claims.ToArray());

        if (result.ReferenceTokenId.IsPresent())
        {
            principal.Identities.First().AddClaim(new Claim(JwtClaimTypes.ReferenceTokenId, result.ReferenceTokenId));
        }

        var isActiveCtx = new IsActiveContext(principal, result.Client, IdentityServerConstants.ProfileIsActiveCallers.AccessTokenValidation);
        await _profile.IsActiveAsync(isActiveCtx);

        if (isActiveCtx.IsActive == false)
        {
            _logger.LogError("User marked as not active: {subject}", subClaim.Value);

            result.IsError = true;
            result.Error = OidcConstants.ProtectedResourceErrors.InvalidToken;
            result.Claims = null;

            return result;
        }
    }

    // check expected scope(s)
    if (expectedScope.IsPresent())
    {
        var scope = result.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Scope && c.Value == expectedScope);
        if (scope == null)
        {
            LogError(string.Format("Checking for expected scope {0} failed", expectedScope));
            return Invalid(OidcConstants.ProtectedResourceErrors.InsufficientScope);
        }
    }

    _logger.LogDebug("Calling into custom token validator: {type}", _customValidator.GetType().FullName);
    var customResult = await _customValidator.ValidateAccessTokenAsync(result);

    if (customResult.IsError)
    {
        LogError("Custom validator failed: " + (customResult.Error ?? "unknown"));
        return customResult;
    }

    // add claims again after custom validation
    _log.Claims = customResult.Claims.ToClaimsDictionary();

    LogSuccess();
    return customResult;
}

有了上面的校验代码,就可以很容易掌握使用的参数和校验的方式,现在我们就分别演示JWT TokenReference token两个校验方式及返回的值。

首先需要新增资源端的授权记录,因为校验时需要,我们就以mpc_gateway为例新增授权记录,为了方便演示,直接使用SQL语句。

-- SELECT * FROM dbo.ApiResources WHERE Name='mpc_gateway'
INSERT INTO dbo.ApiSecrets VALUES(28,NULL,NULL,'SharedSecret','2tytAAysa0zaDuNthsfLdjeEtZSyWw8WzbzM8pfTGNI=');

首先我们测试刚才使用Reference token生成的access_token,参数如下图所示。

查看是否校验成功,从返回的状态码和active结果判断,如果为true校验成功,如果为false或者401校验失败。

我们直接从数据库里删除刚才授权的记录,然后再次提交查看结果,返回结果校验失败。

  DELETE FROM PersistedGrants WHERE ClientId='clientref'

然后我们校验下Jwt Token,同样的方式,先生成jwt token,然后进行校验,结果如下图所示。

可以得到预期结果。

三、强制过期的方式

1、简易黑名单模式

在每次有Token请求时,资源服务器对请求的Token进行校验,在校验有效性校验通过后,再在黑名单里校验是否强制过期,如果存在黑名单里,返回授权过期提醒。资源服务器提示Token无效。注意由于每次请求都会校验Token的有效性,因此黑名单最好使用比如Redis缓存进行保存。

实现方式:

此种方式只需要重写Token验证方式即可实现。

优点

实现简单,改造少。

缺点

1、不好维护黑名单列表

2、对认证服务器请求压力太大

2、策略黑名单模式

建议黑名单有一个最大的弊端是每次请求都需要对服务器进行访问,会对服务器端造成很大的请求压力,而实际请求数据中99%都是正常访问,对于可疑的请求我们才需要进行服务器端验证,所以我们要在客户端校验出可疑的请求再提交到服务器校验,可以在Claim里增加客户端IP信息,当请求的客户端IP和Token里的客户端IP不一致时,我们标记为可疑Token,这时候再发起Token校验请求,校验Token是否过期,后续流程和简易黑名单模式完成一致。

实现方式

此种方式需要增加Token生成的Claim,增加自定义的ip的Claim字段,然后再重写验证方式。

优点

可以有效的减少服务器端压力

缺点

不好维护黑名单列表

3、强化白名单模式

通常不管使用客户端、密码、混合模式等方式登录,都可以获取到有效的Token,这样会造成签发的不同Token可以重复使用,且很难把这些历史的Token手工加入黑名单里,防止被其他人利用。那如何保证一个客户端同一时间点只有一个有效Token呢?我们只需要把最新的Token加入白名单,然后验证时直接验证白名单,未命中白名单校验失败。校验时使用策略黑名单模式,满足条件再请求验证,为了减轻认证服务器的压力,可以根据需求在本地缓存一定时间(比如10分钟)。

实现方式

此种方式需要重写Token生成方式,重写自定义验证方式。

优点

服务器端请求不频繁,验证块,自动管理黑名单。

缺点

实现起来比较改造的东西较多

综上分析后,为了网关的功能全面和性能,建议采用强化白名单模式来实现强制过期策略。

四、强制过期的实现

1.增加白名单功能

为了增加强制过期功能,我们需要在配置文件里标记是否开启此功能,默认设置为不开启。

/// <summary>
/// 金焰的世界
/// 2018-12-03
/// 配置存储信息
/// </summary>
public class DapperStoreOptions
{
    /// <summary>
    /// 是否启用自定清理Token
    /// </summary>
    public bool EnableTokenCleanup { get; set; } = false;

    /// <summary>
    /// 清理token周期(单位秒),默认1小时
    /// </summary>
    public int TokenCleanupInterval { get; set; } = 3600;

    /// <summary>
    /// 连接字符串
    /// </summary>
    public string DbConnectionStrings { get; set; }

    /// <summary>
    /// 是否启用强制过期策略,默认不开启
    /// </summary>
    public bool EnableForceExpire { get; set; } = false;
    
    /// <summary>
    /// Redis缓存连接
    /// </summary>
    public List<string> RedisConnectionStrings { get; set; }
}

然后重写Token生成策略,增加白名单功能,并使用Redis存储白名单。白名单的存储的Key格式为clientId+sub+amr,详细实现代码如下。

using Czar.IdentityServer4.Options;
using IdentityModel;
using IdentityServer4.ResponseHandling;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace Czar.IdentityServer4.ResponseHandling
{
    public class CzarTokenResponseGenerator : TokenResponseGenerator
    {

        private readonly DapperStoreOptions _config;
        private readonly ICache<CzarToken> _cache;
        public CzarTokenResponseGenerator(ISystemClock clock, ITokenService tokenService, IRefreshTokenService refreshTokenService, IResourceStore resources, IClientStore clients, ILogger<TokenResponseGenerator> logger, DapperStoreOptions config, ICache<CzarToken> cache) : base(clock, tokenService, refreshTokenService, resources, clients, logger)
        {
            _config = config;
            _cache = cache;
        }

        /// <summary>
        /// Processes the response.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public override async Task<TokenResponse> ProcessAsync(TokenRequestValidationResult request)
        {
            var result = new TokenResponse();
            switch (request.ValidatedRequest.GrantType)
            {
                case OidcConstants.GrantTypes.ClientCredentials:
                    result = await ProcessClientCredentialsRequestAsync(request);
                    break;
                case OidcConstants.GrantTypes.Password:
                    result = await ProcessPasswordRequestAsync(request);
                    break;
                case OidcConstants.GrantTypes.AuthorizationCode:
                    result = await ProcessAuthorizationCodeRequestAsync(request);
                    break;
                case OidcConstants.GrantTypes.RefreshToken:
                    result = await ProcessRefreshTokenRequestAsync(request);
                    break;
                default:
                    result = await ProcessExtensionGrantRequestAsync(request);
                    break;
            }
            if (_config.EnableForceExpire)
            {//增加白名单
                var token = new CzarToken();
                string key = request.ValidatedRequest.Client.ClientId;
                var _claim = request.ValidatedRequest.Subject?.FindFirst(e => e.Type == "sub");
                if (_claim != null)
                {
                    //提取amr
                    var amrval = request.ValidatedRequest.Subject.FindFirst(p => p.Type == "amr");
                    if (amrval != null)
                    {
                        key += amrval.Value;
                    }
                    key += _claim.Value;
                }
                //加入缓存
                if (!String.IsNullOrEmpty(result.AccessToken))
                {
                    token.Token = result.AccessToken;
                    await _cache.SetAsync(key, token, TimeSpan.FromSeconds(result.AccessTokenLifetime));
                }
            }
            return result;
        }
    }
}

然后定一个通用缓存方法,默认使用Redis实现。

using Czar.IdentityServer4.Options;
using IdentityServer4.Services;
using System;
using System.Threading.Tasks;

namespace Czar.IdentityServer4.Caches
{
    /// <summary>
    /// 金焰的世界
    /// 2019-01-11
    /// 使用Redis存储缓存
    /// </summary>
    public class CzarRedisCache<T> : ICache<T>
        where T : class
    {
        private const string KeySeparator = ":";
        public CzarRedisCache(DapperStoreOptions configurationStoreOptions)
        {
            CSRedis.CSRedisClient csredis;
            if (configurationStoreOptions.RedisConnectionStrings.Count == 1)
            {
                //普通模式
                csredis = new CSRedis.CSRedisClient(configurationStoreOptions.RedisConnectionStrings[0]);
            }
            else
            {
                csredis = new CSRedis.CSRedisClient(null, configurationStoreOptions.RedisConnectionStrings.ToArray());
            }
            //初始化 RedisHelper
            RedisHelper.Initialization(csredis);
        }

        private string GetKey(string key)
        {
            return typeof(T).FullName + KeySeparator + key;
        }

        public async Task<T> GetAsync(string key)
        {
            key = GetKey(key);
            var result = await RedisHelper.GetAsync<T>(key);
            return result;
        }

        public async Task SetAsync(string key, T item, TimeSpan expiration)
        {
            key = GetKey(key);
            await RedisHelper.SetAsync(key, item, (int)expiration.TotalSeconds);
        }
    }
}

然后重新注入下ITokenResponseGenerator实现。

builder.Services.AddSingleton<ITokenResponseGenerator, CzarTokenResponseGenerator>();
builder.Services.AddTransient(typeof(ICache<>), typeof(CzarRedisCache<>));

现在我们来测试下生成Token,查看Redis里是否生成了白名单?

Reference Token生成

客户端模式生成

密码模式生成

从结果中可以看出来,无论那种认证方式,都可以生成白名单,且只保留最新的报名单记录。

2.改造校验接口来适配白名单校验

前面介绍了认证原理后,实现校验非常简单,只需要重写下IIntrospectionRequestValidator接口即可,增加白名单校验策略,详细实现代码如下。

using Czar.IdentityServer4.Options;
using Czar.IdentityServer4.ResponseHandling;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.Extensions.Logging;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;

namespace Czar.IdentityServer4.Validation
{
    /// <summary>
    /// 金焰的世界
    /// 2019-01-14
    /// Token请求校验增加白名单校验
    /// </summary>
    public class CzarIntrospectionRequestValidator : IIntrospectionRequestValidator
    {
        private readonly ILogger _logger;
        private readonly ITokenValidator _tokenValidator;
        private readonly DapperStoreOptions _config;
        private readonly ICache<CzarToken> _cache;
        public CzarIntrospectionRequestValidator(ITokenValidator tokenValidator, DapperStoreOptions config, ICache<CzarToken> cache, ILogger<CzarIntrospectionRequestValidator> logger)
        {
            _tokenValidator = tokenValidator;
            _config = config;
            _cache = cache;
            _logger = logger;
        }

        public async Task<IntrospectionRequestValidationResult> ValidateAsync(NameValueCollection parameters, ApiResource api)
        {
            _logger.LogDebug("Introspection request validation started.");

            // retrieve required token
            var token = parameters.Get("token");
            if (token == null)
            {
                _logger.LogError("Token is missing");

                return new IntrospectionRequestValidationResult
                {
                    IsError = true,
                    Api = api,
                    Error = "missing_token",
                    Parameters = parameters
                };
            }

            // validate token
            var tokenValidationResult = await _tokenValidator.ValidateAccessTokenAsync(token);

            // invalid or unknown token
            if (tokenValidationResult.IsError)
            {
                _logger.LogDebug("Token is invalid.");

                return new IntrospectionRequestValidationResult
                {
                    IsActive = false,
                    IsError = false,
                    Token = token,
                    Api = api,
                    Parameters = parameters
                };
            }

            _logger.LogDebug("Introspection request validation successful.");

            if (_config.EnableForceExpire)
            {//增加白名单校验判断
                var _key = tokenValidationResult.Claims.FirstOrDefault(t => t.Type == "client_id").Value;
                var _amr = tokenValidationResult.Claims.FirstOrDefault(t => t.Type == "amr");
                if (_amr != null)
                {
                    _key += _amr.Value;
                }
                var _sub = tokenValidationResult.Claims.FirstOrDefault(t => t.Type == "sub");
                if (_sub != null)
                {
                    _key += _sub.Value;
                }
                var _token = await _cache.GetAsync(_key);
                if (_token == null || _token.Token != token)
                {//已加入黑名单
                    _logger.LogDebug("Token已经强制失效");
                    return new IntrospectionRequestValidationResult
                    {
                        IsActive = false,
                        IsError = false,
                        Token = token,
                        Api = api,
                        Parameters = parameters
                    };
                }
            }
            // valid token
            return new IntrospectionRequestValidationResult
            {
                IsActive = true,
                IsError = false,
                Token = token,
                Claims = tokenValidationResult.Claims,
                Api = api,
                Parameters = parameters
            };
        }
    }
}

然后把接口重新注入,即可实现白名单的校验功能。

 builder.Services.AddTransient<IIntrospectionRequestValidator, CzarIntrospectionRequestValidator>();

只要几句代码就完成了功能校验,现在可以使用PostMan测试白名单功能。首先使用刚生成的Token测试,可以正确的返回结果。

紧接着,我从新生成Token,然后再次请求,结果如下图所示。

发现校验失败,提示Token已经失效,和我们预期的结果完全一致。

现在获取的Token只有最新的是白名单,其他的有效信息自动加入认定为黑名单,如果想要强制token失效,只要删除或修改Redis值即可。

有了这个认证结果,现在只需要在认证策略里增加合理的校验规则即可,比如5分钟请求一次验证或者使用ip策略发起校验等,这里就比较简单了,就不一一实现了,如果在使用中遇到问题可以联系我。

五、总结与思考

本篇我介绍了IdentityServer4里Token认证的接口及实现过程,然后介绍强制有效Token过期的实现思路,并使用了白名单模式实现了强制过期策略。但是这种实现方式不一定是非常合理的实现方式,也希望有更好实现的朋友批评指正并告知本人。

实际生产环境中如果使用JWT Token,建议还是使用Token颁发的过期策略来强制Token过期,比如对安全要求较高的设置几分钟或者几十分钟过期等,避免Token泄漏造成的安全问题。

至于单机登录,其实只要开启强制过期策略就基本实现了,因为只要最新的登录会自动把之前的登录Token强制失效,如果再配合signalr强制下线即可。

项目源代码地址:https://github.com/jinyancao/Czar.IdentityServer4

【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程 - 金焰的世界 - 博客园

mikel阅读(356)

来源: 【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程 – 金焰的世界 – 博客园

上篇文章介绍了基于Ids4密码授权模式,从使用场景、原理分析、自定义帐户体系集成完整的介绍了密码授权模式的内容,并最后给出了三个思考问题,本篇就针对第一个思考问题详细的讲解下Ids4是如何生成access_token的,如何验证access_token的有效性,最后我们使用.net webapi来实现一个外部接口(本来想用JAVA来实现的,奈何没学好,就当抛砖引玉吧,有会JAVA的朋友根据我写的案例使用JAVA来实现一个案例)。

.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。

一、JWT简介

  1. 什么是JWT?
    JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
  2. 什么时候使用JWT?

1)、认证,这是比较常见的使用场景,只要用户登录过一次系统,之后的请求都会包含签名出来的token,通过token也可以用来实现单点登录。

2)、交换信息,通过使用密钥对来安全的传送信息,可以知道发送者是谁、放置消息是否被篡改。

  1. JWT的结构是什么样的?

JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:

  • Header
  • Payload
  • Signature

Header
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

例如:

{
    "alg": "RS256",
    "typ": "JWT"
}

然后,用Base64对这个JSON编码就得到JWT的第一部分

Payload

JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。

  • Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
  • Public claims : 可以随意定义。
  • Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。

下面是一个例子:

{
 "nbf": 1545919058,
 "exp": 1545922658,
 "iss": "http://localhost:7777",
 "aud": [
     "http://localhost:7777/resources",
     "mpc_gateway"
 ],
 "client_id": "clienta",
 "sub": "1",
 "auth_time": 1545919058,
 "idp": "local",
 "nickname": "金焰的世界",
 "email": "541869544@qq.com",
 "mobile": "13888888888",
 "scope": [
     "mpc_gateway",
     "offline_access"
 ],
 "amr": [
     "pwd"
 ]
}

对payload进行Base64编码就得到JWT的第二部分

注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。

Signature

为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的				那个,然对它们签名即可。

例如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。

二、IdentityServer4是如何生成jwt的?

在了解了JWT的基本概念介绍后,我们要知道JWT是如何生成的,加密的方式是什么,我们如何使用自己的密钥进行加密。

IdentityServer4的加密方式?

Ids4目前使用的是RS256非对称方式,使用私钥进行签名,然后客户端通过公钥进行验签。可能有的人会问,我们在生成Ids4时,也没有配置证书,为什么也可以运行起来呢?这里就要讲解证书的使用,以及Ids4使用证书的加密流程。

1、加载证书

Ids4默认使用临时证书来进行token的生成,使用代码 .AddDeveloperSigningCredential(),这里会自动给生成tempkey.rsa证书文件,所以项目如果使用默认配置的根目录可以查看到此文件,实现代码如下:

public static IIdentityServerBuilder AddDeveloperSigningCredential(this IIdentityServerBuilder builder, bool persistKey = true, string filename = null)
{
    if (filename == null)
    {
        filename = Path.Combine(Directory.GetCurrentDirectory(), "tempkey.rsa");
    }

    if (File.Exists(filename))
    {
        var keyFile = File.ReadAllText(filename);
        var tempKey = JsonConvert.DeserializeObject<TemporaryRsaKey>(keyFile, new JsonSerializerSettings { ContractResolver = new RsaKeyContractResolver() });

        return builder.AddSigningCredential(CreateRsaSecurityKey(tempKey.Parameters, tempKey.KeyId));
    }
    else
    {
        var key = CreateRsaSecurityKey();

        RSAParameters parameters;

        if (key.Rsa != null)
            parameters = key.Rsa.ExportParameters(includePrivateParameters: true);
        else
            parameters = key.Parameters;

        var tempKey = new TemporaryRsaKey
        {
            Parameters = parameters,
            KeyId = key.KeyId
        };

        if (persistKey)
        {
            File.WriteAllText(filename, JsonConvert.SerializeObject(tempKey, new JsonSerializerSettings { ContractResolver = new RsaKeyContractResolver() }));
        }

        return builder.AddSigningCredential(key);
    }
}

这也就可以理解为什么没有配置证书也一样可以使用了。

注意:在生产环境我们最好使用自己配置的证书。

如果我们已经有证书了,可以使用如下代码实现,至于证书是如何生成的,网上资料很多,这里就不介绍了。

 .AddSigningCredential(new X509Certificate2(Path.Combine(basePath,"test.pfx"),"123456"));

然后注入证书相关信息,代码如下:

builder.Services.AddSingleton<ISigningCredentialStore>(new DefaultSigningCredentialsStore(credential));
            builder.Services.AddSingleton<IValidationKeysStore>(new DefaultValidationKeysStore(new[] { credential.Key }));

后面就可以在项目里使用证书的相关操作了,比如加密、验签等。

2、使用证书加密

上篇我介绍了密码授权模式,详细的讲解了流程,当所有信息校验通过,Claim生成完成后,就开始生成token了,核心代码如下。

public virtual async Task<string> CreateTokenAsync(Token token)
{
    var header = await CreateHeaderAsync(token);
    var payload = await CreatePayloadAsync(token);
    return await CreateJwtAsync(new JwtSecurityToken(header, payload));
}
//使用配置的证书生成JWT头部
protected virtual async Task<JwtHeader> CreateHeaderAsync(Token token)
{
    var credential = await Keys.GetSigningCredentialsAsync();

    if (credential == null)
    {
        throw new InvalidOperationException("No signing credential is configured. Can't create JWT token");
    }

    var header = new JwtHeader(credential);

    // emit x5t claim for backwards compatibility with v4 of MS JWT library
    if (credential.Key is X509SecurityKey x509key)
    {
        var cert = x509key.Certificate;
        if (Clock.UtcNow.UtcDateTime > cert.NotAfter)
        {//如果证书过期提示
            Logger.LogWarning("Certificate {subjectName} has expired on {expiration}", cert.Subject, cert.NotAfter.ToString(CultureInfo.InvariantCulture));
        }
        header["x5t"] = Base64Url.Encode(cert.GetCertHash());
    }

    return header;
}
//生成内容
public static JwtPayload CreateJwtPayload(this Token token, ISystemClock clock, ILogger logger)
{
    var payload = new JwtPayload(
        token.Issuer,
        null,
        null,
        clock.UtcNow.UtcDateTime,
        clock.UtcNow.UtcDateTime.AddSeconds(token.Lifetime));

    foreach (var aud in token.Audiences)
    {
        payload.AddClaim(new Claim(JwtClaimTypes.Audience, aud));
    }

    var amrClaims = token.Claims.Where(x => x.Type == JwtClaimTypes.AuthenticationMethod);
    var scopeClaims = token.Claims.Where(x => x.Type == JwtClaimTypes.Scope);
    var jsonClaims = token.Claims.Where(x => x.ValueType == IdentityServerConstants.ClaimValueTypes.Json);

    var normalClaims = token.Claims
        .Except(amrClaims)
        .Except(jsonClaims)
        .Except(scopeClaims);

    payload.AddClaims(normalClaims);

    // scope claims
    if (!scopeClaims.IsNullOrEmpty())
    {
        var scopeValues = scopeClaims.Select(x => x.Value).ToArray();
        payload.Add(JwtClaimTypes.Scope, scopeValues);
    }

    // amr claims
    if (!amrClaims.IsNullOrEmpty())
    {
        var amrValues = amrClaims.Select(x => x.Value).Distinct().ToArray();
        payload.Add(JwtClaimTypes.AuthenticationMethod, amrValues);
    }

    // deal with json types
    // calling ToArray() to trigger JSON parsing once and so later 
    // collection identity comparisons work for the anonymous type
    try
    {
        var jsonTokens = jsonClaims.Select(x => new { x.Type, JsonValue = JRaw.Parse(x.Value) }).ToArray();

        var jsonObjects = jsonTokens.Where(x => x.JsonValue.Type == JTokenType.Object).ToArray();
        var jsonObjectGroups = jsonObjects.GroupBy(x => x.Type).ToArray();
        foreach (var group in jsonObjectGroups)
        {
            if (payload.ContainsKey(group.Key))
            {
                throw new Exception(string.Format("Can't add two claims where one is a JSON object and the other is not a JSON object ({0})", group.Key));
            }

            if (group.Skip(1).Any())
            {
                // add as array
                payload.Add(group.Key, group.Select(x => x.JsonValue).ToArray());
            }
            else
            {
                // add just one
                payload.Add(group.Key, group.First().JsonValue);
            }
        }

        var jsonArrays = jsonTokens.Where(x => x.JsonValue.Type == JTokenType.Array).ToArray();
        var jsonArrayGroups = jsonArrays.GroupBy(x => x.Type).ToArray();
        foreach (var group in jsonArrayGroups)
        {
            if (payload.ContainsKey(group.Key))
            {
                throw new Exception(string.Format("Can't add two claims where one is a JSON array and the other is not a JSON array ({0})", group.Key));
            }

            var newArr = new List<JToken>();
            foreach (var arrays in group)
            {
                var arr = (JArray)arrays.JsonValue;
                newArr.AddRange(arr);
            }

            // add just one array for the group/key/claim type
            payload.Add(group.Key, newArr.ToArray());
        }

        var unsupportedJsonTokens = jsonTokens.Except(jsonObjects).Except(jsonArrays);
        var unsupportedJsonClaimTypes = unsupportedJsonTokens.Select(x => x.Type).Distinct();
        if (unsupportedJsonClaimTypes.Any())
        {
            throw new Exception(string.Format("Unsupported JSON type for claim types: {0}", unsupportedJsonClaimTypes.Aggregate((x, y) => x + ", " + y)));
        }

        return payload;
    }
    catch (Exception ex)
    {
        logger.LogCritical(ex, "Error creating a JSON valued claim");
        throw;
    }
}
//生成最终的Token
protected virtual Task<string> CreateJwtAsync(JwtSecurityToken jwt)
{
    var handler = new JwtSecurityTokenHandler();
    return Task.FromResult(handler.WriteToken(jwt));
}

知道了这些原理后,我们就能清楚的知道access_token都放了那些东西,以及我们可以如何来验证生成的Token

三、如何验证access_token的有效性?

知道了如何生成后,最主要的目的还是要直接我们服务端是如何来保护接口安全的,为什么服务端只要加入下代码就能够保护配置的资源呢?

services.AddAuthentication("Bearer")
        .AddIdentityServerAuthentication(options =>
            {
               options.Authority ="http://localhost:7777";
               options.RequireHttpsMetadata = false;
               options.ApiName = "Api1";
               options.SaveToken = true;
            });
//启用授权 
app.UseAuthentication();

在理解这个前,我们需要了解系统做的验证流程,这里使用一张图可以很好的理解流程了。

img
看完后是不是豁然开朗?这里就可以很好的理解/.well-known/openid-configuration/jwks原来就是证书的公钥信息,是通过访问/.well-known/openid-configuration暴露给所有的客户端使用,安全性是用过非对称加密的原理保证,私钥加密的信息,公钥只能验证,所以也不存在密钥泄漏问题。

虽然只是短短的几句代码,就做了那么多事情,这说明Ids4封装的好,减少了我们很多编码工作。这是有人会问,那如果我们的项目不是.netcore的,那如何接入到网关呢?

网上有一个Python例子,用 Identity Server 4 (JWKS 端点和 RS256 算法) 来保护 Python web api.

本来准备使用Java来实现,好久没摸已经忘了怎么写了,留给会java的朋友实现吧,原理都是一样。

下面我就已webapi为例来开发服务端接口,然后使用Ids4来保护接口内容。

新建一个webapi项目,项目名称Czar.AuthPlatform.WebApi,为了让输出的结果为json,我们需要在WebApiConfig增加config.Formatters.Remove(config.Formatters.XmlFormatter);代码,然后修改默认的控制器ValuesController,修改代码如下。

[Ids4Auth("http://localhost:6611", "mpc_gateway")]
public IEnumerable<string> Get()
{
      var Context = RequestContext.Principal; 
      return new string[] { "WebApi Values" };
}

为了保护api安全,我们需要增加一个身份验证过滤器,实现代码如下。

using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Czar.AuthPlatform.WebApi
{
    public class Ids4AuthAttribute : AuthorizationFilterAttribute
    {
        /// <summary>
        /// 认证服务器地址
        /// </summary>
        private string issUrl = "";
        /// <summary>
        /// 保护的API名称
        /// </summary>
        private string apiName = "";

        public Ids4AuthAttribute(string IssUrl,string ApiName)
        {
            issUrl = IssUrl;
            apiName = ApiName;
        }
        /// <summary>
        /// 重写验证方式
        /// </summary>
        /// <param name="actionContext"></param>
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            try
            {
                var access_token = actionContext.Request.Headers.Authorization?.Parameter; //获取请求的access_token
                if (String.IsNullOrEmpty(access_token))
                {//401
                    actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
                    actionContext.Response.Content = new StringContent("{\"errcode\":401,\"errmsg\":\"未授权\"}");
                }
                else
                {//开始验证请求的Token是否合法
                    //1、获取公钥
                    var httpclient = new HttpClient();
                    var jwtKey= httpclient.GetStringAsync(issUrl + "/.well-known/openid-configuration/jwks").Result;
                    //可以在此处缓存jwtkey,不用每次都获取。
                    var Ids4keys = JsonConvert.DeserializeObject<Ids4Keys>(jwtKey);
                    var jwk = Ids4keys.keys;
                    var parameters = new TokenValidationParameters
                    { //可以增加自定义的验证项目
                        ValidIssuer = issUrl,
                        IssuerSigningKeys = jwk ,
                        ValidateLifetime = true,
                        ValidAudience = apiName
                    };
                    var handler = new JwtSecurityTokenHandler();
                    //2、使用公钥校验是否合法,如果验证失败会抛出异常
                    var id = handler.ValidateToken(access_token, parameters, out var _);
                    //请求的内容保存
                    actionContext.RequestContext.Principal = id;
                }
            }
            catch(Exception ex)
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
                actionContext.Response.Content = new StringContent("{\"errcode\":401,\"errmsg\":\"未授权\"}");
            }
        }
    }

    public class Ids4Keys
    {
        public JsonWebKey[] keys { get; set; }
    }
}

代码非常简洁,就实现了基于Ids4的访问控制,现在我们开始使用PostMan来测试接口地址。

我们直接请求接口地址,返回401未授权。

然后我使用Ids4生成的access_token再次测试,可以得到我们预期结果。

为了验证是不是任何地方签发的token都可以通过验证,我使用其他项目生成的access_token来测试,发现提示的401未授权,可以达到我们预期结果。

现在就可以开心的使用我们熟悉的webapi开发我们的接口了,需要验证的地方增加类似[Ids4Auth("http://localhost:6611", "mpc_gateway")]代码即可。

使用其他语言实现的原理基本一致,就是公钥来验签,只要通过验证证明是允许访问的请求,由于公钥一直不变(除非认证服务器更新了证书),所以我们请求到后可以缓存到本地,这样验签时可以省去每次都获取公钥这步操作。

四、总结

本篇我们介绍了JWT的基本原理和Ids4JWT实现方式,然后使用.NET webapi实现了使用Ids4保护接口,其他语言实现方式一样,这样我们就可以把网关部署后,后端服务使用任何语言开发,然后接入到网关即可。

有了这些知识点,感觉是不是对Ids4的理解更深入了呢?JWT确实方便,但是有些特殊场景是我们希望Token在有效期内通过人工配置的方式立即失效,如果按照现有Ids4验证方式是没有办法做到,那该如何实现呢?我将会在下一篇来介绍如何实现强制token失效,敬请期待吧。

【极速下载】gradle各版本快速下载地址大全_踮脚敲代码-CSDN博客_csdn免积分下载器

mikel阅读(554)

来源: 【极速下载】gradle各版本快速下载地址大全_踮脚敲代码-CSDN博客_csdn免积分下载器

版本 CSDN下载 网盘下载 更新时间
gradle-7.3.3-all 积分下载 211222
gradle-7.3.2-all 积分下载 211215
gradle-7.3.1-all 积分下载 211201
gradle-7.3-all 积分下载 211109
gradle-7.2-all 积分下载 网盘下载 、提取码:7200 210817
gradle-7.1.1-all 积分下载 网盘下载 、提取码:7110 210702
gradle-7.1-all 积分下载 网盘下载 、提取码:7100 210614
gradle-7.0.2-all 积分下载 网盘下载 、提取码:7020 210514
gradle-7.0.1-all 积分下载 网盘下载 、提取码:7010 210510
gradle-7.0-all 积分下载 网盘下载 、提取码:7000 210410
gradle-6.9.1-all 积分下载 网盘下载 、提取码:6910 210820
gradle-6.9-all 积分下载 网盘下载 、提取码:6900 210507
gradle-6.8.3-all 积分下载 网盘下载 、提取码:6830 210222
gradle-6.8.2-all 积分下载 网盘下载 、提取码:6820 210205
gradle-6.8.1-all 积分下载 网盘下载 、提取码:6810 210122
gradle-6.8-all 积分下载 网盘下载 、提取码:6800 210108
gradle-6.7.1-all 积分下载 网盘下载 、提取码:6710 201116
gradle-6.7-all 积分下载 网盘下载 、提取码:6700 201014
gradle-6.6.1-all 积分下载 网盘下载 、提取码:6600 200825
gradle-6.6-all 积分下载 网盘下载 、提取码:6600 200811
gradle-6.5.1-all 积分下载 网盘下载 、提取码:6510 200630
gradle-6.5-all 积分下载 网盘下载 、提取码:6500 200602
gradle-6.4.1-all 积分下载 网盘下载 、提取码:6410 200515
gradle-6.4-all 积分下载 网盘下载 、提取码:6400 200505
gradle-6.3-all 积分下载 网盘下载 、提取码:6300 200324
gradle-6.2.2-all 积分下载 网盘下载 、提取码:6220 200304
gradle-6.2.1-all 积分下载 网盘下载 、提取码:6210 200224
gradle-6.2-all 积分下载 网盘下载 、提取码:6200 200217
gradle-6.1.1-all 积分下载 网盘下载 、提取码:6110 200125
gradle-6.1-all 积分下载 网盘下载 、提取码:6100 200119
gradle-6.0.1-all 积分下载 网盘下载 、提取码:6010
gradle-6.0-all 积分下载 网盘下载 、提取码:6000 191109
gradle-5.6.4-all 积分下载 网盘下载 、提取码:5640
gradle-5.6.3-all 积分下载 网盘下载 、提取码:5630
gradle-5.6.2-all 积分下载 网盘下载 、提取码:5620 190907
gradle-5.6.1-all 积分下载 网盘下载 、提取码:5610
gradle-5.6-all 积分下载 网盘下载 、提取码:5600 190626
gradle-5.5-all 积分下载 网盘下载 、提取码:5500
gradle-5.4.1-all 积分下载 网盘下载 、提取码:5410
gradle-5.4-all 积分下载 网盘下载 、提取码:5400
gradle-5.3-all 积分下载 网盘下载 、提取码:5300
gradle-5.2-all 积分下载 网盘下载 、提取码:5200 190129
gradle-5.1.1-all 积分下载 网盘下载 、提取码:5110
gradle-5.1-all 积分下载 网盘下载 、提取码:5100 190103
gradle-5.0-all 积分下载 网盘下载 、提取码:5000 181127
gradle-4.9-all 积分下载 网盘下载 、提取码:4900 180717
gradle-4.8.1-all 积分下载 网盘下载 、提取码:4810 180618
gradle-4.8-all 积分下载 网盘下载 、提取码:4800 180610
gradle-4.7-all 积分下载 网盘下载 、提取码:4700 180418
gradle-4.6-all 积分下载 网盘下载 、提取码:4600 180302
gradle-4.5.1-all 积分下载 网盘下载 、提取码:4510
gradle-4.5-all 积分下载 网盘下载 、提取码:4500 180109

————————————————
版权声明:本文为CSDN博主「踮脚敲代码」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ii950606/article/details/109105402

Android gradle 各种版本下载 - 简书

mikel阅读(1228)

来源: Android gradle 各种版本下载 – 简书

来气

经常缺少gradle包. 今天网络极佳,各种all版本都down下来. 自取自用, csdn 下载一个还需要积分, 要不要脸了还.

官方地址

官方下载gradel地址

不翻墙不能下载司机

福利

百度网盘下载速度慢 , 怎么办 ?

For macOS.百度网盘 破解SVIP、下载速度限制~

下载地址

gradle-2.0-all下载

gradle-2.1-all下载

gradle-2.2-all下载

gradle-2.3-all下载

gradle-2.4-all下载

gradle-2.5-all下载

gradle-2.6-all下载

gradle-2.7-all下载

gradle-2.8-all下载

gradle-2.9-all下载

gradle-2.10-all下载

gradle-2.11-all下载

gradle-2.12-all下载

gradle-2.13-all下载

gradle-3.1-all下载

gradle-3.2-all下载

gradle-3.3-all下载

gradle-3.4-all下载

gradle-3.5-all下载

gradle-4.1-all下载

gradle-4.2-all下载

gradle-4.3-all下载

gradle-4.4-all下载

gradle-4.5-all下载

gradle-4.6-all下载

2019-12-06 16:59:47   更新以下 gradle

gradle-4.7-all下载

gradle-4.8-all下载

gradle-4.9-all下载

gradle-5.0-all下载

gradle-5.1-all下载

gradle-5.2-all下载

gradle-5.3-all下载

gradle-5.4-all下载

gradle-5.5-all下载

gradle-5.6-all下载

gradle-6.0-all下载

2020年09月24日09:25:59  更新以下 gradle

gradle-6.1-all下载

链接: https://pan.baidu.com/s/10nXj0DxFS-J-WXFjirp8Hw 提取码: 2rvj

gradle-6.1.1-all下载

链接: https://pan.baidu.com/s/1Wuj6xhm4xMmoHTN5Lvp7qQ 提取码: x7r3

gradle-6.2-all下载

链接: https://pan.baidu.com/s/1u8E-dx30mBSFjO59PEDlsQ 提取码: 9421

gradle-6.2.1-all下载

链接: https://pan.baidu.com/s/1PZTgWIrl2oO9I8YSs-T4YA 提取码: n6m5

gradle-6.3-all下载

链接: https://pan.baidu.com/s/1soIvuwD19ooCKpbyjnR5ZQ 提取码: ktji

gradle-6.4-all下载

链接: https://pan.baidu.com/s/1SAnZiqF9hepcmbmB8gUchA 提取码: rwxc

gradle-6.4.1-all下载

链接: https://pan.baidu.com/s/1yPA7MT8gjLcYvP6AW-mAhA 提取码: 8ypw

gradle-6.5-all下载

链接: https://pan.baidu.com/s/1YdGjN6FY3y66Gsc5MDqeRQ 提取码: bkb9

gradle-6.5.1-all下载

链接: https://pan.baidu.com/s/1QcKaVC7ZaDAwbOAz41RRJw 提取码: d8ns

gradle-6.6-all下载

链接: https://pan.baidu.com/s/1iSRGhKrqaUi30GoOi4YuJg 提取码: nb5r

Tips : 

之前我家里网络不好 ,然后用手机4G网络下载极快, 蜜汁吉吉

作者:刘亚芳
链接:https://www.jianshu.com/p/d9329117aa2f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Flutter App Run卡在Running Gradle task 'assembleDebug'..._jinglinggg的专栏-CSDN博客

mikel阅读(642)

来源: Flutter App Run卡在Running Gradle task ‘assembleDebug’…_jinglinggg的专栏-CSDN博客

配置Flutter开发环境,根据网上的教程安装IDE,SDK、各种插件和依赖包

手机:VIVO X21UD A
开发工具:Android Studio 3.6.2
Flutter:1.17.0 channel beta
Java JDK:1.8.0_202
gradle:gradle-6.3

新建了一个Flutter工程后,Run卡在了Running Gradle task ‘assembleDebug’…
百度查询原因是因为Gradle的Maven仓库在国外, 因此需要使用阿里云的镜像地址。
修改内容如下:
maven { url ‘https://maven.aliyun.com/repository/google’ }
maven { url ‘https://maven.aliyun.com/repository/jcenter’ }
maven { url ‘http://maven.aliyun.com/nexus/content/groups/public’ }

1、修改项目中Android/build.gradle文件

2、修改Flutter的配置文件, 该文件在Flutter安装目录/packages/flutter_tools/gradle/flutter.gradle

按说这样之后就可以, 但是我的还是不行,还是卡在Running Gradle task ‘assembleDebug’…,执行大概7分钟左右还是同样的错误提示,在百度查了半天的资料,查到的都是上面的同样的修改方法,问题没有解决。

3、然后到stackoverflow平台上搜索Running Gradle task ‘assembleDebug’,查到一个回答是说执行flutter clean,之后再run,提示gradle最小版本需要5.6.4,我电脑上原来是gradle5.6.2,于是直接安装了最新版的gradle6.3
在Settings里配置最新版的gradle

 

四、Android Studio的版本和项目的classpath版本要一致

更新到最新版后,再回到Android Studio,Run,成功了
记录一下,时间长了容易忘!
朋友们如果遇到其他报错,可以留言,我们一起探讨解决方法。
————————————————
版权声明:本文为CSDN博主「JerrySmurfs」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jinglinggg/article/details/105383270

Android Studio 搭建第一个flutter应用 - 简书

mikel阅读(814)

来源: Android Studio 搭建第一个flutter应用 – 简书

.下载安卓AS,https://developer.android.google.cn/studio/

2.下载 SDK ,SDK下载过中可能会遇到 无法访问 dl.google.com,添加host映射203.208.50.33 dl.google.com   tips–>hosts文件路径C:\Windows\System32\drivers\etc

3.设置SDK路径

4. 下载platforms-tools 并设置环境变量

5.安装flutter插件

6下载虚拟机镜像

7.建立第一个flutter工程

8. 运行

以下介绍运行过程中出现的一些错误

1)Exception in thread “main” java.io.IOException: Unable to tunnel through proxy. Proxy returns “HTTP/1.1 400 Bad Request”

此错误是由于安装AS过程中设置了代理的原因

解决办法:找到 C:\Users\Administrator\.gradle 下的 gradle.properties 文件将里面的代理设置去掉

2)Exception in thread “main” java.util.zip.ZipException: error in opening zip file

找到项目工程目录下的gradle-wrapper.properties文件 修改 distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 为本地目录的gradle  例如:distributionUrl=file:///D:/gradle-5.4.1-bin.zip

3)Minimum supported Gradle version is 5.4.1. Current version is 5.2.1. If using the gradle wrapper, try editing the distributionUrl in

修改gradle为对应版本或以上版本即可

4) Could not download kotlin-reflect.jar (org.jetbrains.kotlin:kotlin-reflect:1.3.41)

SDK目录下,找到flutter插件下找到flutter.gradle(例如D:\AndroidSDK\flutter\packages\flutter_tools\gradle|flutter.gradle)修改为国内仓库

maven { url ‘https://maven.aliyun.com/repository/google’ }

maven { url ‘https://maven.aliyun.com/repository/jcenter’ }

maven { url ‘http://maven.aliyun.com/nexus/content/groups/public’ }

5)dart\runtime\vm\zone.cc: 90: error: Out of memory  构建打包时内存溢出

在app目录下找到build.gradle文件Android节点下增加以下内容

dexOptions{

javaMaxHeapSize =”2g”

}

6)flutter 第三方包依赖无法下载

Windows增加系统环境变量:

PUB_HOSTED_URL=https://pub.flutter-io.cn

FLUTTER_STORAGE_BASE_URL= https://storage.flutter-io.cn

linux/MAC:

export PUB_HOSTED_URL=https://pub.flutter-io.cn

export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

7)flutter protobuf文件生成

a. 安装protoc工具  https://github.com/protocolbuffers/protobuf/releases 下载对应系统的工具,并添加系统环境变量

b.下载dart protobuf插件,可以在 pubspec.yaml添加 protoc_plugin: ^19.2.0+1,pub get下载 ,并可以将~~/.pub-cache\hosted\pub.flutter-io.cn\protoc_plugin-19.2.0+1\bin添加到系统环境变量

c.进入~~/.pub-cache\hosted\pub.flutter-io.cn\protoc_plugin-19.2.0+1\目录,执行pub get 下载插件的依赖包

d.运行protoc –dart_out=. test.proto 即可生成

作者:chrisccnu
链接:https://www.jianshu.com/p/af39430e7fcb
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。