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

mikel阅读(28)

来源: 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

Asp.Net Core 3.1学习-Web Api 文件上传 Ajax请求以及跨域问题(3) - 魏杨杨 - 博客园

mikel阅读(28)

来源: Asp.Net Core 3.1学习-Web Api 文件上传 Ajax请求以及跨域问题(3) – 魏杨杨 – 博客园

1、创建Api项目

我用的是VS2019 Core3.1 。打开Vs2019 创建ASP.NET Core Web应用程序命名CoreWebApi 创建选择API 在Controller文件夹下面添加一个Api控制器 FileUp,修改Api的路由  [Route(“api/[controller]/[action]”)] 这样就可以访问到具体的某一个了  写一个测试 api

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace CoreWebApi.Controllers
{
    [Route("api/[controller]/[action]")]//修改路由
    //[Route("api/[controller]")]//默认路由
    [ApiController]
    public class FileUpController : ControllerBase
    {
        /// <summary>
        /// 测试接口
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        //[HttpGet,Route("Test")]//默认路由
        
        public string Test()
        {
            return "这是测试接口";
        }
    }
}
复制代码

 

写Api的时候一定要加上,请求方式  post、get 其他的暂时我没用到。默认的路由是被我注释的,修改一下 让他的访问时api / 控制器 / 方法名称。不改也可以,测试的接口就用注释的那个特性。然后运行项目,地址栏输入https://localhost:44376/api/FileUp/Test

 

2、上传文件接口

在FileUp里写一个上传文件的接口所有代码都在下,OutPut是一个输出的类,Dto里面是一些参数 应该都有看得懂。里面就是上传文件储存到服务器上 并没有数据库操作,需要的加上去就可以了。

注意:有些会加[Consumes(“application/json”)]//application/json//application/x-www-form-urlencoded 这样来验证媒体类型,这里我弄了好久才想起来我写了这个。因为文件上传怎么说呢,媒体类型肯定不会是Json之类的。我以前的Api就是在基础Api类里加了这个。给我弄了头皮发麻才看到

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

using CoreWebApi.Models;
using Newtonsoft.Json;
using Microsoft.Extensions.Hosting;
using System.IO;

namespace CoreWebApi.Controllers
{
    [Route("api/[controller]/[action]")]//修改路由
    //[Route("api/[controller]")]//默认路由
    [ApiController]
    public class FileUpController : ControllerBase
    {
        public  IHostingEnvironment env;
        public FileUpController(IHostingEnvironment _env)
        {
            env = _env;
        }
        /// <summary>
        /// 测试接口
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        //[HttpGet,Route("Test")]//默认路由
        
        public string Test()
        {
            return "这是测试接口";
        }
        /// <summary>
        /// 文件上传
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        public async Task<OutPut> FileUp()
        {
            var ret = new OutPut();
            try
            {
                //不能用FromBody
                var dto = JsonConvert.DeserializeObject<ImagesDto>(Request.Form["ImageModelInfo"]);//文件类实体参数
                var files = Request.Form.Files;//接收上传的文件,可能多个 看前台
                if (files.Count > 0)
                {
                    var path = env.ContentRootPath + @"/Uploads/Images/";//绝对路径
                    string dirPath = Path.Combine(path, dto.Type + "/");//绝对径路 储存文件路径的文件夹
                    if (!Directory.Exists(dirPath))//查看文件夹是否存在
                        Directory.CreateDirectory(dirPath);
                    var file = files.Where(x => true).FirstOrDefault();//只取多文件的一个
                    var fileNam = $"{Guid.NewGuid():N}_{file.FileName}";//新文件名
                    string snPath = $"{dirPath + fileNam}";//储存文件路径
                    using var stream = new FileStream(snPath, FileMode.Create);
                    await file.CopyToAsync(stream);
                    //次出还可以进行数据库操作 保存到数据库
                    ret = new OutPut { Code = 200, Msg = "上传成功", Success = true };  
                }
                else//没有图片
                {
                    ret = new OutPut { Code = 400, Msg = "请上传图片", Success = false };
                }
            }
            catch (Exception ex)
            {
                ret = new OutPut { Code = 500, Msg = $"异常:{ex.Message}", Success = false };
            }
            return ret;
        }
    }
}
复制代码

 

Dto与返回的类

复制代码
 /// <summary>
    /// 返回输出类
    /// </summary>
    public class OutPut
    {
        /// <summary>
        /// 状态码
        /// </summary>
        public int Code { get; set; }
        /// <summary>
        /// 消息
        /// </summary>
        public string Msg { get; set; }
        /// <summary>
        /// 是否成功
        /// </summary>
        public bool Success { get; set; }
        /// <summary>
        /// 返回数据
        /// </summary>
        public object Data { get; set; }
    }
/// <summary>
    /// 接收参数Dto
    /// </summary>
    public class ImagesDto
    {
        /// <summary>
        /// ID
        /// </summary>
        public int ID { get; set; }
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 地址
        /// </summary>
        public string Url { get; set; }
        /// <summary>
        /// 备注
        /// </summary>
        public string Remark { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public int RelationId { get; set; }
        /// <summary>
        /// 类型
        /// </summary>
        public int Type { get; set; }
    }
复制代码

3、前台Ajax调用

我在一个MVC程序里面用了个Ajax调用  下面是前端代码,contentType这个资源的类型我转载着坑里好久了,哈哈。

复制代码
@{
    ViewBag.Title = "测试";
}

<h2>文件上传测试</h2>
<form enctype="multipart/form-data" id="formData">
    <div>
        <br /><br /><br />
        文件:<input type="file" id="filesp" name="filesp" /><br /><br />
        名称:<input type="text" id="fileName" name="fileName" /><br /><br />
        备注:<input type="text" id="txtRemake" name="txtRemake" /><br /><br />
        RelationId:<input type="number" id="txtRelationId" name="txtRelationId" /><br /><br />
        类型:<select id="selType" name="selType">
            <option value="1">动物</option>
            <option value="2">植物</option>
            <option value="3">妹子</option>
            <option value="3">风景</option>
            <option value="4">滑稽</option>
            <option value="100">其他</option>
        </select>
        <br /><br />
        <input type="button" id="btnSave" name="btnSave" value="提交" />
    </div>
</form>

<script src="~/Scripts/jquery-3.3.1.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        $("#btnSave").click(function () {
            var data = new FormData(document.getElementById("formData"));
            //参数
            var parame = JSON.stringify({ Name: $("#fileName").val(), Remark: $("#txtRemake").val(), Type: $("#selType").val(), RelationId: $("#txtRelationId").val() });
            data.append("ImageModelInfo", parame);
            $.ajax({
                type: "post",
                url: "https://localhost:44376/api/FileUp/FileUp",
                dataType: "json",
                data: data,
                async: true,
                contentType: false,//实体头部用于指示资源的MIME类型 media type 。这里要为false
                processData: false,//processData 默认为true,当设置为true的时候,jquery ajax 提交的时候不会序列化 data,而是直接使用data
                success: function (data) {
                    console.log(data);
                },
                error: function (data) {
                    console.log("错误" + data);
                }
            });
        });
    });
</script>
复制代码

 

运行起来看看 测试一下  ,Api那边也打好断点。打来浏览器调式就是很熟悉的bug就来了(一天看不到出错仿佛心里不踏实)

 

 

很明显的CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。所以这里我们就要允许前端来访问,从而就要在Api增加跨域;

4、跨域设置

