ASP.NET CORE JWT认证

ASP.NET CORE版本:.NET 5.0 

关于JWT的基础知识,请大家上网搜一下,这里主要从代码层面讲解。

首先新建一个ASP.NET CORE Web api的空项目,然后新建一个LoginController的api控制器。

从nuget中安装包:Microsoft.AspNetCore.Authentication.JwtBearer;

1、生成Token

先看代码:

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class LoginController : Controller
    {
        private readonly PermissionRequirement _permissionRequirement;
        public LoginController(PermissionRequirement permissionRequirement)
        {
            _permissionRequirement = permissionRequirement;
        }
        [HttpGet]
        public IActionResult GetJwtToken(string userName, string password)
        {
            var claims = new List<Claim> {
                        new Claim(ClaimTypes.GivenName, "mark"),
                        new Claim(ClaimTypes.Name, userName),
                        new Claim(JwtRegisteredClaimNames.Jti, "10000") };
            var roles = new List<Claim>();
            if (userName == "ncy")
            {
                roles.Add(new Claim(ClaimTypes.Role, "admin"));
            }
            else
            {
                roles.Add(new Claim(ClaimTypes.Role, "normal"));
            }
            claims.AddRange(roles);
            var token = JwtToken.BuildJwtToken(claims.ToArray(), _permissionRequirement);
            var jm = new RespEntity();
            jm.code = 0;
            jm.data = token;
            jm.msg = "success";

            return Json(jm);
        }
    }

我这里根据输入的用户名来添加不同的角色是为了做角色的权限控制,大家不用理会这个。

PermissionRequirement是认证需要的参数字段组成的一个类,定义如下:

    public class PermissionRequirement : IAuthorizationRequirement
    {
        /// <summary>
        /// 用户权限集合,一个订单包含了很多详情,
        /// 同理,一个网站的认证发行中,也有很多权限详情(这里是Role和URL的关系)
        /// </summary>
        public List<PermissionItem> Permissions { get; set; }
        /// <summary>
        /// 无权限action
        /// </summary>
        public string DeniedAction { get; set; }

        /// <summary>
        /// 认证授权类型
        /// </summary>
        public string ClaimType { internal get; set; }
        /// <summary>
        /// 请求路径
        /// </summary>
        public string LoginPath { get; set; } = "/Api/Login";
        /// <summary>
        /// 发行人
        /// </summary>
        public string Issuer { get; set; }
        /// <summary>
        /// 订阅人
        /// </summary>
        public string Audience { get; set; }
        /// <summary>
        /// 过期时间
        /// </summary>
        public TimeSpan Expiration { get; set; }
        /// <summary>
        /// 签名验证
        /// </summary>
        public SigningCredentials SigningCredentials { get; set; }


        /// <summary>
        /// 构造
        /// </summary>
        /// <param name="deniedAction">拒约请求的url</param>
        /// <param name="permissions">权限集合</param>
        /// <param name="claimType">声明类型</param>
        /// <param name="issuer">发行人</param>
        /// <param name="audience">订阅人</param>
        /// <param name="signingCredentials">签名验证实体</param>
        /// <param name="expiration">过期时间</param>
        public PermissionRequirement(string deniedAction, List<PermissionItem> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials, TimeSpan expiration)
        {
            ClaimType = claimType;
            DeniedAction = deniedAction;
            Permissions = permissions;
            Issuer = issuer;
            Audience = audience;
            Expiration = expiration;
            SigningCredentials = signingCredentials;
        }
    }

实际生成token的函数:

    public static string BuildJwtToken(Claim[] claims, PermissionRequirement                 
    permissionRequirement)
        {
            var now = DateTime.Now;
            // 实例化JwtSecurityToken
            var jwt = new JwtSecurityToken(
                issuer: permissionRequirement.Issuer,
                audience: permissionRequirement.Audience,
                claims: claims,
                notBefore: now,
                expires: now.Add(permissionRequirement.Expiration),
                signingCredentials: permissionRequirement.SigningCredentials
            );
            // 生成 Token
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            return encodedJwt;
        }

我们先来看一下Claim这个类。这个类其实是JWT的荷载,即消息体。我们在实例化一个Claim的时候传入了2个参数,一个type,一个value,其实就是键值对。我们打开jwt解析token的网站:JSON Web Tokens - jwt.io,把我们生成的token复制到左边的文本框里,截图如下:

 payload里的前3个键值对是不是我们用Claim定义的(这里的type是字符串)?至于下面那几个键值对,我们看一下函数BuildJwtToken,这几个的值是在PermissionRequirement里赋值的。

2、ClaimTypes.Role

new Claim(ClaimTypes.Role, "admin")。把当前用户的角色信息也写入到荷载中。我这里的admin是测试的角色,实际的角色应该是用户登录后,从数据库获取该用户所属的角色。

把角色写入到荷载中有一个好处:在做角色权限验证的时候,不需要从数据库中去查询当前用户的所属角色,减少了数据库的查询次数。但是也有几个缺点:

1、如果该用户有很多角色,会导致token变大,而每次客户端与服务器交互都要发送token,所以会导致网络流量变大。

2、如果修改了用户的所属角色,那么该用户必须重新登陆。因为JWT Token是一次性的,必须重新登陆才能获得新的token。

对于这个问题,我认为比较好的方案是:把用户的角色信息存储在缓存中(比如redis),然后每次修改用户的角色的信息的时候顺便更新一下缓存。

3、安全性

我们刚才把生成的token放入到jwt官方的token解析工具中就可以解析出荷载的内容,这就说明了jwt是存在安全问题的。其实jwt只是对payload做了base64编码,只要对荷载部分进行base64解码就可以得到荷载的内容。所以说我们不能把重要的信息放在paylaod里面。

