嘗試從零開始構建我的商城 (二) :使用JWT保護我們的資訊保安,完善Swagger配置

有什麼不能一笑而過呢發表於2020-11-06

前言

GitHub地址

https://github.com/yingpanwang/MyShop/tree/dev_jwt
此文對應分支 dev_jwt

此文目的

 上一篇文章中,我們使用Abp vNext構建了一個可以執行的簡單的API,但是整個站點沒有一個途徑去對我們的API訪問有限制,導致API完全是裸露在外的,如果要執行正常的商業API是完全不可行的,所以接下來我們會通過使用JWT(Json Web Toekn)的方式實現對API的訪問資料限制。

JWT簡介

什麼是JWT

現在API一般是分散式且要求是無狀態的,所以傳統的Session無法使用,JWT其實類似於早期API基於Cookie自定義使用者認證的形式,只是JWT的設計更為緊湊和易於擴充套件開放,使用方式更加便利基於JWT的鑑權機制類似於http協議也是無狀態的,它不需要在服務端去保留使用者的認證資訊或者會話資訊。

JWT的組成

JWT (以下簡稱Token)的組成分為三部分 header,playload,signature 完整的Token長這樣

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoid2FuZ3lpbmdwYW4iLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjIiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9tb2JpbGVwaG9uZSI6IjEyMiIsImV4cCI6MTYwNDI4MzczMSwiaXNzIjoiTXlTaG9wSXNzdWVyIiwiYXVkIjoiTXlTaG9wQXVkaWVuY2UifQ.U-2bEniEz82ECibBzk6C5tuj2JAdqISpbs5VrpA8W9s

header

包含型別及演算法資訊

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

通過base64加密後得到了

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

playload
包含標準的公開的生命和私有的宣告,不建議定義敏感資訊,因為該資訊是可以被解密的

部分公開的宣告

  • iss: jwt簽發者
  • aud: 接收jwt的一方
  • exp: jwt的過期時間,這個過期時間必須要大於簽發時間

私有的宣告

{
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "wangyingpan",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "2",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone": "122",
  "exp": 1604283731,
  "iss": "MyShopIssuer",
  "aud": "MyShopAudience"
}

通過base64加密後得到了

eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoid2FuZ3lpbmdwYW4iLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjIiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9tb2JpbGVwaG9uZSI6IjEyMiIsImV4cCI6MTYwNDI4MzczMSwiaXNzIjoiTXlTaG9wSXNzdWVyIiwiYXVkIjoiTXlTaG9wQXVkaWVuY2UifQ

signature

signature 由 三部分資訊組成,分別為base64加密後的headerplayload用"."連線起來,通過宣告的演算法加密使用 服務端secret 作為鹽(salt)

三部分通過“.”連線起三部分組成了最終的Token

程式碼實踐

新增使用者服務

1.Domain中定義使用者實體User

 public class User:BaseGuidEntity
    {
        [Required]
        public string Account { get; set; }

        /// <summary>
        /// 名稱
        /// </summary>
        public string NickName { get; set; }

        /// <summary>
        /// 真實名稱
        /// </summary>
        public string RealName { get; set; }

        /// <summary>
        /// 年齡
        /// </summary>
        public int Age { get; set; }

        /// <summary>
        /// 手機號
        /// </summary>
        public string Tel { get; set; }

        /// <summary>
        /// 住址
        /// </summary>
        public string Address { get; set; }

        /// <summary>
        /// 密碼
        /// </summary>
        public string Password { get; set; }

        /// <summary>
        /// 使用者狀態
        /// </summary>
        public UserStatusEnum UserStatus { get; set; }
    }
    public enum UserStatusEnum 
    {
        Registered,//已註冊
        Incompleted, // 未完善資訊
        Completed,//完善資訊
        Locked, // 鎖定
        Deleted // 刪除
    }

2.EntityFrameworkCore 中新增Table

UserCreatingExtension.cs