在Startup.cs下面的ConfigureServices 中标添加services.AddCors(option => option.AddPolicy(“AllowCors”, bu => bu.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            //跨域
            services.AddCors(option => option.AddPolicy("AllowCors", bu => bu.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
        }

 

AllowAnyOrigin  :允许CORS请求从任何源来访问,这是不安全的
AllowAnyHeader:允许所有的请求头

AllowCredentials :服务端也需要允许证书。
AllowAnyMethod允许跨域策略允许所有的方法:GET/POST/PUT/DELETE 等方法 如果进行限制需要 AllowAnyMethod(“GET”,”POST”) 这样来进行访问方法的限制


在Configure中添加中间件app.UseCors(“AllowCors”);,集体要限制哪些看个人了

复制代码
 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();


            app.UseAuthorization();

            //跨域
            app.UseCors("AllowCors");
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

        }
复制代码

 

现在就配置好了在需要跨域的地方加上特性[EnableCors("AllowCors")]就行了 ,引用命名空间using Microsoft.AspNetCore.Cors;
复制代码
    [EnableCors("AllowCors")]
    [Route("api/[controller]/[action]")]//修改路由
    //[Route("api/[controller]")]//默认路由
    [ApiController]
    public class FileUpController : ControllerBase
    {
        public  IHostingEnvironment env;
        public FileUpController(IHostingEnvironment _env)
        {
            env = _env;
        }
        /// <summary>
        /// 测试接口
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        //[HttpGet,Route("Test")]//默认路由
        
        public string Test()
        {
            return "这是测试接口";
        }
复制代码

 

现在再来调用就解决了

 

前台结果看看。文件夹也创建了。

5、总结

如果所有Api都要跨域的话就建立一个基础的Api 让所有的都继承这个Api,就需要打赏跨域的标签就可以了;

文件上传还可以用From直接提交这边用List<IFormFile>接收,也可以像FromBady一样的一个来接收搞,单词 忘了0.0,参数不能FermBody接收,这样文件就没了。

纸上得来终觉浅,觉知这事不写还是不行哈。

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

Asp.Net Core 3.1学习-使用ASP.NET Core中的RazorPages(2) - 魏杨杨 - 博客园

mikel阅读(27)

来源: Asp.Net Core 3.1学习-使用ASP.NET Core中的RazorPages(2) – 魏杨杨 – 博客园

1、创建一个ASP.NET Core Web应用程序

1.1、打开VS2019 新建项目

1.2、选好项目位置后进入选择界面,选择Web应用程序

 

1.3、进去的页面结构如下

 

 

Pages 文件夹:包含 Razor 页面和支持文件。 每个 Razor 页面都是一对文件:

  • 一个 .cshtml 文件,其中包含使用 Razor 语法的 C# 代码的 HTML 标记 。
  • 一个 .cshtml.cs 文件,其中包含处理页面事件的 C# 代码 。

 

wwwroot 文件夹包含静态文件,如 HTML 文件、JavaScript 文件和 CSS 文件。

appSettings.json包含配置数据,如连接字符串。

Program.cs包含程序的入口点。 

Startup.cs包含配置应用行为的代码。

运行起来如果提示要安装证书的直接点是就可以了,出现WelCome就表示可以了

2、添加模型

2.1、在这里搭建“商品”模型的基架。 确切地说,基架工具将生成页面,用于对“商品”模型执行创建、读取、更新和删除 (CRUD) 操作。

右击项目名称添加文件夹Models,在Models新文件夹里新建一个模型类Shop

复制代码
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace CoreRazorPages.Models
{
    /// <summary>
    /// 商品类
    /// </summary>
    public class Shop
    {
        
        [Display(Name="ID")]
        public int ID { get; set; }
        [Display(Name = "Guid")]
        public string Guid { get; set; }
        [Display(Name = "名称")]
        public string Name { get; set; }
        [Display(Name = "价格")]
        public decimal Price { get; set; }
        [Display(Name = "添加日期")]
        [DataType(DataType.Date)]
        public DateTime AddTime { get; set; }
    }
}
复制代码

3、添加基架

3.1、右击Pages文件夹添加一个文件夹Shop,然后右击Shop>添加>新搭建基架的项目>使用实体框架生成Razor页面(CRUD)模型类选择Shop,数据库上下文点击右边的+号

3.2、生成项目,如果报错Shop就加上命名空间,这里是因为文件夹Shop名称跟类名Shop一样

项目文件夹多了个Data那是数据库上下文,还有配置文件里面也加了数据库访问的字符串在appsetting.json文件夹里

   3.3、更改数据库链接

打开appsettiong.json文件修改里面的数据库链接字符串,然后点工具>nuget包管理器>程序包管理控制台依次输入Add-Migration InitialCreate、Update-Database,警告不要管他(程序员不怕警告就怕错误 哈哈)

复制代码
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
    "ConnectionStrings": {
        //原来生成的Server=(localdb)\\mssqllocaldb;Database=CoreRazorPagesContext-10c906a7-1959-4967-9659-0fcbfe8b7d16;Trusted_Connection=True;MultipleActiveResultSets=true
        "CoreRazorPagesContext": "Data Source=服务器地址;Initial Catalog=数据库名User ID=用户名;Password=密码"
    }
}
复制代码

 

 

看生成的数据库

 

3.4、修改文件夹Shared下的布局_Layout.cchtml添加Shop导航

看起来有点像winform的服务器控件一样哈哈,asp-area是区域名,现在没有 asp-page是指Razor的位置这里是Shop/Index

复制代码
 <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">CoreRazorPages</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="Shop/Index">商品</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
复制代码

  3.5、添加一些数据在Shop中,在Data文件件下面添加一个类 AddDefaultData,然后修改程序入口main 方法。

从依赖关系注入容器获取数据库上下文实例>调用 InitData方法,并将上下文传递给它、方法完成时释放上下文

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

//添加命名空间
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using CoreRazorPages.Models;
namespace CoreRazorPages.Data
{
    public class AddDefaultData
    {
        /// <summary>
        /// 添加数据
        /// </summary>
        /// <param name="serviceProvider"></param>
        public static void InitData(IServiceProvider serviceProvider)
        {
            using (var db = new CoreRazorPagesContext(serviceProvider.GetRequiredService<DbContextOptions<CoreRazorPagesContext>>()))
            {
                //如果有数据就不添加了
                if (db.Shop.Any())
                {
                    return;
                }
                //添加10条初始数据
                for(var i=0;i<10;i++)
                {
                    var model = new Shop
                    {
                        Name = "商品"+i.ToString(),
                        Guid = Guid.NewGuid().ToString("N"),
                        Price = i + 0.66M,
                        AddTime = DateTime.Now
                    };
                    db.Shop.AddRange(model);
                }
                db.SaveChanges();
            }
        }
    }
}
复制代码
复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
//添加命名空间
using Microsoft.Extensions.DependencyInjection;
using CoreRazorPages.Data;

namespace CoreRazorPages
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();
            using (var scope=host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    AddDefaultData.InitData(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "添加初始数据失败");
                }

            }
                host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
复制代码

数据库依赖注入在staup.cs里面已经自己生成好了

复制代码
public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            //数据库连接
            services.AddDbContext<CoreRazorPagesContext>(options =>
                    options.UseSqlServer(Configuration.GetConnectionString("CoreRazorPagesContext")));
        }
复制代码

然后启动 点到商品导航上面数据就有了,也可以一些基本的操作(CRUD)

4、添加搜索

添加名称,guid的搜索,在Pages文件夹下面的Shop文件夹下面的Index.cchtml.sc打开 修改

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using CoreRazorPages.Data;
using CoreRazorPages.Models;

namespace CoreRazorPages.Pages.Shop
{
    public class IndexModel : PageModel
    {
        private readonly CoreRazorPages.Data.CoreRazorPagesContext _context;

        public IndexModel(CoreRazorPages.Data.CoreRazorPagesContext context)
        {
            _context = context;
        }
        /// <summary>
        /// 模型List
        /// </summary>
        public IList<CoreRazorPages.Models.Shop> Shop { get;set; }
        [BindProperty(SupportsGet = true)]
        public string SearchName { get; set; }
        [BindProperty(SupportsGet = true)]
        public string SearchGuid { get; set; }
        public async Task OnGetAsync()
        {
            var model = _context.Shop.Where(x=>x.ID>0);
            if (!string.IsNullOrEmpty(SearchName))
                model = model.Where(x => x.Name.Contains(SearchName));
            if (!string.IsNullOrEmpty(SearchGuid))
                model = model.Where(x => x.Guid.Contains(SearchGuid));
            Shop = await model.ToListAsync();
        }
    }
}
复制代码

在Index.cshtml里面添加

复制代码
<p>
    <a asp-page="Create">Create New</a>
</p>
<form>
    <p>
        名字: <input type="text" asp-for="SearchName" />
        Guid: <input type="text" asp-for="SearchGuid" />
        <input type="submit" value="搜索" />
    </p>
</form>
复制代码

SearchName 、SearchGuid:包含用户在搜索文本框中输入的文本。 SearchString 也有 [BindProperty] 属性。 [BindProperty] 会绑定名称与属性相同的表单值和查询字符串。 在 GET 请求中进行绑定需要 (SupportsGet = true)

5、添加字段,验证

5.1在模型文件Shop.cs添加一个字段Remark

复制代码
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace CoreRazorPages.Models
{
    /// <summary>
    /// 商品类
    /// </summary>
    public class Shop
    {
        
        [Display(Name="ID")]
        public int ID { get; set; }
        [Display(Name = "Guid")]
        public string Guid { get; set; }
        [Display(Name = "名称")]
        public string Name { get; set; }
        [Display(Name = "价格")]
        public decimal Price { get; set; }
        [Display(Name = "添加日期")]
        [DataType(DataType.Date)]
        public DateTime AddTime { get; set; }
        /// <summary>
        /// 新加字段、验证
        /// </summary>
        [Required]//必填
        [StringLength(10,ErrorMessage ="{0}最大长度10  必填")]//最大长度
        [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]//验证格式
        public string Ramake { get; set; }
    }
}
复制代码

 

 5.2nuget包管理器>程序包管理控制台依次输入Add-Migration Remake、Update-Database,

 

 

 

 5.3、编辑Shop文件夹下面的Index.cshtml、Create.cshtml、Delete.cshtml、Details.cshtml、Edit.cshtml文件添加Remake字段

复制代码
 <td>
                @Html.DisplayFor(modelItem => item.Ramake)
            </td>



 <div class="form-group">
                <label asp-for="Shop.Ramake" class="control-label"></label>
                <input asp-for="Shop.Ramake" class="form-control" />
                <span asp-validation-for="Shop.Ramake" class="text-danger"></span>
            </div>




 <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Shop.Ramake)
        </dd>




<div class="form-group">
                <label asp-for="Shop.Ramake" class="control-label"></label>
                <input asp-for="Shop.Ramake" class="form-control" />
                <span asp-validation-for="Shop.Ramake" class="text-danger"></span>
            </div>
复制代码

 

5.4、运行起来测试看

6、代码总结:

  • Razor 页面派生自 PageModel。 按照约定,PageModel 派生的类称为 <PageName>Model。 此构造函数使用依赖关系注入将 RazorPagesMovieContext 添加到页。 所有已搭建基架的页面都遵循此模式,对页面发出请求时,OnGetAsync 方法向 Razor 页面返回影片列表。 调用 OnGetAsync 或 OnGet 以初始化页面的状态。 在这种情况下,OnGetAsync 将获得影片列表并显示出来。当 OnGet 返回 void 或 OnGetAsync 返回 Task 时,不使用任何返回语句。 当返回类型是 IActionResult 或 Task<IActionResult> 时,必须提供返回语句
  • @page 指令:@page Razor 指令将文件转换为一个 MVC 操作,这意味着它可以处理请求。 @page 必须是页面上的第一个 Razor 指令。 @page 是转换到 Razor 特定标记的一个示例
  • @model 指令指定传递给 Razor 页面的模型类型。 在前面的示例中,@model 行使 PageModel 派生的类可用于 Razor 页面。 在页面上的 @Html.DisplayNameFor 和 @Html.DisplayFor HTML 帮助程序中使用该模型。