4、添加JWT认证服务

下面的代码是写在startup类中ConfigureServices函数中,用来注册JWT认证

            var keyBytes = Encoding.Default.GetBytes("MyJwtWebTokenApplication");
            var signingKey = new SymmetricSecurityKey(keyBytes);
            var issuer = "CoreShop";
            var audience = "CoreCms";
            // 令牌验证参数
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,   //是否验证SecurityKey
                IssuerSigningKey = signingKey,  //拿到SecurityKey
                ValidateIssuer = true, //是否验证Issuer
                ValidIssuer = issuer,//发行人 //Issuer,这两项和前面签发jwt的设置一致
                ValidateAudience = true, //是否验证Audience
                ValidAudience = audience,//订阅人
                ValidateLifetime = true,//是否验证失效时间
                ClockSkew = TimeSpan.FromSeconds(0),
                RequireExpirationTime = true,
            };
            services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(o =>
            {
                o.TokenValidationParameters = tokenValidationParameters;
                o.Events = new JwtBearerEvents
                 {
                     OnChallenge = context =>
                     {
                         context.Response.Headers.Add("Token-Error", context.ErrorDescription);
                         return Task.CompletedTask;
                     },
                     OnAuthenticationFailed = context =>
                     {
                         var token = context.Request.Headers["Authorization"].ObjectToString().Replace("Bearer ", "");
                         var jwtToken = (new JwtSecurityTokenHandler()).ReadJwtToken(token);

                         if (jwtToken.Issuer != issuer)
                         {
                             context.Response.Headers.Add("Token-Error-Iss", "issuer is wrong!");
                         }

                         if (jwtToken.Audiences.FirstOrDefault() != audience)
                         {
                             context.Response.Headers.Add("Token-Error-Aud", "Audience is wrong!");
                         }

                         // 如果过期,则把<是否过期>添加到,返回头信息中
                         if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                         {
                             context.Response.Headers.Add("Token-Expired", "true");
                         }
                         return Task.CompletedTask;
                     }
                 };
            });

这里的几个常量字符串应该是定义在配置文件中的,为了方便,我直接写在这里了。

注意这里的密钥长度必须要大于等于16个字节,不然在生成token的时候会报错

令牌验证参数中的ClockSkew参数(默认时钟偏差字段),您在生成token时指定的过期时间要加上这个偏差时间才是token的过期时间。它的默认值是300秒,定义如下:

 这个地方感觉有点坑,大家要小心哦。

接下来就是指定认证方案,默认的方案是Bearer。使用这个方案的时候,认证的结果是JWT默认的返回结果。主要体现在返回的状态码上,如果是认证成功,状态码就是200。失败(Challenge)就是401。认证成功但是没有授权(Forbid)就是403。

除了使用默认的方案,我们还可以自定义方案。定义一个类继承AuthenticationHandler<AuthenticationSchemeOptions>,然后重写您想要自定义的返回结果(Authenticate、Challenge、Forbid),代码如下:

x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = nameof(ApiResponseForAdminHandler);
x.DefaultForbidScheme = nameof(ApiResponseForAdminHandler);
public class ApiResponseForAdminHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public ApiResponseForAdminHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            throw new NotImplementedException();
        }
        protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            Response.ContentType = "application/json";    
            var jm = new AdminUiCallBack();
            jm.code = 401;
            jm.data = 401;
            jm.msg = "很抱歉,您无权访问该接口,请确保已经登录!";
            await Response.WriteAsync(JsonConvert.SerializeObject(jm));
        }

        protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
        {
            Response.ContentType = "application/json";        
            var jm = new AdminUiCallBack();
            jm.code = 403;
            jm.msg = "很抱歉,您的访问权限等级不够,联系管理员!!";
            await Response.WriteAsync(JsonConvert.SerializeObject(jm));

        }

    }

这里的HandleAuthenticateAsync我们直接抛出了没有实现的异常,因为在AddAuthentication时DefaultAuthenticateScheme=JwtBearerDefaults.AuthenticationScheme;所以不会调用这个函数。但是下面的challenge和forbidden是使用这个类做为它的方案。

这种情况下,即使认证没有成功,返回的状态码也是200,我们需要从AdminUiCallBack类中获取认证的结果。

不过使用自定义方案时,需要加上一行代码,指定提供方案的类

.AddScheme<AuthenticationSchemeOptions, ApiResponseForAdminHandler>(nameof(ApiResponseForAdminHandler), o => { })

这是在AddJwtBearer函数后面调用。这里有一点需要注意一下,使用了自定义方案,JwtBearerEvents里面定义的事件委托就会失效,所以这里是二选一。如果方案太复杂,可以使用自定义方案,反之,可以使用事件委托来处理。如果使用事件委托,就不需要调用AddScheme函数了。

最后在Configure函数中调用app.UseAuthentication();启用认证中间件即可。

5、C/S客户端认证

webapi不仅仅是b/s前端可以调用,c# winform也可以调用

            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token);
            var response = await client.GetAsync("http://localhost:5000/api/test/GetMyAddress");
            if (response.StatusCode == System.Net.HttpStatusCode.OK)
            {
                var content = await response.Content.ReadAsStringAsync();
                Console.WriteLine($"api返回数据:{content}");
            }
            else
            {
                Console.WriteLine($"请求api接口失败,状态码:{((int)response.StatusCode)}");
            }

请大家看一下第二行代码,在new一个AuthenticationHeaderValue对象时,一个参数是认证方案,一个是token的值,这个认证方案一定要带上。

在服务端解析token的时候也要先把Bearer这个字符串去掉才能解析出token

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值