【.Net Core学习笔记】API JWT授权认证

 

API授权认证原理


通常我们在调用第三方API的时候都需要一个Token作为凭证,调用方自行根据第三方Token生成规则来生成Token,并作为参数传入。现在作为API提供方,我们需要一套认证机制来确保API和数据的安全,仅被授权的调用方所使用。

JWT(Json Web Token)


Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT是由.分割的如下三部分组成:

  • 头部(Header)

Header 一般由两个部分组成:

  • alg
  • typ

alg是是所使用的hash算法,如:HMAC SHA256或RSA,typ是Token的类型,在这里就是:JWT。

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

然后使用Base64Url编码成第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>
  • 载荷(Payload)

这一部分是JWT主要的信息存储部分,其中包含了许多种的声明(claims)。

Claims的实体一般包含用户和一些元数据,这些claims分成三种类型:

  • reserved claims:预定义的 一些声明,并不是强制的但是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(这里都使用三个字母的原因是保证 JWT 的紧凑)。

  • public claims: 公有声明,这个部分可以随便定义,但是要注意和 IANA JSON Web Token 冲突。

  • private claims: 私有声明,这个部分是共享被认定信息中自定义部分。

一个简单的Pyload可以是这样子的:

{
  "sub": "1234567890",
  "name": "Jonny Yan",
  "admin": true
}

这部分同样使用Base64Url编码成第二部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>
  • 签名(Signature)

Signature是用来验证发送者的JWT的同时也能确保在期间不被篡改。

在创建该部分时候你应该已经有了编码后的Header和Payload,然后使用保存在服务端的秘钥对其签名,一个完整的JWT如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

因此使用JWT具有如下好处:

  • 通用:因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。

  • 紧凑:JWT的构成非常简单,字节占用很小,可以通过 GET、POST 等放在 HTTP 的 header 中,非常便于传输。

  • 扩展:JWT是自我包涵的,包含了必要的所有信息,不需要在服务端保存会话信息, 非常易于应用的扩展。

关于更多JWT的介绍,网上非常多,这里就不再多做介绍。下面,演示一下 ASP.NET Core 中 JwtBearer 认证的使用方式。

JwtBearer认证实现


根据前面授权认证的原理,大致包括两部分:发放Token验证Token,先来看总体的框架搭建,后续会解释每个类的作用。

JwtHelper.cs:主要用于生成Token和验证Token

TokenAuthMiddleware.cs:Token验证中间件,用于截获请求

TokenModelJwt.cs:Token实体类,定义一些自己想放置在token中的属性


  • 发放Token

在JwtHelper类中,实现发放token方法,该方法输入参数为Token实体类

/// <summary>
        /// 生成AccessToken
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string IssueJWT(TokenModelJwt tokenModel)
        {
            DateTime UTC = DateTime.UtcNow;
            var claims = new List<Claim>
            {
                    new Claim(JwtRegisteredClaimNames.Sub,tokenModel.Id.ToString()),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),//JWT ID,JWT的唯一标识
                    new Claim(JwtRegisteredClaimNames.Iat, UTC.ToString(), ClaimValueTypes.Integer64),//Issued At,JWT颁发的时间,采用标准unix时间,用于验证过期
            };
            claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s)));
            JwtSecurityToken jwt = new JwtSecurityToken(
            issuer: ConfigHelper.GetConfig("Token:Issuer"),
            audience: tokenModel.Name,
            claims: claims,//声明集合
            expires: UTC.AddMinutes(int.Parse(ConfigHelper.GetConfig("Token:Expires"))),//指定token的生命周期,unix时间戳格式,非必须
            signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(ConfigHelper.GetConfig("Token:SecurityKey"))), SecurityAlgorithms.HmacSha256)
            );
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);//生成最后的JWT字符串
            return encodedJwt;
        }

Token实体类

public class TokenModelJwt
    {
        public int Id { get; set; }
        /// <summary>
        /// Name
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// Role
        /// </summary>
        public string Role { get; set; }

    }

在控制器中,新建一个Action用于给外部调用来生成Token

/// <summary>
        /// 获取Access Token
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="passWord"></param>
        /// <returns></returns>
        [HttpGet]
        public object GetAccessToken([FromQuery]string userName, [FromQuery] string passWord)
        {
            string jwtStr = string.Empty;
            bool suc = false;
            //查询数据库,检验用户是否存在
            if (true)
            {
                TokenModelJwt tokenModel = new TokenModelJwt
                {
                    Id = 1,
                    Name = "Jonny Yan",
                    Role = "Admin"
                };
                jwtStr = JwtHelper.IssueJWT(tokenModel);//登录,获取到一定规则的 Token 令牌
                suc = true;
            }
            else
            {
                jwtStr = "InValid User!";
                Logger.Info("InValid User");
            }
            return new { Token = jwtStr, Success = suc };
        }

至此已经完成Token发送的功能


  • 验证Token

我们发放了Token,调用方将Token至于请求header中传递过来,我们要验证Token的有效性,主要验证如下部分

  1. Token的格式是否正确
  2. Token的颁发者,过期时间等
  3. 解析Token中的用户信息(发放Token时,我们传入的token实体)