现在开始对Razor页面有一定了解,大多数代码都是系统自己生成的,数据库上下文类、依赖注入、我也是对Core还不怎么熟悉不知道怎么学习跟着做就行了,对后面肯定有很大的帮助。

原文地址:https://www.cnblogs.com/w5942066/p/12597712.html

Asp.Net Core 3.1学习-初始.Net Core与VS Code 第一个web程序(1) - 魏杨杨 - 博客园

mikel阅读(20)

来源: Asp.Net Core 3.1学习-初始.Net Core与VS Code 第一个web程序(1) – 魏杨杨 – 博客园

1、.Net Core介绍

.NET Core是.NET Framework的新一代版本, 是微软开发的第一个具有跨平台(Windows、Macosx、Linux) 能力的应用程序开发框架,未来也将会支持FreeBSD与Alpine平台,是微软在一开始发展时就开源的软件平台,它也经常被拿来和现有的开源NET平台Mono比较。
由于.NET Core的开发目标是跨平台的.NET平台,因此.NET Core会包含.NET Framework的类库。与.NET Framework不同的是,.NET Core 采用包化(Packages) 的管理方式,应用程序只需获取需要的组件即可。与.NET Framework大包式安装的做法截然不同,并且各包亦有独立的版本线,不再硬性要求应用程序跟随主线版本。

2、.Net Core跨平台

.Net Core 拥有跨平台能力,并支持多种系统,让我们开大的程序可以在多个系统中运行。.Net支持的操作系统:Windows 客户端、Windows 服务端、Debian、Red Hat Enterpise Linux、 Fedora、 Ubuntu、 Linux Mint、 OpenSUSE、 Oracle Linux、 CentOS、 Mac OSX

3、 .Net Core SDK 下载安装

下载链接  点击就下载了3.0.1.00 也可以去管网下载 https://dotnet.microsoft.com/download

下载下来微软的就是傻瓜式安装,位置就默认了装在C盘了,下一步下一步就好了 哈哈。安装好了 win+R 输入cmd 打开命令提示符输入 dotnet –info 看到下面的信息就证明安装好了

 4、 dotnet命令

一般的命令在命令提示符里面直接输入dotnet -all 查看

 

比如新建一个项目 它提示了是 new 怎么搞呢? 不知道就是在来一次 dotnet new -all 可以看到很多创建的命令 都有提示  表头的意思: 模板、短名称、语言、标记 下面创建一个控制台应用程序

进入文件夹的命令自己搜吧 创建控制台应用程序是dotnet new console -n 名称     后面要接语言的话后面加上 -lang f#这样

 

运行 dotnet run 的时候先要进去到创建项目的文件夹里面  其他命令有兴趣的自己试试   感觉跟git一样 哈哈。

 5、在VS Code中创建.Net Core项目

VS codel下载地址:https://code.visualstudio.com/download      根据版本选择自己的下载 我的是Windows 当然安装过VSCode 的就忽略这一步。安装好了打开安装插件 快捷键(Ctrl+Shift+X)或者点击左上一排的倒数第二个按钮 搜索 C#(包括语法高亮显示、智能感知、定义、查找所有引用等。调试支持。网络核心(CoreCLR)。) 安装一下 ,一般语言都是中文的 可能有的是英文的就还要安装一个简体中文包Chinese (Simplified) 。安装好了重启VS Code。

接下来用dotnet new 创建一个.Net Core web程序  ,输入了命令要稍等一下 ,有点慢 (可能我的机子慢 0.0)。 创建好了 命令进入创建的文件夹WebFirst 输入code . 用VScode快速打开,并加载当前目录。你也可以打开VS Code左上角打开文件夹来完成这一部操作。

 

 

首次打开的时候会提示我们添加VS Code配置文件,选择Yes就好了,等出现.vscode文件夹后就可以开发、调试程序了。按F5或者菜单:调试->启动调试启动项目  出现错误咯

遇到问题不要慌 看看 。这是因为net core2.1默认使用的https,如果使用Kestrel web服务器的话没有安装证书就会报这个错。其实仔细看他的错误提示,其中有一句叫你执行一个命令安装证书的语句: dotnet dev-certs https –trust 

安装就是了  这是启动就浏览器就出现了 Hello Word!好熟悉的感觉  你要相信这是真的 这就是.Net Core 的一个程序。神奇吧。

5.1关于 VS Code C#插件配置介绍

使用VS Code打开.Net Core 项目文件夹后悔根据提示生成VS Code所需要的的配置文件,就是刚才说的C#配置文件。在.vscode文件夹下面有两个文件 :launch.json和tasks.json。tasks.json是用于配置执行那些命令行命令来构建项目,launch.json配置需要使用的调试器的类型。有了这两个文件 VS Code就知道如何构建调式了。

复制代码
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "command": "dotnet",
            "type": "process",
            "args": [
                "build",
                "${workspaceFolder}/Co.csproj",
                "/property:GenerateFullPaths=true",
                "/consoleloggerparameters:NoSummary"
            ],
            "problemMatcher": "$msCompile"
        },
        {
            "label": "publish",
            "command": "dotnet",
            "type": "process",
            "args": [
                "publish",
                "${workspaceFolder}/Co.csproj",
                "/property:GenerateFullPaths=true",
                "/consoleloggerparameters:NoSummary"
            ],
            "problemMatcher": "$msCompile"
        },
        {
            "label": "watch",
            "command": "dotnet",
            "type": "process",
            "args": [
                "watch",
                "run",
                "${workspaceFolder}/Co.csproj",
                "/property:GenerateFullPaths=true",
                "/consoleloggerparameters:NoSummary"
            ],
            "problemMatcher": "$msCompile"
        }
    ]
}
复制代码

节点介绍:

command节点在这里表示使用dotnet命令;

args 在这里是参数对应路径dotnet.csproj 及build

结合起来就相当于指向了dotnet build 路径\dotnet.csproj

复制代码
{
   // Use IntelliSense to find out which attributes exist for C# debugging
   // Use hover for the description of the existing attributes
   // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
   "version": "0.2.0",
   "configurations": [
        {
            "name": ".NET Core Launch (web)",
            "type": "coreclr",
            "request": "launch",
            "preLaunchTask": "build",
            // If you have changed target frameworks, make sure to update the program path.
            "program": "${workspaceFolder}/bin/Debug/netcoreapp3.1/Co.dll",
            "args": [],
            "cwd": "${workspaceFolder}",
            "stopAtEntry": false,
            // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
            "serverReadyAction": {
                "action": "openExternally",
                "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)"                
            },
            "env": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            },
            "sourceFileMap": {
                "/Views": "${workspaceFolder}/Views"
            }
        },
        {
            "name": ".NET Core Attach",
            "type": "coreclr",
            "request": "attach",
            "processId": "${command:pickProcess}"
        }
    ]
}
复制代码

节点介绍:(这里主要是configurations节点下)

program: 这里表示程序build以后生成的dll,默认是bin\Debug\框架\项目名称.dll

args:用样式参数,这个参数可以传递到程序里

cwd:代码的目录

访问的时候是https不信的试试看,要改成http。打开Properties/launchSettings.json文件

复制代码
{
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:22028",
      "sslPort": 44383
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "WebFirst": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
复制代码

 

iisSettings、profiles.helloweb配置节点都有启动绑定配置,因为VS Code启动项目默认是不通过IIS来host的,iisSettings选项我们忽略就可以了。将applicationUrl修改为http://localhost:5001 重启就看到了是http协议了

5.2项目结构说明:

 

 

 

 

 

.vscode目录:VS Code项目配置目录,相当于.vs、.idea文件夹;

bin:编译输出目录;

obj:编译配置与中间目录,用于存放编译配置与编译中间结果;

Properties:用于存放项目配置;

wwwroot:存放静态文件(JS,css,img等);

WebFirst.csproj:项目描述文件;

Program.cs文件:应用程序入口类文件

Startup.cs文件:ASP.NET Core Web应用启动类文件,用于项目启动前进行相关配置

 6、总结

现在大致已经会在VS Code建一个.Net Core应用程序了,我门的目的不是这么简单,接下来就是在VS 2017 里面开始学习 ASP.NET Core 了(ASP.NET Core ==.Net Core ??),重点介绍一下Program.cs 跟Startup.cs这两个文件,我觉得现在不知道他们是什么意思没事 接下来慢慢学吧只有知道了就好写程序了吧。哪里不对的多多指教^_^

本文链接 https://www.cnblogs.com/w5942066/p/12195984.html

Asp.Net Core 3.1学习- 应用程序的启动过程(5) - 魏杨杨 - 博客园

mikel阅读(18)

来源: Asp.Net Core 3.1学习- 应用程序的启动过程(5) – 魏杨杨 – 博客园

本文主要讲的是ASP.NET Core的启动过程,帮助大家掌握应用程序的关键配置点。

1、创建项目

1.1、用Visual Studio 2019 创建WebApi项目。

 

 

 

 

 

 

这里面可以看到有两个关键的类。 一个Program,一个stsrtup

Program里面有一个Main函数,Main函数里面会调用我们的CreateHosbuilder这个方法,CreateHosbuilder里面返回了一个IHostBuilder,那么IHostBuilder就是我们应用程序启动的核心接口

1.2、我们转到定义看一下IHostBuilder这个接口的定义

 

这个接口主要有6个方法这里面我们需要关注的是:ConfigureAppConfiguration、ConfigureHostConfiguration、和ConfigureServices

为了演示整个应用程序的启动过程我们回到Program,修改CreateHostBuilder 在里面添加一些代码,把方法名打印出来

复制代码
 public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                //委托入参是:IConfigurationBuilder
                .ConfigureAppConfiguration(builder =>
                {
                    Console.WriteLine("ConfigureAppConfiguration");
                })
                //入参 IServiceCollection
                .ConfigureServices(services =>
                {
                    Console.WriteLine("ConfigureServices");
                })
                //入参IConfigurationBuilder
                .ConfigureHostConfiguration(builder =>
                {
                    Console.WriteLine("ConfigureHostConfiguration");
                })
                //入参 IWebHostBuilder
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    Console.WriteLine("ConfigureWebHostDefaults");
                    webBuilder.UseStartup<Startup>();
                });
    }
