title: Asp.Net Core权限认证
date: 2022-10-27 16:17:52
tags:
- .NET

翻了很多的博客,文档,发现asp.net core自带的权限认证还是比较复杂的,极少有哪篇文章把整个体系涉及到的知识点都讲清楚的,因此自己整理出了这篇文章,相当于自己的一个个人理解和总结吧

关键概念

认证和授权

asp.net core中将权限认证分成了两个部分,一个是认证(Authentication),一个是授权(Authorization),他们的作用分别是:

  1. Authentication是根据不同方案(Scheme),来校验用户是否通过认证,比如用户名密码是否正确,通过了的话就会调用下面的语句来写入登录信息
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,claimPrincipal);
  1. Authorization是根据规则来判断用户是否通过了认证,通过了认证才授权这个用户访问对应的接口

举一个简单的例子:你们小区出入都有保安在守着,还有门禁,没有权限的人不让进入(接口上使用了Authorize特性),如果你是小区的业主,你得去物业那边登记一下个人资料,物业会记录你的手机号,身份证等个人信息,然后给你颁发一个门禁卡(这一步相当于认证),之后你每次进入小区,都需要刷门禁卡才能进入(授权)

用户角色

用户登录后我们可以通过HttpContext.User访问当前用户的个人信息,其中包含了以下三个重要的概念:

  1. ClaimsPrincipal:当前登录用户的角色
  2. ClaimsIdentity:当前用户持有的证件,比如身份证,银行卡
  3. Claim:证件上的每一个信息,比如身份证上的姓名,出生日期,过期时间,每个都是一个claim

还是接着上面那个例子讲,去物业登记的时候给你颁发的门禁卡,就相当于ClaimsIdentity,上面的每一个信息就是一个Claim,你是ClaimsPrincipal,一个人可以拥有多个证件,比如门禁卡、银行卡、身份证,在进入小区的时候就会通过门禁卡里面的信息判断是否授权给你进去

授权方式

Authorize特性其实已经包含了几种不同的授权方式

  1. 基于角色 Roles
  2. 基于策略 Policy
  3. 基于方案 Scheme

还是上面那个例子,进入小区的时候必须得刷门禁卡,刷身份证、银行卡肯定是进不去的,为什么呢?这就好比小区门禁的授权采用的是基于方案Scheme的,认证方式是物业认证,认证通过之后给你颁发了一个门禁卡,进入小区的时候根据物业指定的方案来验证颁发的门禁卡是否符合授权的要求

实操案例

新建一个Webapi项目,首先我们需要安装一个jwt的nuget包:

Microsoft.AspNetCore.Authentication.JwtBearer

首先我们需要添加认证的服务:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddJwtBearer(
        JwtBearerDefaults.AuthenticationScheme,
        opt =>
        {
            opt.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = "benji",
                ValidAudience = "benji",
                // 这里是签名秘钥
                IssuerSigningKey = new                    SymmetricSecurityKey(Encoding.ASCII.GetBytes("miyaomiyao12312312312312312312")),
                // 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
                ValidateLifetime = true
            };
        })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opt =>
    {
        // opt.Cookie
        opt.Cookie.Name = "MyCookie";
        opt.ExpireTimeSpan = TimeSpan.FromMinutes(10);
        opt.Events.OnRedirectToLogin = context =>
        {
            context.Response.Headers["Location"] = context.RedirectUri;
            // 认证失败返回401 否则返回的是404
            context.Response.StatusCode = 401;
            return Task.CompletedTask;
        };
    }).AddScheme<AuthenticationSchemeOptions, CustomAuthHandler>(CustomAuthHandler.SchemeName, it => { });

这里一共添加了三种认证方案,一种是基于Jwt的,一种是基于Cookie的,还有一种是我们自定义的认证方案,并且第一行就设置了默认的认证方案是Cookie认证

自定义认证方案需要继承AuthenticationHandler<AuthenticationSchemeOptions>类,下面是示例代码:

public class CustomAuthHandler:AuthenticationHandler<AuthenticationSchemeOptions>
{
    public CustomAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {

    }

    public const string SchemeName= "自定义验证";

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var auth=Request.Headers["Authorization"].ToString();
        if (auth == "自定义验证条件")
        {
            //验证成功后创建用户信息
            var claimsIdentity = new ClaimsIdentity(new Claim[]
            {
                new Claim(ClaimTypes.Name, "testUser"),
                new Claim(ClaimTypes.Role, "testRole")
            }, SchemeName);

            var principal = new ClaimsPrincipal(claimsIdentity);
            var ticket = new AuthenticationTicket(principal, this.Scheme.Name);
            return AuthenticateResult.Success(ticket);
        }
        else
        {
            return AuthenticateResult.Fail("验证失败");
        }
    }
}