public static class UserCreatingExtension
    {
        public static void ConfigureUserStore(this ModelBuilder builder)
        {
            Check.NotNull(builder, nameof(builder));
            builder.Entity<User>(option =>
            {
                option.ToTable("User");
                option.ConfigureByConvention();
            });
        }
    }

MyShopDbContext.cs


[ConnectionStringName("Default")]
    public class MyShopDbContext:AbpDbContext<MyShopDbContext>
    {
        public MyShopDbContext(DbContextOptions<MyShopDbContext> options) : base(options) 
        {
            
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.ConfigureProductStore();
            builder.ConfigureOrderStore();
            builder.ConfigureOrderItemStore();
            builder.ConfigureCategoryStore();
            builder.ConfigureBasketAndItemsStore();

            // 配置使用者表
            builder.ConfigureUserStore();
        }

        public DbSet<Product> Products { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderItem> OrderItems { get; set; }
        public DbSet<Category> Categories { get; set; }
        public DbSet<Basket> Basket { get; set; }
        public DbSet<BasketItem> BasketItems { get; set; }

        //新增使用者表
        public DbSet<User> Users { get; set; }
    }

3.遷移生成User表

首先新增使用者表定義


[ConnectionStringName("Default")]
    public class DbMigrationsContext : AbpDbContext<DbMigrationsContext>
    {
        public DbMigrationsContext(DbContextOptions<DbMigrationsContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            builder.ConfigureProductStore();
            builder.ConfigureOrderStore();
            builder.ConfigureOrderItemStore();
            builder.ConfigureCategoryStore();
            builder.ConfigureBasketAndItemsStore();

            // 使用者配置
            builder.ConfigureUserStore();
        }

    }

然後開啟程式包管理控制檯切換為遷移專案MyShop.EntityFrameworkCore.DbMigration並輸入

  • Add-Migration "AddUser"

  • Update-Database

此時User表就已經生成並對應Migration檔案

4.Application中構建UserApplication
Api中新增配置AppSetting.json

"Jwt": {
    "SecurityKey": "1111111111111111111111111111111",
    "Issuer": "MyShopIssuer", 
    "Audience": "MyShopAudience",
    "Expires": 30 // 過期分鐘
  }

IUserApplicationService

namespace MyShop.Application.Contract.User
{
    public interface IUserApplicationService
    {
        Task<BaseResult<TokenInfo>> Register(UserRegisterDto registerInfo, CancellationToken cancellationToken);
        Task<BaseResult<TokenInfo>> Login(UserLoginDto loginInfo);
    }
}

AutoMapper Profiles中新增對映

namespace MyShop.Application.AutoMapper.Profiles
{
    public class MyShopApplicationProfile:Profile
    {
        public MyShopApplicationProfile() 
        {
            CreateMap<Product, ProductItemDto>().ReverseMap();
               
            CreateMap<Order, OrderInfoDto>().ReverseMap();

            CreateMap<Basket, BasketDto>().ReverseMap();
            CreateMap<BasketItem, BasketItemDto>().ReverseMap();

            CreateMap<InsertBasketItemDto, BasketItem>().ReverseMap();

            // 使用者註冊資訊對映
            CreateMap<UserRegisterDto, User>()
                .ForMember(src=>src.UserStatus ,opt=>opt.MapFrom(src=> UserStatusEnum.Registered))
                .ForMember(src=>src.Password , opt=>opt.MapFrom(src=> EncryptHelper.MD5Encrypt(src.Password,string.Empty)));

        }
    }
}

UserApplicationService

namespace MyShop.Application
{
    /// <summary>
    /// 使用者服務
    /// </summary>
    public class UserApplicationService : ApplicationService, IUserApplicationService
    {
        private readonly IConfiguration _configuration;
        private readonly IRepository<User,Guid> _userRepository;

        /// <summary>
        /// 構造
        /// </summary>
        /// <param name="userRepository">使用者倉儲</param>
        /// <param name="configuration">配置資訊</param>
        public UserApplicationService(IRepository<User, Guid> userRepository, IConfiguration configuration) 
        {
            _userRepository = userRepository;
            _configuration = configuration;
        }

        /// <summary>
        /// 登入
        /// </summary>
        /// <param name="loginInfo">登入資訊</param>
        /// <returns></returns>
        public async Task<BaseResult<TokenInfo>> Login(UserLoginDto loginInfo)
        {
            if (string.IsNullOrEmpty(loginInfo.Account) || string.IsNullOrEmpty(loginInfo.Password))
                return BaseResult<TokenInfo>.Failed("使用者名稱密碼不能為空!");

            var user = await Task.FromResult(_userRepository.FirstOrDefault(p => p.Account == loginInfo.Account));
            if (user == null)
            {
                return BaseResult<TokenInfo>.Failed("使用者名稱密碼錯誤!");
            }
            string md5Pwd = EncryptHelper.MD5Encrypt(loginInfo.Password);
            if (user.Password != md5Pwd)
            {
                return BaseResult<TokenInfo>.Failed("使用者名稱密碼錯誤!");
            }

            var claims = GetClaims(user);

            var token = GenerateToken(claims);

            return BaseResult<TokenInfo>.Success(token);
        }

        /// <summary>
        /// 註冊
        /// </summary>
        /// <param name="registerInfo">註冊資訊</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns></returns>
        public async Task<BaseResult<TokenInfo>> Register(UserRegisterDto registerInfo,CancellationToken cancellationToken)
        {
            var user = ObjectMapper.Map<UserRegisterDto, User>(registerInfo);

            var registeredUser = await _userRepository.InsertAsync(user, true, cancellationToken);

            var claims = GetClaims(user);

            var token = GenerateToken(claims);

            return BaseResult<TokenInfo>.Success(token);
        }

        #region Token生成

        private IEnumerable<Claim> GetClaims(User user) 
        {
            var claims = new[]
            {
                new Claim(AbpClaimTypes.UserName,user.NickName),
                new Claim(AbpClaimTypes.UserId,user.Id.ToString()),
                new Claim(AbpClaimTypes.PhoneNumber,user.Tel),
                new Claim(AbpClaimTypes.SurName, user.UserStatus == UserStatusEnum.Completed ?user.RealName:string.Empty)
            };
            return claims;
        }

        /// <summary>
        /// 生成token
        /// </summary>
        /// <param name="claims">宣告</param>
        /// <returns></returns>
        private TokenInfo GenerateToken(IEnumerable<Claim> claims) 
        {
            // 金鑰
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecurityKey"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            // 過期時間
            int expires = string.IsNullOrEmpty(_configuration["Expires"]) ? 30 : Convert.ToInt32(_configuration["Expires"]);
            
            //生成token
            var token = new JwtSecurityToken(
                    issuer: _configuration["Jwt:Issuer"],
                    audience: _configuration["Jwt:Audience"],
                    claims: claims,
                    expires: DateTime.Now.AddMinutes(expires),
                    signingCredentials: creds);

            return new TokenInfo()
            {
                Expire = expires,
                Token = new JwtSecurityTokenHandler().WriteToken(token)
            };
        }

        #endregion
    }
}

啟用認證並新增相關Swagger配置

在需要啟動認證的站點模組中新增以下程式碼(MyShopApiModule)

MyShopApiModule.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.PlatformAbstractions;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using MyShop.Admin.Application;
using MyShop.Admin.Application.Services;
using MyShop.Api.Middleware;
using MyShop.Application;
using MyShop.Application.Contract.Order;
using MyShop.Application.Contract.Product;
using MyShop.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.AspNetCore;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
using Volo.Abp.Autofac;
using Volo.Abp.Modularity;

namespace MyShop.Api
{

    // 注意是依賴於AspNetCoreMvc 而不是 AspNetCore
    [DependsOn(typeof(AbpAspNetCoreMvcModule),typeof(AbpAutofacModule))]
    [DependsOn(typeof(MyShopApplicationModule),typeof(MyShopEntityFrameworkCoreModule),typeof(MyShopAdminApplicationModule))]
    public class MyShopApiModule :AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var service = context.Services;

            // 配置jwt
            ConfigureJwt(service);

            // 配置跨域
            ConfigureCors(service);

            // 配置swagger
            ConfigureSwagger(service);

            service.Configure((AbpAspNetCoreMvcOptions options) =>
            {
                options.ConventionalControllers.Create(typeof(Application.ProductApplicationService).Assembly);
                options.ConventionalControllers.Create(typeof(Application.OrderApplicationService).Assembly);
                options.ConventionalControllers.Create(typeof(Application.UserApplicationService).Assembly);
                options.ConventionalControllers.Create(typeof(Application.BasketApplicationService).Assembly);
                options.ConventionalControllers.Create(typeof(Admin.Application.Services.ProductApplicationService).Assembly, options =>
                {
                    options.RootPath = "admin";
                });
            });

        }

        public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {
            var env = context.GetEnvironment();
            var app = context.GetApplicationBuilder();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // 跨域
            app.UseCors("AllowAll");

            // swagger
            app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "MyShopApi");
            });

            app.UseRouting();

            //新增jwt驗證 注意:必須先新增認證再新增授權中介軟體,且必須新增在UseRouting 和UseEndpoints之間
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseConfiguredEndpoints();
        }

        #region ServicesConfigure

        private void ConfigureJwt(IServiceCollection services) 
        {
            var configuration = services.GetConfiguration();
            services
                .AddAuthentication(options=> 
                {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(options=> 
                {
                    options.RequireHttpsMetadata = false;// 開發環境為false

                    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                    {
                        ValidateIssuer = true,//是否驗證Issuer
                        ValidateAudience = true,//是否驗證Audience
                        ValidateLifetime = true,//是否驗證失效時間
                        ClockSkew = TimeSpan.FromSeconds(30), // 偏移時間,所以實際過期時間 = 給定過期時間+偏移時間
                        ValidateIssuerSigningKey = true,//是否驗證SecurityKey
                        ValidAudience = configuration["Jwt:Audience"],//Audience
                        ValidIssuer = configuration["Jwt:Issuer"],//Issuer,這兩項和前面簽發jwt的設定一致
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecurityKey"]))//拿到SecurityKey
                    };

                    // 事件
                    options.Events = new JwtBearerEvents()
                    {
                        OnAuthenticationFailed = context => 
                        {
                            return Task.CompletedTask;
                        },
                        OnChallenge = context => 
                        {
                            // 驗證失敗
                            BaseResult<object> result = new BaseResult<object>(ResponseResultCode.Unauthorized,"未授權",null);
                            context.HandleResponse();
                            context.Response.ContentType = "application/json;utf-8";

                            context.Response.StatusCode = StatusCodes.Status401Unauthorized;

                            await context.Response.WriteAsync(JsonConvert.SerializeObject(result), Encoding.UTF8);
                        },
                        OnForbidden = context => 
                        {
                            return Task.CompletedTask;
                        },
                        OnMessageReceived = context =>
                        {
                            return Task.CompletedTask;
                        }
                    };
                });
        }

        private void ConfigureCors(IServiceCollection services) 
        {
            services.AddCors(options =>
            {
                options.AddPolicy("AllowAll", builder =>
                {
                    builder.AllowAnyOrigin()
                        .SetIsOriginAllowedToAllowWildcardSubdomains()
                        .AllowAnyHeader()
                        .AllowAnyMethod();
                });
            });
        }

        private void ConfigureSwagger(IServiceCollection services)
        {
            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo()
                {
                    Title = "MyShopApi",
                    Version = "v0.1"
                });
                options.DocInclusionPredicate((docName, predicate) => true);
                options.CustomSchemaIds(type => type.FullName);

                var basePath = PlatformServices.Default.Application.ApplicationBasePath;
                options.IncludeXmlComments(Path.Combine(basePath, "MyShop.Application.xml"));
                options.IncludeXmlComments(Path.Combine(basePath, "MyShop.Application.Contract.xml"));

                #region 新增請求認證

                //Bearer 的scheme定義
                var securityScheme = new OpenApiSecurityScheme()
                {
                    Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
                    Name = "Authorization",
                    //引數新增在頭部
                    In = ParameterLocation.Header,
                    //使用Authorize頭部
                    Type = SecuritySchemeType.Http,
                    //內容為以 bearer開頭
                    Scheme = "Bearer",
                    BearerFormat = "JWT"
                };

                //把所有方法配置為增加bearer頭部資訊
                var securityRequirement = new OpenApiSecurityRequirement
                    {
                        {
                                new OpenApiSecurityScheme
                                {
                                    Reference = new OpenApiReference
                                    {
                                        Type = ReferenceType.SecurityScheme,
                                        Id = "MyShopApi"
                                    }
                                },
                                new string[] {}
                        }
                    };

                options.AddSecurityDefinition("MyShopApi", securityScheme);
                options.AddSecurityRequirement(securityRequirement);

                #endregion

            });
        }

        #endregion
    }
}