复制代码

 

Starup里面的主要有三个方法 构造行数、ConfigureServices、Configure 同样的打印出来

复制代码
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Console.WriteLine("Startup构造函数");
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

    
        public void ConfigureServices(IServiceCollection services)
        {
            Console.WriteLine("Startup.ConfigureServices");
            
            services.AddControllers();
        }

      
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            Console.WriteLine("Startup.Configure");
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
复制代码

 

启动应用程序看看,顺序我标注在下面了,Starup里面的是按顺序启动的

 

如果应用程序启动起来没有输出控制台的修改一下启动  选择项目名称 不要选择IIS 我在次研究了半天哈哈

 

修改一下里面顺序看看,可以发现执行的顺序与之前略微不同,就是ConfigureService这一行跑到了Startup.ConfigureServices之后。

 

 

2、结论

这些委托注册进去之后都是按一定顺序来执行的

 

整个启动过程分为5个阶段:

2.1、ConfigureWebHostDefaults

这个阶段注册了我们应用程序必要的几个组件,比如说配置的组件、容器的组件

2.2、ConfigureHostConfiguration

它是用来配置我们应用程序启动时必要的配置。比如说我们应用程序启动时所需要的监听的端口、我们需要监听的Url地址这些。在这个过程我们可以嵌入一些我们自己的配置内容注入到我们的配置的框架中去

 

 2.3、ConfigureAppConfiguration

是让我们来嵌入我们自己的配置文件供应用程序来读取,这些配置将来就会在后续的应用程序执行过程中间每个组件读取

2.4、ConfigureServices、ConfigureLogging、Startup、Startup.ConfigureServices

这些的话都是用来往容器里面注入我们的应用的组件

2.5、Startup.Configure

是我们用来注入我们的中间件,处理HttpContext整个请求过程的

3、Startup类非必要

Startup这个类其实不是必要的,在Progarm里面也可以配置,

复制代码
 public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                //入参 IWebHostBuilder
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    Console.WriteLine("ConfigureWebHostDefaults");
                    // webBuilder.UseStartup<Startup>();
                    webBuilder.ConfigureServices(services =>//对等Startup里面的Startup.ConfigureServices
                    {
                        services.AddControllers();
                    })
                    .Configure(app =>// 对等Startup里面的Startup.Configure
                    {
                        //if (env.IsDevelopment())
                        //{
                        //    app.UseDeveloperExceptionPage();
                        //}

                        app.UseHttpsRedirection();

                        app.UseRouting();

                        app.UseAuthorization();

                        app.UseEndpoints(endpoints =>
                        {
                            endpoints.MapControllers();
                        });
                    });
                })
                //入参 IServiceCollection
                .ConfigureServices(services =>
                {
                    Console.WriteLine("ConfigureServices");
                })
                //委托入参是:IConfigurationBuilder
                .ConfigureAppConfiguration(builder =>
                {
                    Console.WriteLine("ConfigureAppConfiguration");
                })
               
                //入参IConfigurationBuilder
                .ConfigureHostConfiguration(builder =>
                {
                    Console.WriteLine("ConfigureHostConfiguration");
                }) ;
复制代码

 

这样应用程序一样的可以启动起来 这只是为了规范建议还是留着哈哈

记得添加命名空间

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

参考视频链接:https://time.geekbang.org/course/intro/272?code=PuP-H4FsFaRyMFC-qRHJndCmpGGNZ64zelNHZOYI1R8%3D

原文地址 https://www.cnblogs.com/w5942066/p/12803556.html

使用 JMeter 进行压力测试 - 晓晨Master - 博客园

mikel阅读(25)

来源: 使用 JMeter 进行压力测试 – 晓晨Master – 博客园

一.前言

压力测试是每一个Web应用程序上线之前都需要做的一个测试,他可以帮助我们发现系统中的瓶颈问题,减少发布到生产环境后出问题的几率;预估系统的承载能力,使我们能根据其做出一些应对措施。所以压力测试是一个非常重要的步骤,下面我带大家来使用一款压力测试工具JMeter。

二.关于JMeter

Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外,JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。为了最大限度的灵活性,JMeter允许使用正则表达式创建断言。
Apache jmeter 可以用于对静态的和动态的资源(文件,Servlet,Perl脚本,java 对象,数据库和查询,FTP服务器等等)的性能进行测试。它可以用于对服务器、网络或对象模拟繁重的负载来测试它们的强度或分析不同压力类型下的整体性能。你可以使用它做性能的图形分析或在大并发负载测试你的服务器/脚本/对象。

官网:http://jmeter.apache.org/download_jmeter.cgi

这里我选用了 4.0 版本的二进制包:http://mirror.bit.edu.cn/apache//jmeter/binaries/apache-jmeter-4.0.zip

更多内容介绍 https://baike.baidu.com/item/Jmeter/3104456

三.准备工作

因为JMeter是使用JAVA写的,所以使用JMeter之前,先安装JAVA环境,本文就不讲不如安装JAVA环境了。.

JAVA环境变量配置:https://jingyan.baidu.com/article/fd8044fa2c22f15031137a2a.html

解压下载的二进制包,进入bin目录,使用jmeter.bat启动程序。

启动之后会有两个窗口,一个cmd窗口,一个JMeter的 GUI。前面不要忽略CMD窗口的提示信息:

JMeter:

CMD窗口的提示信息

================================================================================
Don't use GUI mode for load testing !, only for Test creation and Test debugging.
For load testing, use NON GUI Mode:
   jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]
& increase Java Heap to meet your test requirements:
   Modify current env variable HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m" in the jmeter batch file
Check : https://jmeter.apache.org/usermanual/best-practices.html
================================================================================

上面的意思就是:不要使用GUI运行压力测试,GUI仅用于压力测试的创建和调试;执行压力测试请不要使用GUI。使用下面的命令来执行测试:

 jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]

并且修改JMeter批处理文件的环境变量:HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m"

更改语言为中文

官方默认为我们提供了简体中文。通过 【Options】->【Choose Language】变更为简体中文

四.创建测试

1.创建线程组

在“测试计划”上右键 【添加】–>【Threads(Users)】–>【线程组】。

设置线程数和循环次数。我这里设置线程数为500,循环一次。

2.配置元件

在我们刚刚创建的线程组上右键 【添加】–>【配置元件】–>【HTTP请求默认值】。

配置我们需要进行测试的程序协议、地址和端口

当所有的接口测试的访问域名和端口都一样时,可以使用该元件,一旦服务器地址变更,只需要修改请求默认值即可。

3.构造HTTP请求

在“线程组”右键 【添加-】->【samlper】–>【HTTP 请求】设置我们需要测试的API的请求路径和数据。我这里是用的json

4.添加HTTP请求头

在我们刚刚创建的线程组上右键 【添加】–>【配置元件】–>【HTTP信息头管理器】。

因为我要传输的数据为json,所以设置一个 Content-Type:application/json

5.添加断言

在我们刚刚创建的线程组上右键 【添加】–>【断言】–>【响应断言】。

根据响应的数据来判断请求是否正常。我在这里只判断的响应代码是否为200。还可以配置错误信息

6.添加察看结果树

在我们刚刚创建的线程组上右键 【添加】–>【监听器】–>【察看结果树】。

直接添加,然后点击运行按钮就可以看到结果了。

7.添加Summary Report

在我们刚刚创建的线程组上右键 【添加】–>【监听器】–>【Summary Report】。

直接添加,然后点击运行按钮就可以看到结果了。

为了不引起不必要的争论,隐藏了TPS。此数据不具备任何价值,仅仅为文章演示。

8.测试计划创建完成

记得点保存。

五.执行测试计划

前面我们说过,执行测试计划不能用GUI,需要用命令行来执行。

我这里执行的命令为:

jmeter -n -t testplan/RedisLock.jmx -l testplan/result/result.txt -e -o testplan/webreport

说明:

testplan/RedisLock.jmx 为测试计划文件路径
testplan/result/result.txt 为测试结果文件路径
testplan/webreport 为web报告保存路径。

Web报告如下:

六.写在最后

线程数量和循环次数将会影响最终的测试报告,请大家多多测试。

asp.net core 3.x 授权默认流程,

mikel阅读(25)

ASP.NET core 3.x 授权默认流程,

 

一、前言

接上一篇《ASP.NET core 3.x 授权中的概念》,本篇看看ASP.NET core默认授权的流程。从两个方面来看整个授权系统是怎么运行的:启动阶段的配置请求阶段中间件的处理流程

由于asp.net core 3.x目前使用终结点路由,因此授权框架可以用于所有asp.net web项目类型,比如:webapi mvc razorpages…。但本篇只以MVC为例

二、核心概念关系图

 

三、启动阶段的配置

主要体现为3点

 

