.NET CORE 3.1 integrates JWT authentication and authorization

 Official website: JSON Web Tokens - jwt.io JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS). https://jwt.io/ JWT: The full name is JSON Web Token, which is currently the most popular solution for cross-domain authentication, distributed login, and single sign-on.

 

In layman's terms, JWT is a token that can represent the user's identity, and the JWT token can be used to verify the user's identity in the API interface to confirm whether the user has permission to access the API.

Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access the routes, services, and resources allowed by that token.

In authentication, when a user successfully logs in with their credentials, a JSON Web Token is returned. Since tokens are credentials, great care must be taken to prevent security issues. In general, you should not keep tokens longer than required.

Whenever a user wants to access a protected route or resource, the user agent should send the JWT using bearer mode, usually in the Authorization header , the content of the header should look like this:

Authorization: Bearer <token>

1. The application requests authorization from the authorization server;

2. Verify the user identity, if the verification is successful, return the token;

3. The application uses the access token to access the protected resource.

 The implementation of JWT is to store user information on the client, and the server does not save it. Each request brings the token to verify the user's login status, so that the service becomes stateless and the server cluster is easy to expand.

For more theoretical knowledge, you can check the official website, or check the articles of related netizens, the following recommended articles:

Refer to the jwt integrated package in nuget. It should be noted here that if you are using the framework of .NET Core 3.1, the package version should be 3.1.7

Microsoft.AspNetCore.Authentication.JwtBearer

Add data access simulation api, create new controller ValuesController

Among them, api/value1 can be directly accessed, and api/value2 has added the permission verification feature label [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}" };
        }

     
    }
}

Add the api that simulates login to generate Token, and create a new controller AuthController

Here is a simulation of the login verification, which only verifies that the user password is not empty and passes the verification. The real environment improves the logic of verifying the user and password.

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 adds related configurations for JWT verification

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();
            });
        }
    }
}

Create a constant class 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 login authorization test succeeded

A status code of 401 was returned, which is Unauthorized: Access is denied due to invalid credentials. It means that the JWT verification has taken effect, and our interface has been protected.

Call the simulated login authorization interface: https://localhost: 44345 /api/auth?userName=xiongze&pwd=123456

The user password here is written casually, because our simulated login is only verified to be non-empty, so everything can be written.

Then we got a token value in the format of xxx.yyy.zzz. We copy the token out.

Add JWT parameters in the request header of the 401 interface ( https://localhost:44345/api/values2 ) just now, and add our token

Call our simulated data interface again, but this time we add a header, KEY: Authorization Value: the value of Bearer Tokne

It should be noted here that there is a space after Bearer, and then it is the token we obtained in the previous step.

Get the return value, the correct authorization is successful, we support custom return parameters, the above code contains relevant content, such as user name and other insensitive information can be returned with it.

When the expiration time set by the token is up, or a new Token is regenerated and not updated in time, then our authorization will also expire, 401,

Upgrade operation: interface authority isolation

The above operation means that all roles with successful login authorization can call all interfaces, so now we want to implement interface isolation restrictions,

That is to say, although the login is authorized, my interface is accessed with specified permissions.

For example: the deletion interface can only be operated by the administrator role, so although other roles are authorized to log in, they do not have permission to call the deletion interface.

Let's take a look at the transformation and upgrading of the original operation.

add class

Create a new AuthManagement folder, add PolicyRequirement class and PolicyHandler class ,

PolicyRequirement class:

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 class (note the difference between 2.x and 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;
        }
    }
}

Add specified role

Add custom parameters to the GetToken authorization of the AuthController controller, as  follows 

new Claim("Role", userName) //Here is the role, I use the login account admin instead

Add a method for unauthorized access in the  AuthController  controller 

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

Modify the Startup configuration

Add policy authentication mode, add JWT Scheme, and inject authorization Handler in the ConfigureServices method of startup.cs 

The modified file is as follows

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();
            });
            
        }
    }
}

Add the method of api access

Add the method of specifying permission access in  the ValuesController controller, as follows:

/**
        * 这个接口必须用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}" };
        }

Test Access with Different Permissions

We use the same method to simulate login, https://localhost:44345/api/auth?userName=xiongze&pwd=123

Note that the account does not need to log in with admin first, and then use the returned token to request the interface we just added for access to the specified permission. At this time, there is no permission to access, because this is admin permission access.

We use the same method to simulate login, https://localhost:44345/api/auth?userName=xiongze&pwd=123

Note that the account does not need to log in with admin first, and then use the returned token to request the interface we just added for access to the specified permission. At this time, there is no permission to access, because this is admin permission access.

We use the same method to simulate login, https://localhost:44345/api/auth?userName=admin&pwd=123

The visit was successful.

Blog reprint address: https://www.cnblogs.com/xiongze520/p/15540035.html https://www.cnblogs.com/xiongze520/p/15540035.html

Guess you like

Origin blog.csdn.net/qq_26695613/article/details/129822445