zoukankan      html  css  js  c++  java
  • 10分钟简单学习net core集成jwt权限认证,快速接入项目落地使用 熊泽

    什么是JWT

    JSON Web Token(JWT)是目前最流行的跨域身份验证、分布式登录、单点登录等解决方案。

    JWT的官网地址:https://jwt.io/

    通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限。

    JWT中包含了身份认证必须的参数以及用户自定义的参数,JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

    JSON Web令牌能做什么?

    1. 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由,服务和资源。Single Sign On是一种现在广泛使用JWT的功能,因为它的开销很小,并且能够在不同的域中轻松使用。
    2. 信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。因为JWT可以签名 - 例如,使用公钥/私钥对 - 您可以确定发件人是他们所说的人。此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。

    JSON Web令牌如何工作?

    在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,您不应该将令牌保留的时间超过要求。

    每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,通常在Authorization标头中,标题的内容应如下所示:

    Authorization: Bearer <token>

    在某些情况下,这可以是无状态授权机制。服务器的受保护路由将检查Authorization标头中的有效JWT ,如果存在,则允许用户访问受保护资源。如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。

    如果在标Authorization头中发送令牌,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。

    下图显示了如何获取JWT并用于访问API或资源:

    1、应用程序向授权服务器请求授权;

    2、校验用户身份,校验成功,返回token;

    3、应用程序使用访问令牌访问受保护的资源。

    JWT的实现方式是将用户信息存储在客户端,服务端不进行保存。每次请求都把令牌带上以校验用户登录状态,这样服务就变成了无状态的,服务器集群也很好扩展。

    更多理论知识可以查看官网,或者查看相关网友的文章,如下推荐文章:

    net core 集成jwt代码实现

    新建项目

    首先我们新建一个ASP.NET Core Web API项目,命名为 jwtWebAPI,选择目标框架.NET Core3.1,注意,如果勾选了https配置,postman请求的时候要设置去除ssl认证才能使用,建议不配置https。

    在nuget里面引用jwt集成的程序包,这里需要注意的是,如果你用的是.NET Core 3.1的框架的话,程序包版本选择3.1.10

    Microsoft.AspNetCore.Authentication.JwtBearer

    添加数据访问模拟api,新建控制器ValuesController

    其中api/value1是可以直接访问的,api/value2添加了权限校验特性标签 [Authorize]

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;
    
    namespace jwtWebAPI.Controllers
    {
        [ApiController]
        public class ValuesController : ControllerBase
        {
            [HttpGet]
            [Route("api/values1")]
            public ActionResult<IEnumerable<string>> values1()
            {
                return new string[] { "value1", "value1" };
            }
    
            /**
             * 该接口用Authorize特性做了权限校验,如果没有通过权限校验,则http返回状态码为401
             * 调用该接口的正确姿势是:
             * 1.登陆,调用api/Auth接口获取到token
             * 2.调用该接口 api/value2 在请求的Header中添加参数 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzM1MzM3IiwiZXhwIjoxNTYwMzM3MTM3LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.1S-40SrA4po2l4lB_QdzON_G5ZNT4P_6U25xhTcl7hI
             * Bearer后面有空格,且后面是第一步中接口返回的token值
             * */
            [HttpGet]
            [Route("api/value2")]
            [Authorize]
            public ActionResult<IEnumerable<string>> value2()
            {
                //这是获取自定义参数的方法
                var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims;
                var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value;
                return new string[] { "访问成功:这个接口登陆过的用户都可以访问", $"userName={userName}" };
            }
    
         
        }
    }

    添加模拟登陆生成Token的api,新建控制器AuthController

    这里模拟一下登陆校验,只验证了用户密码不为空即通过校验,真实环境完善校验用户和密码的逻辑。

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.IdentityModel.Tokens;
    using System;
    using System.Collections.Generic;
    using System.IdentityModel.Tokens.Jwt;
    using System.Linq;
    using System.Security.Claims;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace jwtWebAPI.Controllers
    {
        [ApiController]
        public class AuthController : Controller
        {
            /// <summary>
            /// 通过账号+密码获取Token
            /// </summary>
            /// <param name="userName"></param>
            /// <param name="pwd"></param>
            /// <returns>Token</returns>
            [AllowAnonymous]
            [HttpGet]
            [Route("api/auth")]
            public IActionResult GetToken(string userName, string pwd)
            {
                if (!string.IsNullOrEmpty(userName))
                {
                    //每次登陆动态刷新
                    Const.ValidAudience = userName + pwd + DateTime.Now.ToString();
                    // push the user’s name into a claim, so we can identify the user later on.
                    //这里可以随意加入自定义的参数,key可以自己随便起
                    var claims = new[]
                    {
                        new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                        new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(3)).ToUnixTimeSeconds()}"),
                        new Claim(ClaimTypes.NameIdentifier, userName)
                    };
                    //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit.
                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
                    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                    //.NET Core’s JwtSecurityToken class takes on the heavy lifting and actually creates the token.
                    var token = new JwtSecurityToken(
                        //颁发者
                        issuer: Const.Domain,
                        //接收者
                        audience: Const.ValidAudience,
                        //过期时间(可自行设定,注意和上面的claims内部Exp参数保持一致)
                        expires: DateTime.Now.AddMinutes(3),
                        //签名证书
                        signingCredentials: creds,
                        //自定义参数
                        claims: claims
                        );
    
                    return Ok(new
                    {
                        token = new JwtSecurityTokenHandler().WriteToken(token)
                    });
                }
                else
                {
                    return BadRequest(new { message = "username or password is incorrect." });
                }
            }
        }
    }

    Startup添加JWT验证的相关配置

    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.HttpsPolicy;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    using Microsoft.IdentityModel.Tokens;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace jwtWebAPI
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                //添加jwt验证:
                services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options => {
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateLifetime = true,//是否验证失效时间
                            ClockSkew = TimeSpan.FromSeconds(30),  //时间偏移量(允许误差时间)
                            ValidateAudience = true,//是否验证Audience(验证之前的token是否失效)
                            //ValidAudience = Const.GetValidudience(),//Audience
                            //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了
                            AudienceValidator = (m, n, z) =>
                            {
                                return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
                            },
                            ValidateIssuer = true,//是否验证Issuer(颁发者)
                            ValidAudience = Const.Domain,//Audience    【Const是新建的一个常量类】  接收者 
                            ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致      颁发者
                            ValidateIssuerSigningKey = true,//是否验证SecurityKey
                            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘钥SecurityKey
                        };
                        options.Events = new JwtBearerEvents
                        {
                            OnAuthenticationFailed = context =>
                            {
                                //Token expired
                                if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                                {
                                    context.Response.Headers.Add("Token-Expired", "true");
                                }
                                return Task.CompletedTask;
                            }
                        };
                    });
    
                services.AddControllers();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            { 
                //添加jwt验证
                app.UseAuthentication();
    
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                app.UseHttpsRedirection();
    
                app.UseRouting();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                });
            }
        }
    }

    创建常量类Const

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace jwtWebAPI
    {
        public class Const
        {
            /// <summary>
            /// 这里为了演示,写死一个密钥。实际生产环境可以从配置文件读取,这个是用网上工具随便生成的一个密钥(md5或者其他都可以)
            /// </summary>
            public const string SecurityKey = "48754F4C58F9EA428FE09D714E468211";
    
            /// <summary>
            /// 站点地址(颁发者、接受者),这里测试和当前本地运行网站相同,实际发到正式环境应为域名地址
            /// </summary>
            public const string Domain = "https://localhost:44345";
    
            /// <summary>
            /// 受理人,之所以弄成可变的是为了用接口动态更改这个值以模拟强制Token失效
            /// 真实业务场景可以在数据库或者redis存一个和用户id相关的值,生成token和验证token的时候获取到持久化的值去校验
            /// 如果重新登陆,则刷新这个值
            /// </summary>
            public static string ValidAudience;
        }
    }

    JWT登录授权测试成功

    把程序编译运行起来,打开postman,输入地址,首先测试不需要任何授权的

    正确地返回了数据,那么接下来我们测试JWT的流程。

    首先我们什么都不加调用接口:https://localhost:44345/api/values2,注意,我创建的时候是https的,大家注意看是http还是https

    返回了状态码401,也就是未经授权:访问由于凭据无效被拒绝。 说明JWT校验生效了,我们的接口收到了保护。

    调用模拟登陆授权接口:https://localhost:44345/api/auth?userName=xiongze&pwd=123456

    这里的用户密码是随便写的,因为我们模拟登陆只是校验了下非空,因此写什么都能通过。

    然后我们得到了一个xxx.yyy.zzz 格式的 token 值。我们把token复制出来。

    在刚才401的接口(https://localhost:44345/api/values2)请求header中添加JWT的参数,把我们的token加上去

    再次调用我们的模拟数据接口,但是这次我们加了一个header,KEY:Authorization     Value:Bearer Tokne的值

    这里需要注意 Bearer 后面是有一个空格的,然后就是我们上一步获取到的token,

    得到返回值,正确授权成功,我们是支持自定义返回参数的,上面代码里面有相关内容,比如用户名这些不敏感的信息可以带着返回。

    等token设置的过期时间到了,或者重新生成了新的Token,没有及时更新,那么我们的授权也到期,401,

    升级操作:接口权限隔离

    上面的操作是所有登录授权成功的角色都可以进行调用所有接口,那么我们现在想要进行接口隔离限制,

    也就是说,虽然授权登录了,但是我这个接口是指定权限访问的。

    比如说:删除接口只能管理员角色操作,那么其他角色虽然授权登录了,但是没有权限调用删除接口。

    我们在原来的操作进行改造升级看一下。

    添加类

    新建一个AuthManagement文件夹,添加PolicyRequirement类PolicyHandler类

    PolicyRequirement类:

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Http;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace jwtWebAPI.AuthManagement
    {
        /// <summary>
        /// 权限承载实体
        /// </summary>
        public class PolicyRequirement : IAuthorizationRequirement
        {
            /// <summary>
            /// 用户权限集合
            /// </summary>
            public List<UserPermission> UserPermissions { get; private set; }
            /// <summary>
            /// 无权限action
            /// </summary>
            public string DeniedAction { get; set; }
            /// <summary>
            /// 构造
            /// </summary>
            public PolicyRequirement()
            {
                //没有权限则跳转到这个路由
                DeniedAction = new PathString("/api/nopermission");
                //用户有权限访问的路由配置,当然可以从数据库获取
                UserPermissions = new List<UserPermission> {
                                  new UserPermission {  Url="/api/values3", UserName="admin"},
                              };
            }
        }
    
        /// <summary>
        /// 用户权限承载实体
        /// </summary>
        public class UserPermission
        {
            /// <summary>
            /// 用户名
            /// </summary>
            public string UserName { get; set; }
            /// <summary>
            /// 请求Url
            /// </summary>
            public string Url { get; set; }
        }
    }

    PolicyHandler类(注意2.x和3.x的区别)

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Http;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;
    
    namespace jwtWebAPI.AuthManagement
    {
        public class PolicyHandler : AuthorizationHandler<PolicyRequirement>
        {
            private readonly IHttpContextAccessor _httpContextAccessor;
            public PolicyHandler(IHttpContextAccessor httpContextAccessor)
            {
                _httpContextAccessor = httpContextAccessor;
            }
            protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
            {
                //赋值用户权限
                var userPermissions = requirement.UserPermissions;
                var httpContext = _httpContextAccessor.HttpContext;
    
                //请求Url
                var questUrl = httpContext.Request.Path.Value.ToUpperInvariant();
                //是否经过验证
                var isAuthenticated = httpContext.User.Identity.IsAuthenticated;
                if (isAuthenticated)
                {
                    if (userPermissions.GroupBy(g => g.Url).Any(w => w.Key.ToUpperInvariant() == questUrl))
                    {
                        //用户名
                        var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value;
                        if (userPermissions.Any(w => w.UserName == userName && w.Url.ToUpperInvariant() == questUrl))
                        {
                            context.Succeed(requirement);
                        }
                        else
                        {
                            ////无权限跳转到拒绝页面
                            //httpContext.Response.Redirect(requirement.DeniedAction);
                            return Task.CompletedTask;
                        }
                    }
                    else
                    {
                        context.Succeed(requirement);
                    }
                }
                return Task.CompletedTask;
            }
        }
    }

    添加指定角色

    在 AuthController 控制器的GetToken授权加入自定义的参数,如下

    new Claim("Role", userName)  //这里是角色,我使用登录账号admin代替

    在 AuthController 控制器里面添加无权限访问的方法

    [AllowAnonymous]
    [HttpGet]
    [Route("api/nopermission")]
    public IActionResult NoPermission()
    {
         return Forbid("No Permission!");
    }

    修改Startup配置

    在startup.cs的ConfigureServices 方法里面添加策略鉴权模式、添加JWT Scheme、注入授权Handler 

    修改后的文件如下

    using jwtWebAPI.AuthManagement;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.HttpsPolicy;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.DependencyInjection.Extensions;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    using Microsoft.IdentityModel.Tokens;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace jwtWebAPI
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services
                    //添加策略鉴权模式
                    .AddAuthorization(options =>
                    {
                        options.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement()));
                    })
                    //添加JWT Scheme
                    .AddAuthentication(s =>
                    {
                        s.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                        s.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                        s.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    })
                    //添加jwt验证:
                    .AddJwtBearer(options => {
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateLifetime = true,//是否验证失效时间
                            ClockSkew = TimeSpan.FromSeconds(30),  //时间偏移量(允许误差时间)
                            ValidateAudience = true,//是否验证Audience(验证之前的token是否失效)
                            //ValidAudience = Const.GetValidudience(),//Audience
                            //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了
                            AudienceValidator = (m, n, z) =>
                            {
                                return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
                            },
                            ValidateIssuer = true,//是否验证Issuer(颁发者)
                            ValidAudience = Const.Domain,//Audience    【Const是新建的一个常量类】  接收者 
                            ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致      颁发者
                            ValidateIssuerSigningKey = true,//是否验证SecurityKey
                            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘钥SecurityKey
                        };
                        options.Events = new JwtBearerEvents
                        {
                            OnAuthenticationFailed = context =>
                            {
                                //Token expired
                                if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                                {
                                    context.Response.Headers.Add("Token-Expired", "true");
                                }
                                return Task.CompletedTask;
                            }
                        };
                    });
    
                //注入授权Handler
                services.AddSingleton<IAuthorizationHandler, PolicyHandler>();
                //注入获取HttpContext
                services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
                services.AddControllers();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            { 
                //添加jwt验证
                app.UseAuthentication();
    
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                app.UseHttpsRedirection();
    
                app.UseRouting();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                });
                
            }
        }
    }

    添加api访问的方法

    在 ValuesController控制器添加指定权限访问的方法,如下:

            /**
            * 这个接口必须用admin
            **/
            [HttpGet]
            [Route("api/values3")]
            [Authorize("Permission")]
            public ActionResult<IEnumerable<string>> values3()
            {
                //这是获取自定义参数的方法
                var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims;
                var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value;
                var role = auth.FirstOrDefault(t => t.Type.Equals("Role"))?.Value;
    
                return new string[] { "访问成功:这个接口有管理员权限才可以访问", $"userName={userName}", $"Role={role}" };
            }

    不同权限测试访问

    我们同样的方法去模拟登录,https://localhost:44345/api/auth?userName=xiongze&pwd=123

    注意,账号先不用admin登录,然后用返回的token去请求我们刚刚添加的指定权限访问的接口,这个时候是没有权限访问的,因为这个是admin权限访问。

    我们同样的方法去模拟登录,https://localhost:44345/api/auth?userName=admin&pwd=123

    访问成功。

    源码下载地址

    Gitee:https://gitee.com/xiongze/jwtWebAPI.git

    参考文献

     
    欢迎关注订阅微信公众号【熊泽有话说】,更多好玩易学知识等你来取
    作者:熊泽-学习中的苦与乐
    公众号:熊泽有话说
    出处: https://www.cnblogs.com/xiongze520/p/15540035.html
    您可以随意转载、摘录,但请在文章内注明作者和原文链接。  

     

  • 相关阅读:
    桥梁模式
    原型模式
    css backgroundposition
    eclipse配置了maven,项目报错
    SQL 练习题目
    Springmvc + Ibatis 搭建 点餐系统
    Delphi 性能优化工具
    Delphi的接口陷阱
    delphi 内存管理,定期释放
    Delphi制作数据感知控件之浮想联翩
  • 原文地址:https://www.cnblogs.com/xiongze520/p/15540035.html
Copyright © 2011-2022 走看看