3.1、注册相关服务和选项配置

在mvc项目Startup.ConfigreServices中services.AddControllersWithViews(); (MvcServiceCollectionExtensions)用来向依赖注入框架注册各种mvc相关服务。其中会调用services.AddAuthorization(选项)扩展方法(PolicyServiceCollectionExtensions)注册授权相关服务,此扩展方法内部还会调用两个扩展方法,这里不再深入。

这里主要需要搞懂2个问题:

3.1.1、授权选项AuthorizationOptions

AddAuthorization扩展方法的参数是Action<AuthorizationOptions>类型的,这是asp.net core中典型的选项模型,将来某个地方需要时,直接注入此选项对象,那时依赖注入容器会使用此委托对这个选项对象赋值。所以我们在启动时可以通过此对象来对授权框架进行配置。

最最重要的是我们可以在这里配置全局授权策略列表,参考上图的右侧中间部分,源码不多,注意注释。

 1 //代表授权系统的全局选项对象,里面最最核心的就是存储着全局授权策略 2 public class AuthorizationOptions 3 { 4 //存储全局授权策略(AuthorizationPolicy),key是策略唯一名,方便将来获取 5 private IDictionary<string, AuthorizationPolicy> PolicyMap { get; } = new Dictionary<string, AuthorizationPolicy>(StringComparer.OrdinalIgnoreCase); 6 //授权验证时,将遍历所有授权处理器(Authorization)逐个进行验证,若某个发生错误是否立即终止后续的授权处理器的执行 7 public bool InvokeHandlersAfterFailure { get; set; } = true; 8 //默认授权策略,拒绝匿名访问 9 public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); 10 //若将来授权检查时没有找到合适的授权策略,默认授权策略也是空的情况下会回退使用此策略 11 public AuthorizationPolicy FallbackPolicy { get; set; } 12 //添加全局策略 13 public void AddPolicy(string name, AuthorizationPolicy policy) 14 { 15 if (name == null) 16 { 17 throw new ArgumentNullException(nameof(name)); 18 } 19 20 if (policy == null) 21 { 22 throw new ArgumentNullException(nameof(policy)); 23 } 24 25 PolicyMap[name] = policy; 26 } 27 //添加全局策略,同时可以对此策略进行配置 28 public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy) 29 { 30 if (name == null) 31 { 32 throw new ArgumentNullException(nameof(name)); 33 } 34 35 if (configurePolicy == null) 36 { 37 throw new ArgumentNullException(nameof(configurePolicy)); 38 } 39 40 var policyBuilder = new AuthorizationPolicyBuilder(); 41 configurePolicy(policyBuilder); 42 PolicyMap[name] = policyBuilder.Build(); 43 } 44 //获取指定名称的策略 45 public AuthorizationPolicy GetPolicy(string name) 46 { 47 if (name == null) 48 { 49 throw new ArgumentNullException(nameof(name)); 50 } 51 52 return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null; 53 } 54 } AuthorizationOptions

举个栗子:

 1 services.AddControllersWithViews(); 2 services.AddRazorPages(); 3 services.AddAuthorization(opt => 4 { 5 opt.AddPolicy(“授权策略1”, builer => { 6 builer.RequireRole(“admin”, “manager”); 7 builer.AddAuthenticationSchemes(“cookie”, “google”); 8 //继续配置…. 9 }); 10 opt.AddPolicy(“授权策略2”, builer => { 11 builer.RequireRole(“teacher”); 12 builer.AddAuthenticationSchemes(“wechat”, “qq”); 13 //继续配置…. 14 }); 15 }); View Code

 

3.1.2、具体注册了哪些服务:

  • 策略评估器IPolicyEvaluator:名字有点诡异。默认实现PolicyEvaluator,授权中间件委托它来实现身份验证和授权处理,它内部会调用AuthorizationService,进而执行所有授权处理器AuthorizationHandler
  • 授权服务IAuthorizationService:上一篇有说,不详述了,默认实现DefaultAuthorizationService,除了授权中间件会调用它来进行授权处理,我们业务代码中也可以调用它来做授权验证,比如:针对资源的特殊授权
  • 授权策略提供器IAuthorizationPolicyProvider:默认实现DefaultAuthorizationPolicyProvider,可以通过它来获取指定名称的授权,它就是从全局授权策略列表里去找,也就是上面说的AuthorizationOptions中
  • 授权处理器提供器IAuthorizationHandlerProvider:默认实现DefaultAuthorizationHandlerProvider,通过它来获取系统中所有的授权处理器,其实就是从IOC容器中获取
  • 授权评估器IAuthorizationEvaluator:默认实现DefaultAuthorizationEvaluator,授权处理器AuthorizationHandler在执行完授权后,结果是存储在AuthorizationHandlerContext中的,这里的评估器只是根据AuthorizationHandlerContext创建一个授权结果AuthorizationResult,给了我们一个机会来加入自己的代码而已
  • 授权处理器上下文对象的工厂IAuthorizationHandlerContextFactory:默认实现DefaultAuthorizationHandlerContextFactory,授权处理器AuthorizationHandler在授权时需要传入AuthorizationHandlerContext(上面说了授权完成后的结果也存储在里面)。所以在执行授权处理器之前需要构建这个上下文对象,就是通过这个工厂构建的,主要的数据来源就是 当前 或者  指定的  授权策略AuthorizationPolicy
  • 授权处理器IAuthorizationHandler:默认实现PassThroughAuthorizationHandler。授权的主要逻辑在授权处理器中定义,授权服务在做授权时会遍历系统所有的授权处理器逐一验证,而验证往往需要用到授权依据,PassThroughAuthorizationHandler比较特殊,它会看授权依据是否已经实现了IAuthorizationHandler,如过是,则直接把授权依据作为授权处理器进行执行。因为多数情况下一个授权处理器类型是专门针对某种授权依据定义的。

这些接口都是扩展点,就问你怕不怕?当然绝大部分时候我们不用管,默认的就足够用了。

 

3.2、注册授权中间件

主要注意的位置的为题,必须在路由和身份验证之后。

1 app.UseRouting();
2 app.UseAuthentication();
3 app.UseAuthorization();

扩展方法内部注册了AuthorizationMiddleware

 

四、请求阶段的处理流程

如果你对mvc稍有经验,就晓得在一个Action上使用[Authorize]就可以实施授权,现在我们假设我们在默认mvc项目中的HomeController定义如下Action,并应用授权标签

1         [Authorize(Policy = "p1")]//使用全局授权策略中的"p1"进行授权判断
2         [Authorize(AuthenticationSchemes = "google")]//只允许使用google身份验证登录的用户访问
3         [Authorize(Roles = "manager")]//只允许角色为manager的访问
4         public IActionResult Privacy()
5         {
6             return View();
7         }

4.1、授权中间件AuthorizationMiddleware

核心步骤如下:

步骤1、2得益于asp.net core 3.x的终结点路由,我们可以在进入MVC框架前就拿到Action及其之上应用的各种Atrribute,从而得到我们对当前授权策略定制所需要的数据

步骤3会根据得到IAuthorizeData集合(AuthorizeAttribute实现了IAuthorizeData,它不再是一个过滤器)创建当前准备用来做授权的策略。授权策略中 “身份验证方案列表” 和 “授权依据列表”,就是通过这里的标签来的。具体来说:

  • [Authorize(Policy = “p1”)]:会通过“p1”去全局授权策略(AuthorizationOptions对象中)拿到对应的策略,然后与当前策略合并,也就是把“p1”策略中的身份验证方案列表、授权依据列表与当前策略合并。
  • [Authorize(AuthenticationSchemes = “google”)]:用来定制当前策略的“身份验证方案列表”,当然最终和上面说的合并,
  • [Authorize(Roles = “manager”)]:会创建一个RolesAuthorizationRequirement类型的授权依据加入到当前策略中

这些Attribute可以应用多次,最终都是来定制当前授权策略的。后续步骤都会依赖此授权策略。

步骤4中,若发现本次授权策略中定义了多个身份验证方案,则会注意进行身份验证,得到的多张证件会合并到当前用户HttpContext.User中,当然默认身份验证得到的用户信息也在其中。

上面步骤4、6是委托策略评估器PolicyEvaluator来完成的,往下看..

 

4.2、策略评估器PolicyEvaluator

核心任务就两个,身份验证、进行授权

4.2.1、AuthenticateAsync

若策略没有设置AuthenticationSchemes,则只判断下当前请求是否已做身份验证,若做了就返回成功
若策略设置了AuthenticationSchemes,则遍历身份验证方案逐个进行身份验证处理 context.AuthenticateAsync(scheme); ,将所有得到的用户标识重组成一个复合的用户标识。

4.2.2、AuthorizeAsync

调用IAuthorizationService进行权限判断,若成功则返回成功。
否则     若身份验证通过则 PolicyAuthorizationResult.Forbid() 直接通知身份验证方案,做拒绝访问处理;否则返回质询

所以授权检查的任务又交给了授权服务AuthorizationService

 

4.3、授权服务AuthorizationService

核心步骤如下:

步骤2还会判断AuthorizationOptios.InvokeHandlersAfterFailure,来决定当某个处理器发生错误时,是否停止执行后续的授权处理器。

 

4.4、授权处理器AuthorizationHandler

前面讲过,默认只注册了一个PassThroughAuthorizationHandler授权处理器,它会遍历当前授权策略中实现了IAuthorizationHandler的授权依据(意思说这些对象既是授权处理器,也是授权依据)。直接执行它们。

public class PassThroughAuthorizationHandler : IAuthorizationHandler
{
    public async Task HandleAsync(AuthorizationHandlerContext context)
    {
        foreach (var handler in context.Requirements.OfType<IAuthorizationHandler>())
        {
            await handler.HandleAsync(context);
        }
    }
}

以基于角色的授权依据RolesAuthorizationRequirement为例,它继承于AuthorizationHandler<RolesAuthorizationRequirement>,且实现IAuthorizationRequirement

 1 public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement
 2 {
 3     //省略部分代码...
 4     public IEnumerable<string> AllowedRoles { get; }
 5     protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement)
 6     {
 7         if (context.User != null)
 8         {
 9             bool found = false;
10             if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any())
11             {
12                 // Review: What do we want to do here?  No roles requested is auto success?
13             }
14             else
15             {
16                 found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r));
17             }
18             if (found)
19             {
20                 context.Succeed(requirement);
21             }
22         }
23         return Task.CompletedTask;
24     }
25 }

 