統一響應格式

基礎 BaseResult

定義全體響應型別父類,並提供基礎響應成功及響應失敗結果建立靜態函式

    /// <summary>
    /// 基礎響應資訊
    /// </summary>
    /// <typeparam name="T">響應資料型別</typeparam>
    public class BaseResult<T> where T:class
    {
        /// <summary>
        /// 響應碼
        /// </summary>
        public ResponseResultCode Code { get; set; }

        /// <summary>
        /// 響應訊息
        /// </summary>
        public string Message { get; set; }

        /// <summary>
        /// 響應資料
        /// </summary>
        public virtual T Data { get; set; }

        /// <summary>
        /// 響應成功資訊
        /// </summary>
        /// <param name="data">響應資料</param>
        /// <returns></returns>
        public static BaseResult<T> Success(T data,string message = "請求成功") => new BaseResult<T>(ResponseResultCode.Success,message, data);

        /// <summary>
        /// 響應失敗資訊
        /// </summary>
        /// <param name="message">響應資訊</param>
        /// <returns></returns>
        public static BaseResult<T> Failed(string message = "請求失敗!") 
            => new BaseResult<T> (ResponseResultCode.Failed,message,null);

        /// <summary>
        /// 響應異常資訊
        /// </summary>
        /// <param name="message">響應資訊</param>
        /// <returns></returns>
        public static BaseResult<T> Error(string message = "請求失敗!")
            => new BaseResult<T>(ResponseResultCode.Error, message, null);

        /// <summary>
        /// 構造響應資訊
        /// </summary>
        /// <param name="code">響應碼</param>
        /// <param name="message">響應訊息</param>
        /// <param name="data">響應資料</param>
        public BaseResult(ResponseResultCode code,string message,T data) 
        {
            this.Code = code;
            this.Message = message;
            this.Data = data;
        }
    }

    public enum ResponseResultCode 
    {
        Success = 200,
        Failed = 400,
        Unauthorized = 401,
        Error = 500
    }