编辑Jwthelper.cs类,实现验证Token的方法

        /// <summary>
        /// 验证Token是否有效
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public static SecurityToken VerifyToken(string token)
        {
            var validationParameters = new TokenValidationParameters()
            {
                ValidateIssuerSigningKey = true, //验证私钥
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(ConfigHelper.GetConfig("Token:SecurityKey"))),
                ValidateLifetime = true,//是否验证Token有效期
                ClockSkew = TimeSpan.Zero, // 允许的服务器时间偏移量
                RequireExpirationTime = true,//否要求Token的Claims中必须包含Expires
                ValidateAudience = false, //是否验证订阅者
                ValidateIssuer = true,//是否验证提供者
                ValidIssuers = new List<string> { ConfigHelper.GetConfig("Token:Issuer") } //合法的token提供者
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            SecurityToken validatedToken = null;
            try
            {
                tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
            }
            catch (SecurityTokenException ex)
            {
                //log ex.message
                Logger.Error(ex.Message, ex);
            }
            catch (Exception ex)
            {
                //log ex.message
                Logger.Error(ex.Message, ex);
            }
            return validatedToken;
        }

编辑TokenAuthMiddleware.cs中间件类,截获请求,实现Token验证

public class TokenAuthMiddleware
    {
        /// <summary>
        /// http委托
        /// </summary>
        private readonly RequestDelegate _next;
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="next"></param>
        public TokenAuthMiddleware(RequestDelegate next)
        {
            _next = next;
        }
        /// <summary>
        /// 验证授权
        /// </summary>
        /// <param name="httpContext"></param>
        /// <returns></returns>
        public Task Invoke(HttpContext httpContext)
        {
            var headers = httpContext.Request.Headers;
            //检测是否包含'Authorization'请求头,如果不包含返回context进行下一个中间件,用于访问不需要认证的API
            if (!headers.ContainsKey("Authorization"))
            {
                return _next(httpContext);
            }
            var tokenStr = headers["Authorization"];

            string jwtStr = tokenStr.ToString().Replace("Bearer ", "");
            try
            {


                JwtSecurityToken access_token = JwtHelper.VerifyToken(jwtStr) as JwtSecurityToken;
                if (access_token != null)
                {
                    object role;
                    access_token.Payload.TryGetValue(ClaimTypes.Role, out role);
                    TokenModelJwt tm = new TokenModelJwt
                    {
                        Id = int.Parse(access_token.Payload.Sub),
                        Name = access_token.Payload.Aud[0]
                    };
                    var claimList = new List<Claim>();
                    if (role.GetType().Equals(typeof(JArray)))
                    {
                        IEnumerable enumerable = role as IEnumerable;
                        foreach (object element in enumerable)
                        {
                            claimList.Add(new Claim(ClaimTypes.Role, element.ToString()));
                        }
                    }
                    else
                    {
                        claimList.Add(new Claim(ClaimTypes.Role, role.ToString()));
                    }
                    claimList.Add(new Claim(ClaimTypes.Name, tm.Name));
                    claimList.Add(new Claim(ClaimTypes.PrimarySid, tm.Id.ToString()));
                    var identity = new ClaimsIdentity(claimList);
                    var principal = new ClaimsPrincipal(identity);
                    httpContext.User = principal;
                }
                return _next(httpContext);
            }
            catch (Exception ex)
            {
                Logger.Info(ex.Message, ex);
                return httpContext.Response.WriteAsync("Invalid Access Token");
            }
        }
    }

注意事项:在解析Token后,如果用户由多个角色,我们需要循环将每个Role都加入到Claim List中

至此已经完成Token验证的功能


  • 配置自定义的中间件

打开iStartup.cs类,编辑ConfigureServices方法,插入以下代码

 //自定义认证
            services.AddAuthorization(options =>
            {
                options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());
                options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build());
                options.AddPolicy("ClientOrAdmin", policy => policy.RequireRole("Admin", "Client").Build());
            });
            //微软自带认证
            services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(o =>
                    {
                        o.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuerSigningKey = true, //验证私钥
                            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["Token:SecurityKey"])),
                            ValidateLifetime = true,//是否验证Token有效期
                            ClockSkew = TimeSpan.Zero, // 允许的服务器时间偏移量
                            RequireExpirationTime = true,//否要求Token的Claims中必须包含Expires
                            ValidateAudience = false, //是否验证订阅者
                            ValidateIssuer = true,//是否验证提供者
                            ValidIssuers = new List<string> { Configuration["Token:Issuer"] } //合法的token提供者
                        };

                    });

编辑Config方法,启用中间件

至此已经完成自定义中间件的配置


  • 测试授权

在需要授权的Action中,加上Authorize特性,并且指定可以访问的角色(此处为策略,因为一个Action可能会被允许多个role访问,所以我们用策略来对role分组)

如果没由输入Token,访问API直接返回401错误

至此基于JWT授权认证的整个过程已经完成

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值