五、最后

可以感受到asp.net core 3.x目前的权限设计棒棒哒,默认的处理方式已经能满足大部分需求,即使有特殊需求扩展起来也非常简单,前面注册部分看到注册了各种服务,且都有默认实现,这些服务在授权检查的不同阶段被使用,如果有必要我们可以自定义实现某些接口来实现扩展。本篇按默认流程走了一波,最好先看前一篇。这时候再去看官方文档或源码应该更容易。日常开发使用其实参考官方文档就足够了,无非就是功能权限和数据权限,看情况也许不会写后续的应用或扩展篇了。

UseAuthorization 及相关源代码 - dreamw - 博客园

mikel阅读(21)

来源: UseAuthorization 及相关源代码 – dreamw – 博客园

学习:《asp.net core 3.x 授权中的概念》《asp.net core 3.x 授权默认流程》

——————————————————————————————————–

在 Controller 中可以这样标注特性  /*注意,这里的名字取决于你添加AuthenticationHandler时的名字*/

[ApiController]
[Route("api/[controller]")]
public class ValueController : ControllerBase
{ [Authorize(AuthenticationSchemes ="jwtxxx", Policy ="role-policy")] 
     //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]  
     [HttpPost]
     public async Task<IActionResult> Test()
} 
    


——————————————————————————————————–

app.UseAuthorization 是Aspnet Core 3.X 的启动 Startup 的配置授权中间件。

提示:授权 Authorization 与身份验证(Scheme)、endpoint 路由(元属性)有关联!

官方定义:https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization?view=aspnetcore-3.1

https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationmiddleware?view=aspnetcore-3.1

Github 库源代码如下:

public static class AuthorizationAppBuilderExtensions
{
public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}

VerifyServicesRegistered(app);

return app.UseMiddleware<AuthorizationMiddleware>();
}

private static void VerifyServicesRegistered(IApplicationBuilder app)
{
// 在调用 UseAuthorization 之前校验 AddAuthorizationPolicy
// 用 AuthorizationPolicyMarkerService 以保证所有 services  被 added.
if (app.ApplicationServices.GetService(typeof(AuthorizationPolicyMarkerService)) == null)
{
throw new InvalidOperationException(Resources.FormatException_UnableToFindServices(
nameof(IServiceCollection),
nameof(PolicyServiceCollectionExtensions.AddAuthorization),
“ConfigureServices(…)”));
}
}

——————   AuthorizationMiddleware  的源代码:

https://github.com/dotnet/aspnetcore/blob/b56f84131af2e1ece61241a016e191f5f2fe3fc0/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs

 

public class AuthorizationMiddleware
{
// Property key is used by Endpoint routing to determine if Authorization has run
private const string AuthorizationMiddlewareInvokedWithEndpointKey = “__AuthorizationMiddlewareWithEndpointInvoked”;
private static readonly object AuthorizationMiddlewareWithEndpointInvokedValue = new object();

private readonly RequestDelegate _next;
private readonly IAuthorizationPolicyProvider _policyProvider;

public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
}

public async Task Invoke(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

var endpoint = context.GetEndpoint();

if (endpoint != null)
{
// EndpointRoutingMiddleware uses this flag to check if the Authorization middleware processed auth metadata on the endpoint.
// The Authorization middleware can only make this claim if it observes an actual endpoint.
context.Items[AuthorizationMiddlewareInvokedWithEndpointKey] = AuthorizationMiddlewareWithEndpointInvokedValue;
}

// IMPORTANT: Changes to authorization logic should be mirrored in MVC’s AuthorizeFilter
var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);
if (policy == null)
{
await _next(context);
return;
}

// Policy evaluator has transient lifetime so it fetched from request services instead of injecting in constructor
var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();

var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);

// Allow Anonymous skips all authorization
if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
{
await _next(context);
return;
}

// Note that the resource will be null if there is no matched endpoint
var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource: endpoint);

if (authorizeResult.Challenged)
{
if (policy.AuthenticationSchemes.Count > 0)
{
foreach (var scheme in policy.AuthenticationSchemes)
{
await context.ChallengeAsync(scheme);
}
}
else
{
await context.ChallengeAsync();
}

return;
}
else if (authorizeResult.Forbidden)
{
if (policy.AuthenticationSchemes.Count > 0)
{
foreach (var scheme in policy.AuthenticationSchemes)
{
await context.ForbidAsync(scheme);
}
}
else
{
await context.ForbidAsync();
}

return;
}

await _next(context);
}
}

 

[Authorize(AuthenticationSchemes JwtBearerDefaults.AuthenticationScheme)]
[Route(“api/[controller]”)]
[ApiController]
public class ValueController : ControllerBase

asp.net core 3.x 授权中的概念

mikel阅读(24)

ASP.NET core 3.x 授权中的概念,

 

前言

预计是通过三篇来将清楚ASP.NET core 3.x中的授权:1、基本概念介绍;2、asp.net core 3.x中授权的默认流程;3、扩展。

在完全没有概念的情况下无论是看官方文档还是源码都晕乎乎的,希望本文能帮到你。不过我也是看源码结合官方文档看的,可能有些地方理解不对,所以只作为参考。

要求对asp.net core的基础有所有了解:Host、依赖注入、日志、配置、选项模式、终结点路由、身份验证等。还是推荐A大博客

 

概述

归纳来说授权就是:某人  针对某个资源  可以做什么操作,如:张三   针对销售订单    可以查看、审核、取消等操作

  • 某人:这个好理解,只要登录系统的用户我们就晓得他是谁;额外的他属于某个角色、属于某个部门、甚至我们可以规定年龄段在18-30岁的能干什么,30-50岁的能干啥,这些都属于所属角色、所属部门、所属年龄段都是用户的一个属性,会作为权限判断的一个依据
  • 资源:可以任何形式的资源,比如销售订单、商品、等等;也可以有更复杂的规则,比如金额大于10000以上的,必须经过老总审核这种要求;另外比如一个页面也可以看做是资源,比如是否允许谁可以访问某个页面。对资源的限定也将作为权限判断的一部分
  • 操作:比如上面说的查看、审核、新增、修改..巴拉巴拉…当然操作也作为权限判断的一部分。

除了上面这3个概念外加一个权限判断逻辑,就组成了授权系统。下面逐一介绍asp.net core 3.x中授权系统涉及到的相关概念

注:
功能权限:这是我们通常说的某人(包含所属角色,所属部门等)可以访问某个菜单下的某个按钮
数据权限:上面说的订单金额大于10000的必须要经理角色才可以审核
这个说来也没啥区别,一个按钮通常是对应到mvc中的某个action,所以还是可以看成是操作;金额大于10000,这个也只是对资源的一种选定

还有一种情况,说一个按钮的点击对应到一个action,那么这个按钮到底是看做“操作”呢,还是把这个Action看成是一个页面地址,作为资源呢?这个就看怎么设计了,mvc默认是当做资源。

 

用户标识ClaimsPrincipal

ClaimsPrincipal理解为一个登录到系统的主体更合理。

在一个系统中可能同时存在多种身份验证方案,比如我们系统本身做了用户管理功能,使用最简单的cookie身份验证方案,或者使用第三方登录,微信、QQ、支付宝账号登录,通常一个身份验证方案可以产生一张证件(ClaimsIdentity),当然某个身份验证方案也可以将获得的Claim添加到一张现有的证件中,这个是灵活的。默认情况下,用户登录时asp.net core会选择设置好的默认身份验证方案做身份验证,本质是创建一个ClaimsPrincipal,并根据当前请求创建一个证件(ClaimsIdentity),然后将此ClaimsIdentity加入到ClaimsPrincipal,最后将这个ClaimsPrincipal设置到HttpContext.User属性上。身份验证不是本篇重点,详细描述参考:《asp.net core 3.x 身份验证-1涉及到的概念》。我们目前只要记住一个字符串代表一个身份验证方案,它可以从当前请求或第三方去获得一张证件(ClaimsIdentity)

当用户登录后,我们已经可以从HttpContext.User拿到当前用户,里面就包含一张或多张证件,后续的权限判断通常就依赖里面的信息,比如所属角色、所属部门,除了证件的信息我们也可以通过用户id去数据库中查询得到更多用户信息作为权限判断的依据。

 

资源

资源的概念很宽泛,上面说的销售订单、客户档案、属于资源,我们可以控制某个用户是否能查看、新增、审核订单。或者说一个页面也是一种资源,我们希望控制某用户是否能访问某个页面。在asp.net core中直接以object类型来表示资源,因为asp.net core作为一个框架,它不知道将来使用此框架的开发者到底是对什么类型的资源做权限限制。

在我们日常开发中经常在Action上应用AuthorizeAttribute标签来进行授权控制,其实这里就是将这个Action当做资源。由于目前asp.net core 3.x中默认使用终结点路由,所以现在在asp.net core 3.x中的默认授权流程中当前Endpoint就是资源