列表 ListResult

派生自BaseResult並新增泛型為IEnumerable

 /// <summary>
    /// 列表響應
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class ListResult<T> : BaseResult<IEnumerable<T>> where T : class
    {
        public ListResult(ResponseResultCode code, string message, IEnumerable<T> data) 
        : base(code, message, data)
        {
        }
    }
分頁列表 PagedResult

派生自BaseResult並新增PageData分頁資料型別泛型

public class PagedResult<T> : BaseResult<PageData<T>>
    {
        public PagedResult(ResponseResultCode code, string message, PageData<T> data) : base(code, message, data)
        {
            
        }

        /// <summary>
        /// 響應成功資訊
        /// </summary>
        /// <param name="total">資料總條數</param>
        /// <param name="list">分頁列表資訊</param>
        /// <returns></returns>
        public static PagedResult<T> Success(int total,IEnumerable<T> list,string message= "請求成功") 
            => new PagedResult<T> (ResponseResultCode.Success,message,new PageData<T> (total,list));

    }

    /// <summary>
    /// 分頁資料
    /// </summary>
    /// <typeparam name="T">資料型別</typeparam>
    public class PageData<T> 
    {

        /// <summary>
        /// 構造
        /// </summary>
        /// <param name="total">資料總條數</param>
        /// <param name="list">資料集合</param>
        public PageData(int total,IEnumerable<T> list) 
        {
            this.Total = total;
            this.Data = list;
        }

        /// <summary>
        /// 資料總條數
        /// </summary>
        public int Total { get; set; }

        /// <summary>
        /// 資料集合
        /// </summary>
        public IEnumerable<T> Data { get; set; }
    }