认证方案添加后,我们需要在管道内添加认证和授权的中间件:

app.UseAuthentication();
app.UseAuthorization();

添加完成后就可以使用了,我们先给三种认证授权写对应的登录接口,从而颁发对应的token:

 [HttpPost]
    public IActionResult LoginByJwt(string username,string password)
    {
        if (username == "test" && password == "test")
        {
            //JWT载荷(Payload)
            var key = Encoding.ASCII.GetBytes("miyaomiyao12312312312312312312");
            var authTime = DateTime.UtcNow;
            var expiresAt = authTime.AddDays(7);
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Issuer = "benji",
                Audience = "benji",
                //自定义内容
                Subject = new ClaimsIdentity(new Claim[]
                {
                    new Claim(ClaimTypes.Name,"local"),
                    new Claim(ClaimTypes.Sid,"123456"),
                    new Claim("随便定义一个字段","字段对应的值"),
                    new Claim(ClaimTypes.Role,"admin"),
                    new Claim(ClaimTypes.Role,"user"),
                    new Claim(ClaimTypes.Role,"superadmin"),
                }),
                //过期时间
                Expires = expiresAt,
                //签证
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            var token = tokenHandler.CreateToken(tokenDescriptor);
            var tokenString = tokenHandler.WriteToken(token);
            return Ok(new
            {
                access_token = tokenString,
                token_type = "Bearer"
            });
        }
        else
        {
            return BadRequest("账号错误");
        }
    }

    [HttpPost]
    public async Task<IActionResult> LoginByCookie(string username, string password)
    {
        if (username == "test" && password == "test")
        {
            //1.创建cookie 保存用户信息,使用claim。将序列化用户信息并将其存储在cookie中
            var claims = new List<Claim>()
            {
                new Claim(ClaimTypes.MobilePhone,"123"),
                new Claim(ClaimTypes.Name,"test"),
                new Claim(ClaimTypes.Role,"admin"),
                new Claim("Id","123"),
                new Claim(ClaimTypes.Role,"user"),
                new Claim(ClaimTypes.Role,"superadmin"),
            };
 
            //2.创建声明主题 指定认证方式 这里使用cookie
            var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
 
            //3.配置认证属性 比如过期时间,是否持久化。。。。
            var authProperties = new AuthenticationProperties
            {
                // ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
                
                // 是否持久化,类似于前端勾选记住密码
                //IsPersistent = true,
 
                //IssuedUtc = <DateTimeOffset>,
            };
 
            //4.登录
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
            return Ok();
        }
        else
        {
            return BadRequest("账号密码错误");
        }
}

这里只提供了Jwt和Cookie的创建token的方案,自定义认证方案的话可以根据自己的需求来定制怎么颁发这个token,想弄在header上或者添加一个cookie都行,下面我们就可以对其进行授权测试了:

[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[HttpGet]
public List<string> TestCookie()
{
    List<string> res = new List<string>();
    foreach (var claim in HttpContext.User.Claims)
    {
        res.Add( claim.Type + "-" + claim.Value);
    }

    return res;
}

[Authorize(JwtBearerDefaults.AuthenticationScheme)]
[HttpGet]
public List<string> TestJwt()
{
    List<string> res = new List<string>();
    foreach (var claim in HttpContext.User.Claims)
    {
        res.Add( claim.Type + "-" + claim.Value);
    }

    return res;
}
[Authorize(AuthenticationSchemes = CustomAuthHandler.SchemeName)]
[HttpGet]
public List<string> TestCustom()
{
    List<string> res = new List<string>();
    foreach (var claim in HttpContext.User.Claims)
    {
        res.Add( claim.Type + "-" + claim.Value);
    }

    return res;
}

这是针对三种不同认证方案的对应授权策略的写法,其中基于Cookie认证的授权方案可以不用写AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme,因为默认的就是这个,这里就相当于上面说的基于Scheme方案的授权

下面我们再介绍一下基于角色和基于策略的授权,我们先添加一些自定义的授权策略:

builder.Services.AddAuthorization(options =>
{
    //基于角色组的策略
    options.AddPolicy("管理员", policy =>
    {
        policy.RequireRole("admin", "system");
        // 三种认证结果的证件都算进来
        policy.AuthenticationSchemes=new []{ JwtBearerDefaults.AuthenticationScheme,CustomAuthHandler.SchemeName,CookieAuthenticationDefaults.AuthenticationScheme};
    });
    //基于用户名
    options.AddPolicy("用户名是张三", policy => policy.RequireUserName("张三"));
    // 基于ClaimType
    options.AddPolicy("地址是中国", policy => policy.RequireClaim(ClaimTypes.Country,"中国"));
    //自定义值
    options.AddPolicy("自定义Claim要求", policy => policy.RequireClaim("date","2017-09-02"));
   
});

上面都是基于策略的授权,每个策略都可以使用不同认证方案颁发的证件,如果不写的话就是只能使用默认方案的认证结果证件,当然,这里也可以自定义自己的策略,只需要继承IAuthorizationRequirement接口就行了,如下:



/// <summary>
/// 最小年龄限制
/// </summary>
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public MinimumAgeRequirement(int age)
    {
        MinimumAge = age;
    }

    public int MinimumAge { get; set; }
}