记住权限判断中不一定需要资源的参与,比如只要用户登录,就允许使用系统中所有功能。此时整个系统就是资源,允许所有操作。

 

授权依据IAuthorizationRequirement

试想这样一种权限需求:要求属于角色”经理”或”系统管理员”的用户可以访问系统任何功能。当我们做权限判断时我们可以从HttpContext.User得到当前用户,从其证件列表中总能找到当前用户的所属角色,那么这里需要进行比较的两个角色”经理”、”系统管理员”从哪里获得呢?
再比如:要求只要当前用户的证件中包含一个”特别通行证”的Calim,就允许他访问系统的任何功能。同上面的情况一样,在判断权限时我们可以知道当前登录用户的Calim列表,那需要进行比对的”特别通行证”这个字符串从哪来呢?

asp.net core将这种权限判断时需要用来比对的数据定义为IAuthorizationRequirement,我这里叫做”授权依据”,在一次权限判断中可能会存在多个判断,所以可能需要多个授权依据,文件后面会讲如何定制授权依据

其实某种意义上说“当前用户(及其包含的Calim列表)”也可以看做是一种依据,因为它也是在授权判断过程中需要访问的数据,但是这个我们是直接通过HttpContext.User来获取的,不需要我们来定义。

当我们针对某个页面或Action进行授权时可以直接从当前路由数据中获取Action名,在asp.net core 3.x中甚至更方便,可以在请求管道的早期就能获得当前请求的终结点。所以针对Action的访问也不需要定义成授权依据中

所以授权依据算是一种静态数据,为了更好的理解,下面列出asp.net core中已提供的几种授权依据

  • ClaimsAuthorizationRequirement
public string ClaimType { get; }
public IEnumerable<string> AllowedValues { get; }

将来在权限判断是会判断当前用户的Claim列表中是否包含一个类型为ClaimType的Claim,若AllowedValues有数据,则进一步判断是否完整包含AllowedValues中定义的值

  • DenyAnonymousAuthorizationRequirement:权限判断发现存在这个依据,则直接拒绝匿名用户访问
  • RolesAuthorizationRequirement:这就是最常见的基于角色的授权时会使用的,它定义了 public IEnumerable<string> AllowedRoles { get; } ,将来做权限判断时会看当前用户是否属于这里允许的角色中的一种
  • OperationAuthorizationRequirement:这个也比较常用,在做功能授权时比较常用。它定义了 public string Name { get; set; } ,Name代表当前操作名,比如“Order.Add”就是新增订单,将来权限判断是可以根据当前用户Id、所属角色和”Order.Add”到数据库去做对比
  • AssertionRequirement:这个就更强大了,它定义了  public Func<AuthorizationHandlerContext, Task<bool>> Handler { get; } ,将来权限判断时发现是这个类型,直接调用这个委托来进行权限判断,所以灵活性非常大

 

授权策略AuthorizationPolicy

策略同时作为身份验证方案和授权依据的容器,它包含本次授权需要的数据。

请求抵达时asp.net core会找到默认身份验证方案进行身份验证(根据请求获取用户ClaimsPrincipal),但有时候我们希望由自己来指定本次授权使用哪些身份验证验证方案,而不是使用默认的,这样将来身份验证过程中会调用设置的这几个身份验证方案去获得多张证件,此时HttpContext.User中就包含多张证件。所以授权策略里包含多种身份验证方案。

一次授权可能需要多种判断,比如同时判断所属角色、并且是否包含哪种类型的Calim并且…..,某些判断可能需要对比“授权依据”,所以一个授权策略包含多个授权依据。

另外我们可以将多个授权策略合并成一个对吗?所有的身份验证方案合并,所有的“授权依据”合并

将来授权检查时将根据身份验证方案获取当前用户的多个证件(里面包含很多Cliam可以用作权限判断),然后逐个判断授权依据,若都满足则认为授权检查成功。

若是针对某个资源的授权,授权方法大概是这样定义的xxxx.Authorize(策略,订单),这里不一定直接传入整个订单,可能只传入订单金额,这个根据业务需要。若是简单的情况只判断页面访问权限,则xxx.Authorize(策略),因为当前页面可以直接通过当前请求获取。

在asp.net core 3.x中启动阶段我们可以定义一个授权策略列表,这个看成是全局授权策略,一直存在于应用中。
在应用运行时,每次进行授权时会动态创建一个授权策略,这个策略是最终进行本次授权检查用的,它可能会引用某一个或多个全局策略,所谓的引用就是合并其“身份验证方案”列表和“授权依据列表”,当然其自身的“身份验证方案”列表和“授权依据列表”也是可以定制的,待会在AuthorizeAttribute部分再详细说

 

策略构造器AuthorizationPolicyBuilder

主要用来帮助创建一个授权策略(.net中典型的Builder模式),使用步骤是:

下面用伪代码感受下

var builder = new AuthorizationPolicyBuilder();
builder.RequireRole("Manager","Admin");
//builder....继续配置
var authorizationPolicy = builder.Build();

RequireRole将为最终会生成的策略中的“授权依据”列表加入一个RolesAuthorizationRequirement(“Manager”,”Admin”)。其它类似的api就不介绍了。

 

授权处理器AuthorizationHandler

上面说的当前用户、授权依据、以及授权时传递进来的资源都是可以看成是静态的数据,作为授权判断的依据,真正授权的逻辑就是用IAuthorizationHandler来表示的,先看看它的定义

public interface IAuthorizationHandler
{
    Task HandleAsync(AuthorizationHandlerContext context);
}

 

AuthorizationHandlerContext

中包含当前用户、授权依据列表和参与授权判断的资源,前者是根据授权策略中的多个身份验证方案经过身份验证后得到的;后者就是授权策略中的授权依据列表。在方法内部处理成功或失败的结果是直接存储到context对象上的。

一个应用中可以存在多个AuthorizationHandler,在执行授权检查时逐个调用它们进行检查,若都成功则本次授权成功。

 

针对特定授权依据类型  的  授权处理器AuthorizationHandler<TRequirement>

上面聊过授权依据是有多种类型的,将来还可能自定义,通常授权依据不同,授权的判断逻辑也不同。

  • 比如RolesAuthorizationRequirement这个授权依据,它里面包含角色列表,授权判断逻辑应该是判断当前用户是否属于这里面的角色;
  • 再比如OperationAuthorizationRequirement它里面定义了操作的名称,所以授权判断逻辑应该是拿当前用户以及它所属角色和这个操作(比如是“新增”)拿到数据库去做对比

所以这样看来一种“授权依据”类型应该对应一种“授权处理器”,所以微软定义了public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler ,这个TRequirement就代表这个授权处理器类型是针对哪种类型的“授权依据的”

一个授权策略AuthorizationPolicy是包含多个“授权依据”的,这其中可能有几个“授权依据”的类型是一样的,只是里面存储的值不同,以OperationAuthorizationRequirement为例,一个授权策略里可能包含如下授权依据列表:

new OperationAuthorizationRequirement{ Name="新增" }
new OperationAuthorizationRequirement{ Name="审核" }
new RolesAuthorizationRequirement("Manager","Admin");
//其它。。。

所以一个授权处理器AuthorizationHandler虽然只关联一种类型“授权依据”,但是一个授权处理器实例可以处理多个相同类型的“授权依据”

在授权过程中,每个AuthorizationHandler<TRequirement>会找到自己能处理的“授权依据”,逐个进行检查

 

针对特定授权依据类型、特定类型的资源  的  授权处理器AuthorizationHandler<TRequirement, TResource>

定义是这样的 public abstract class AuthorizationHandler<TRequirement, TResource> : IAuthorizationHandler
跟AuthorizationHandler<TRequirement>定义及处理逻辑唯一的区别是多了个TResource,在授权过程中是可以对给定资源进行判断的,资源在AuthorizationHandlerContext.Resource,这个是object类型,为了方便子类降重重写,所以由这里的父类将AuthorizationHandlerContext.Resource转换为TResource

干脆贴下源码吧

 1 public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler 2 where TRequirement : IAuthorizationRequirement 3 { 4 public virtual async Task HandleAsync(AuthorizationHandlerContext context) 5 { 6 foreach (var req in context.Requirements.OfType<TRequirement>()) 7 { 8 await HandleRequirementAsync(context, req); 9 } 10 } 11 12 protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement); 13 } 14 15 public abstract class AuthorizationHandler<TRequirement, TResource> : IAuthorizationHandler 16 where TRequirement : IAuthorizationRequirement 17 { 18 public virtual async Task HandleAsync(AuthorizationHandlerContext context) 19 { 20 if (context.Resource is TResource) 21 { 22 foreach (var req in context.Requirements.OfType<TRequirement>()) 23 { 24 await HandleRequirementAsync(context, req, (TResource)context.Resource); 25 } 26 } 27 } 28 29 protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TResource resource); 30 } View Code

 

合并AuthorizationHandler & AuthorizationRequirement