自定義異常

在我們新增自定義異常時需要先將abp vNext 預設提供的全域性異常過濾器移除。
在Module的ConfigureServices中新增移除程式碼


    // 移除Abp異常過濾器
    Configure<MvcOptions>(options =>
    {
        var index = options.Filters.ToList().FindIndex(filter => filter is ServiceFilterAttribute attr && attr.ServiceType.Equals(typeof(AbpExceptionFilter)));

        if (index > -1)
            options.Filters.RemoveAt(index);
    });

定義MyShop自定義異常中介軟體


/// <summary>
    /// MyShop異常中介軟體
    /// </summary>
    public class MyShopExceptionMiddleware 
    {
        private readonly RequestDelegate _next;
        public MyShopExceptionMiddleware(RequestDelegate next) 
        {
            _next = next;
        }
        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception ex)
            {
                await HandleException(context, ex);
            }
            finally 
            {
                await HandleException(context);
            }
        }

        private async Task HandleException(HttpContext context, Exception ex = null) 
        {

            BaseResult<object> result = null; ;

            bool handle = true;
            if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
            {
                result = new BaseResult<object>(ResponseResultCode.Unauthorized, "未授權!", null);
            }
            else if (context.Response.StatusCode == (int)HttpStatusCode.InternalServerError)
            {
                result = new BaseResult<object>(ResponseResultCode.Error, "服務繁忙!", null);
            }
            else 
            {
                handle = false;
            }

            if(handle) await context.Response.WriteAsync(JsonConvert.SerializeObject(result), Encoding.UTF8);
        }
    }

為了方便通過IApplicationBuilder呼叫這裡我們新增個擴充套件方法用於方便新增我們的自定義異常中介軟體


    public static class MiddlewareExtensions
    {

        /// <summary>
        /// MyShop異常中介軟體
        /// </summary>
        /// <param name="app"></param>
        /// <returns></returns>
        public static IApplicationBuilder UseMyShopExceptionMiddleware(this IApplicationBuilder app) 
        {
            app.UseMiddleware<MyShopExceptionMiddleware>();
            return app;
        }
    }

最後在 Module類中 新增對應的中介軟體


app.UseMyShopExceptionMiddleware();

程式執行

直接訪問Order列表

由於我們的Order服務帶有[Authrorize]特性

所以結果直接顯示為 未授權

訪問登入介面獲取token

通過註冊介面註冊一個使用者用於登入
使用wyp登入

使用token訪問Order列表

將獲取到的token新增到 請求頭中的Authoritarian中 格式為:Bearer {token},這裡我們通過swagger 配置了所以直接可以使用它的小綠鎖

隨後訪問之前的Order介面
顯示請求成功

手動丟擲異常顯示自定義異常響應

訪問一個未實現的介面服務

正常顯示了我們的全域性異常資訊

相關文章