public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(
            c => c.Type == ClaimTypes.DateOfBirth );

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

MinimumAgeRequirement只需要包含我们需要处理的一些信息,当然不写也行,主要是这个Handler才是核心,在这里我们要写对应的校验逻辑,上面这个示例是微软官方文档中的用户最小年龄判断,接下来我们需要添加对应的服务和策略:

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

options.AddPolicy("自定义策略", policy =>
                  {
                      policy.Requirements.Add(new MinimumAgeRequirement(1));
                  });

注意,如果同一个Requirement有多个对应的handler,那么都会执行,只要一个校验的结果是fail,那么整个都是fail,然后在我们颁发token时,需要添加对应的字段:

  new Claim(ClaimTypes.DateOfBirth,DateTime.Now.AddYears(-200).ToString()),

接着我们写一个对应的权限校验接口:

[Authorize(Policy = "自定义策略",AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[HttpGet]
public List<string> TestCustomPolicy()
{
    List<string> res = new List<string>();
    foreach (var claim in HttpContext.User.Claims)
    {
        res.Add( claim.Type + "-" + claim.Value);
    }

    return res;
}

那么基于策略的授权方案这里就介绍完了,下面介绍一下基于角色的授权方案,这种方案比较简单,就是判断认证的证件里面的Role这个Claim是否有自己要求的角色,比如:

[Authorize(Roles = "admin",AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[HttpGet]
public List<string> TestRole()
{
    List<string> res = new List<string>();
    foreach (var claim in HttpContext.User.Claims)
    {
        res.Add( claim.Type + "-" + claim.Value);
    }

    return res;
}

在颁发token的时候一定要加对应的角色:

new Claim(ClaimTypes.Role,"admin"),
new Claim(ClaimTypes.Role,"user"),
new Claim(ClaimTypes.Role,"superadmin"),

角色可以有多个,授权的时候也可以判断多个:

[Authorize(Roles = "admin,user")]

不过直接使用这种基于角色的授权有个问题,就是必须得指定使用某个认证方案,否则使用的是默认的认证方案给出的证件(也有可能是有但是我没找到对应的方法),所以如果需要针对所有的认证方案都进行收集并判断是否有某个角色,可以直接使用基于策略的授权方法,比如上面写过的:

options.AddPolicy("管理员", policy =>
                  {
                      policy.RequireRole("admin", "system");
                      // 三种认证的结果都算进来
                      policy.AuthenticationSchemes=new []{ JwtBearerDefaults.AuthenticationScheme,CustomAuthHandler.SchemeName,CookieAuthenticationDefaults.AuthenticationScheme};
                  });

总结

上面基本把所有认证授权相关的内容都介绍完了,还有一些更细节的地方,比如默认的认证方案可以根据条件动态选择、Token的过期刷新、自定义AuthorizeAttribute特性来定义自己的授权方法,这些就属于细枝末节的东西了,可以在后面的参考链接中找到官方文档、博客自行查阅

博客中所有的示例代码可以在这下载: https://github.com/li-zheng-hao/AspNetCore.AuthDemo

参考链接

  1. https://aspdotnetcore.net/docs/claims-based-authentication/
  2. https://zhuanlan.zhihu.com/p/359691679
  3. https://www.cnblogs.com/diudiu1/p/15818648.html
  4. https://zhuanlan.zhihu.com/p/359691679
  5. https://blog.csdn.net/icoolno1/article/details/108190010
  6. https://www.cnblogs.com/fanfan-90/p/11918537.html
  7. https://www.cnblogs.com/danvic712/p/use-cookie-authentication-in-asp-net-core.html
  8. Cookie刷新有效期
  9. https://zhuanlan.zhihu.com/p/364928893
  10. https://blog.csdn.net/qq_25991955/article/details/100540155
  11. https://www.cnblogs.com/axzxs2001/p/7482777.html
  12. 自定义授权Handler和Requirement
  13. 基于角色授权
  14. https://www.cnblogs.com/RainingNight/p/authorization-in-asp-net-core.html#iauthorizationrequirement
  15. asp.net core 6认证示例代码
  16. 微软官方认证授权的文档