我们发现通常一个授权依据的类型会有个对应的授权处理器,如果只定义一个类,实现这两种接口事情不是更简单吗?举个例子:

 1 public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement 2 { 3 public RolesAuthorizationRequirement(IEnumerable<string> allowedRoles) 4 { 5 AllowedRoles = allowedRoles; 6 } 7 public IEnumerable<string> AllowedRoles { get; } 8 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) 9 { 10 if (context.User != null) 11 { 12 bool found = false; 13 if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any()) 14 { 15 // Review: What do we want to do here? No roles requested is auto success? 16 } 17 else 18 { 19 found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r)); 20 } 21 if (found) 22 { 23 context.Succeed(requirement); 24 } 25 } 26 return Task.CompletedTask; 27 } 28 } View Code

我们上面讲的微软定义的那几个授权依据基本都是这样定义的

 

何时实施授权检查?

如果是用的asp.net core 3.x之前的版本,那么在Action执行前做授权判断比较合适,常用的就是过滤器Filter咯。这个我不是特别确定,至少在.net framework时代是用的授权过滤器AuthorizeAttribute
请求  >  其它中间件  >   路由中间件  >  身份验证中间件  >  MVC中间件  >  Controller  >  [授权过滤器]Action

若是asp.net core 3.x之后,由于目前用的终结点路由,所以在 路由中间件 和 身份验证中间件  后做权限判断(使用授权中间件)比较合适,因为 路由中间件执行后我们可以从当前请求上下文中获取当前终结点(它代表一个Action或一个页面)。身份验证中间件执行后可以通过HttpContext.User获取当前用户,此时有了访问的页面和当前用户 就可以做权限判断了
请求  >  其它中间件  >   路由中间件(这里就拿到终结点了)  >  身份验证中间件  >  授权中间件  >  MVC中间件  >  Controller  >  Action

还有一种情况是在业务代码内部去执行权限判断,比如:希望销售订单金额大于10000的,必须要经理角色才可以审核,此时因为我们要先获取订单才知道它的金额,所以我们最好在Action执行内部根据路由拿到订单号,去数据库查询订单金额后,调用某个方法执行权限判断。

 

授权服务AuthorizationService

所以执行权限判断的点不同,AuthorizationService就是用来封装授权检查的,我们在不同的点都可以来调用它执行权限判断。看看接口定义

public interface IAuthorizationService
{
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);
}

user:要进行判断的用户,它里面可能存在一张或多张证件
resource:可能是一个终结点,也可能是一个页面RazorPage,也可能是一个订单(或者是单独的订单金额)
requirements:授权依据列表
policyName:一个授权策略名

在授权中间件和在业务逻辑代码中手动进行授权检查时都是调用此接口
它内部会去调用AuthorizationHandler来进行权限判断。

 

定制授权依据AuthorizeAttribute : IAuthorizeData

在asp.net core 3.x中 启动阶段可以配置一个全局策略列表,它一直存在于系统中,暂时称为“静态策略列表”
在每次执行授权检查时也需要一个策略,暂时称为“运行时授权策略”,授权中间件执行时就会创建此策略,然后调用AuthorizationService根据此策略进行权限判断,那此策略中的“授权依据”和“身份验证方案”这俩列表从哪来的呢?就是在Action通过AuthorizeAttribute来定制的,它实现 IAuthorizeData接口

如果你对早期版本mvc有一丢丢了解的话,你可能记得有个授权过滤器的概念AuthorizeAttribute,在Action执行前会先去做授权判断,若成功才会继续执行Action,否则就返回403.

在asp.net core 3.x中不是这样了,AuthorizeAttribute只是用来定制当前授权策略(AuthorizationPolicy)的,并不是过滤器,它实现IAuthorizeData接口,此接口定义如下:

public interface IAuthorizeData
{
    string Policy { get; set; }//直接指定此Action将来授权验证时要使用的授权策略AuthorizationPolicy,此策略会被合并到当前授权策略
    string Roles { get; set; } //它会创建一个基于角色的授权依据RolesAuthorizationRequirement,此依据会被放入当前授权策略
    string AuthenticationSchemes { get; set; }//它用来定义当前授权策略里要使用哪些身份验证方案
}

Policy属性指明从“静态策略列表”拿到指定策略,然后将其“授权依据”和“身份验证方案”这俩列表合并到“运行时授权策略”

看个例子:

1 [Authorize(AuthenticationSchemes = "cookie,jwtBearer")]
2 [Authorize(Roles = "manager,admin")]
3 [Authorize(policy:"test")]
4 [Authorize]
5 public IActionResult Privacy()
6 {
7      return View();
8 }

以上定制只是针对使用授权中间件来做权限判断时,对当前授权策略进行定制。若我们直接在业务代码中调用AuthorizationService手动进行权限判断呢,就截止调用咯。参考上面的描述

 

授权中间件AuthorizationMiddleware

上面我们介绍了何时实施授权检查,授权中间件(AuthorizationMiddleware)就是其中最为常用的一个授权检查点,相当于是一个授权检查的入口方法,它在进入MVC中间件之前就可以做授权判断,所以比之前的在Action上做判断更早。并且由于授权检查是根据终结点的,因此同一套授权代码可以应用在mvc/webapi/razorPages…等多种web框架。由于授权检查依赖当前访问的终结点(若不理解终结点,可以暂时认为它=Action及其之上应用的各种Attribute) 和 当前登录用户,因此  授权中间件  应该在   路由中间件 和 身份验证中间件  之后注册

 1 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
 2 {
 3        //略....
 4        app.UseHttpsRedirection();
 5        app.UseStaticFiles();
 6        app.UseRouting();
 7        app.UseAuthentication();
 8        app.UseAuthorization();
 9        app.UseEndpoints(endpoints =>
10        {
11            endpoints.MapRazorPages();
12        });
13 }

它的核心步骤大致如下:

所以重点是可以在执行mvc中间件之前拿到终结点及其之上定义的AuthorizeAttribute,从其中的数据就可以构建出本次权限判断的“授权策略”,有了授权策略就可以通过AuthorizationService执行授权判断,内部会使用到授权处理器AuthorizationHandler

 

结束

暂时就BB到这里,先有个大概印象,下一篇按asp.net core的默认授权流程走走源码,再结合此篇应该就差不多了…

C#操作MongoDB入门 - 荆棘中的百合花 - 博客园

mikel阅读(34)

来源: C#操作MongoDB入门 – 荆棘中的百合花 – 博客园

1、MongoDB安装及配置

(1)下载:

mongodb官网 https://www.mongodb.com/download-center

进入官网下载页,你会发现版本都是windows Server……  但是不要担心,本人亲测win10也可以用,所以放心大胆的下载就可以了。安装过程比较简单,下一步就可以了,就不多说了。

(2)配置

在“E:\MongoDB“(与默认安装位置不同,需要自己找到这个文件夹)目录下新建“data”文件夹,它将会作为数据存放的根文件夹。

在“E:\MongoDB”目录下新建“log”文件夹,作为日志文件夹。

 

    配置Mongo服务端:

以管理员身份打开CMD窗口,按照如下方式输入命令:
> E:
> cd MongoDB

>cd bin
> mongod –dbpath “E:\MongoDB\data”

 

然后在浏览器输入:http://localhost:27017/,可以看到如下提示:

You are trying to access MongoDB on the native driver port. For http diagnostic access, add 1000 to the port number (可能会和这个不太一样,有字就行)

如此,MongoDB数据库服务已经成功启动了。

 

封装服务:

还是以管理员身份运行cmd,进入bin目录,执行下列命令
>mongod -dbpath “D:\MongoDB\data” -logpath “D:\MongoDB\log\MongoDB.log” -install -serviceName “MongoDB”

这里–MongoDB.log就是开始建立的日志文件,–serviceName “MongoDB” 服务名为MongoDB。

接着启动mongodb服务

> E:\MongoDB>NET START MongoDB

服务启动成功后可以测试一下

进入bin目录,输入mongo显示如下信息,则服务启动成功。

E:\MongoDB\bin>mongo
MongoDB shell version: 3.2.9
connecting to: test
>

至此安装配置就完成了。

2、安装MongoDB的C#驱动

打开C#,新建项目,点击 工具>Nuget程序包管理器>管理解决方案的Nuget程序包>联机,

在搜索栏搜索mongodb

安装MongoDB.Driver,MongoDB.Bson,MongoDB.Driver.Core三个程序包。

然后添加引用

using MongoDB.Driver;
using MongoDB.Bson;

3、代码

接下来就是写代码了,以下是我写的最简单的增删改查操作(水平有限,凑合看)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using MongoDB.Driver;
using MongoDB.Bson;
namespace MongoDBTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            Mongo();
        }
        public void Mongo()
        {
            //建立连接
            var client = new MongoClient();
            //建立数据库
            var database = client.GetDatabase("TestDb");
            //建立collection
            var collection = database.GetCollection<BsonDocument>("foo");
            var document = new BsonDocument
            {
                {"name","MongoDB"},
                {"type","Database"},
                {"count",1},
                {"info",new BsonDocument{{"x",203},{"y",102}}}
            };
            //插入数据
            collection.InsertOne(document);
            var count = collection.Count(document);
            Console.WriteLine(count);
            //查询数据
            var document1 = collection.Find(document);
            Console.WriteLine(document1.ToString());
            //更新数据
            var filter = Builders<BsonDocument>.Filter.Eq("name""MongoDB");
            var update = Builders<BsonDocument>.Update.Set("name""Ghazi");
            collection.UpdateMany(filter, update);
            //删除数据
            var filter1 = Builders<BsonDocument>.Filter.Eq("count", 101);
            collection.DeleteMany(filter1);
            BsonDocument document2 = new BsonDocument();
            document2.Add("name""MongoDB");
            document2.Add("type""Database");
            document2.Add("count""1");
            collection.InsertOne(document2);
        }
    }
}

 

4、参考资料

http://mongodb.github.io/mongo-csharp-driver/2.2/getting_started/ 这个一定要静下心去看,虽然是英文的,但是看代码就行,还是不难的。

http://www.cnblogs.com/qq75077027/category/441114.html 这个是大神写的,讲的很全面。

http://wenku.baidu.com/link?url=NYAYJfx907QwrzVmcvrZAYqZUML6tiqClsJMNIxKjmR MongoDB权威指南中文版。