[toc]
https://github.com/yingpanwang/MyShop/tree/dev_jwt 此文对应分支 dev_jwt
上一篇文章中,我们使用Abp vNext构建了一个可以运行的简单的API,但是整个站点没有一个途径去对我们的API访问有限制,导致API完全是裸露在外的,如果要运行正常的商业API是完全不可行的,所以接下来我们会通过使用JWT(Json Web Toekn)的方式实现对API的访问数据限制。
现在API一般是分布式且要求是无状态的,所以传统的Session无法使用,JWT其实类似于早期API基于Cookie自定义用户认证的形式,只是JWT的设计更为紧凑和易于扩展开放,使用方式更加便利基于JWT的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。
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 : 名称
- http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: 标识
- http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone: 电话
{"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加密后的header,playload用"."连接起来,通过声明的算法加密使用 服务端secret 作为盐(salt)
三部分通过“.”连接起三部分组成了最终的Token
publicclassUser:BaseGuidEntity{[Required]publicstringAccount{get;set;}/// <summary>/// 名称/// </summary>publicstringNickName{get;set;}/// <summary>/// 真实名称/// </summary>publicstringRealName{get;set;}/// <summary>/// 年龄/// </summary>publicintAge{get;set;}/// <summary>/// 手机号/// </summary>publicstringTel{get;set;}/// <summary>/// 住址/// </summary>publicstringAddress{get;set;}/// <summary>/// 密码/// </summary>publicstringPassword{get;set;}/// <summary>/// 用户状态/// </summary>publicUserStatusEnumUserStatus{get;set;}}publicenumUserStatusEnum{Registered,//已注册Incompleted,// 未完善信息Completed,//完善信息Locked,// 锁定Deleted// 删除}UserCreatingExtension.cs
publicstaticclassUserCreatingExtension{publicstaticvoidConfigureUserStore(thisModelBuilderbuilder){Check.NotNull(builder,nameof(builder));builder.Entity<User>(option =>{option.ToTable("User");option.ConfigureByConvention();});}}MyShopDbContext.cs
[ConnectionStringName("Default")]publicclassMyShopDbContext:AbpDbContext<MyShopDbContext>{publicMyShopDbContext(DbContextOptions<MyShopDbContext>options):base(options){}protectedoverridevoidOnModelCreating(ModelBuilderbuilder){builder.ConfigureProductStore();builder.ConfigureOrderStore();builder.ConfigureOrderItemStore();builder.ConfigureCategoryStore();builder.ConfigureBasketAndItemsStore();// 配置用户表builder.ConfigureUserStore();}publicDbSet<Product>Products{get;set;}publicDbSet<Order>Orders{get;set;}publicDbSet<OrderItem>OrderItems{get;set;}publicDbSet<Category>Categories{get;set;}publicDbSet<Basket>Basket{get;set;}publicDbSet<BasketItem>BasketItems{get;set;}//添加用户表publicDbSet<User>Users{get;set;}}首先添加用户表定义
[ConnectionStringName("Default")]publicclassDbMigrationsContext:AbpDbContext<DbMigrationsContext>{publicDbMigrationsContext(DbContextOptions<DbMigrationsContext>options):base(options){}protectedoverridevoidOnModelCreating(ModelBuilderbuilder){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文件
"Jwt":{"SecurityKey":"1111111111111111111111111111111","Issuer":"MyShopIssuer","Audience":"MyShopAudience","Expires":30// 过期分钟}namespaceMyShop.Application.Contract.User{publicinterfaceIUserApplicationService{Task<BaseResult<TokenInfo>>Register(UserRegisterDtoregisterInfo,CancellationTokencancellationToken);Task<BaseResult<TokenInfo>>Login(UserLoginDtologinInfo);}}namespaceMyShop.Application.AutoMapper.Profiles{publicclassMyShopApplicationProfile:Profile{publicMyShopApplicationProfile(){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)));}}}namespaceMyShop.Application{/// <summary>/// 用户服务/// </summary>publicclassUserApplicationService:ApplicationService,IUserApplicationService{privatereadonlyIConfiguration_configuration;privatereadonlyIRepository<User,Guid>_userRepository;/// <summary>/// 构造/// </summary>/// <param name="userRepository">用户仓储</param>/// <param name="configuration">配置信息</param>publicUserApplicationService(IRepository<User,Guid>userRepository,IConfigurationconfiguration){_userRepository=userRepository;_configuration=configuration;}/// <summary>/// 登录/// </summary>/// <param name="loginInfo">登录信息</param>/// <returns></returns>publicasyncTask<BaseResult<TokenInfo>>Login(UserLoginDtologinInfo){if(string.IsNullOrEmpty(loginInfo.Account)||string.IsNullOrEmpty(loginInfo.Password))returnBaseResult<TokenInfo>.Failed("用户名密码不能为空!");varuser=awaitTask.FromResult(_userRepository.FirstOrDefault(p =>p.Account==loginInfo.Account));if(user==null){returnBaseResult<TokenInfo>.Failed("用户名密码错误!");}stringmd5Pwd=EncryptHelper.MD5Encrypt(loginInfo.Password);if(user.Password!=md5Pwd){returnBaseResult<TokenInfo>.Failed("用户名密码错误!");}varclaims=GetClaims(user);vartoken=GenerateToken(claims);returnBaseResult<TokenInfo>.Success(token);}/// <summary>/// 注册/// </summary>/// <param name="registerInfo">注册信息</param>/// <param name="cancellationToken">取消令牌</param>/// <returns></returns>publicasyncTask<BaseResult<TokenInfo>>Register(UserRegisterDtoregisterInfo,CancellationTokencancellationToken){varuser=ObjectMapper.Map<UserRegisterDto,User>(registerInfo);varregisteredUser=await_userRepository.InsertAsync(user,true,cancellationToken);varclaims=GetClaims(user);vartoken=GenerateToken(claims);returnBaseResult<TokenInfo>.Success(token);} #region Token生成 privateIEnumerable<Claim>GetClaims(Useruser){varclaims=new[]{newClaim(AbpClaimTypes.UserName,user.NickName),newClaim(AbpClaimTypes.UserId,user.Id.ToString()),newClaim(AbpClaimTypes.PhoneNumber,user.Tel),newClaim(AbpClaimTypes.SurName,user.UserStatus==UserStatusEnum.Completed?user.RealName:string.Empty)};returnclaims;}/// <summary>/// 生成token/// </summary>/// <param name="claims">声明</param>/// <returns></returns>privateTokenInfoGenerateToken(IEnumerable<Claim>claims){// 密钥varkey=newSymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecurityKey"]));varcreds=newSigningCredentials(key,SecurityAlgorithms.HmacSha256);// 过期时间intexpires=string.IsNullOrEmpty(_configuration["Expires"])?30:Convert.ToInt32(_configuration["Expires"]);//生成tokenvartoken=newJwtSecurityToken(issuer:_configuration["Jwt:Issuer"],audience:_configuration["Jwt:Audience"],claims:claims,expires:DateTime.Now.AddMinutes(expires),signingCredentials:creds);returnnewTokenInfo(){Expire=expires,Token=newJwtSecurityTokenHandler().WriteToken(token)};} #endregion }}在需要启动认证的站点模块中添加以下代码(MyShopApiModule)
usingMicrosoft.AspNetCore.Authentication.JwtBearer;usingMicrosoft.AspNetCore.Builder;usingMicrosoft.AspNetCore.Mvc;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Hosting;usingMicrosoft.Extensions.PlatformAbstractions;usingMicrosoft.IdentityModel.Tokens;usingMicrosoft.OpenApi.Models;usingMyShop.Admin.Application;usingMyShop.Admin.Application.Services;usingMyShop.Api.Middleware;usingMyShop.Application;usingMyShop.Application.Contract.Order;usingMyShop.Application.Contract.Product;usingMyShop.EntityFrameworkCore;usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;usingVolo.Abp;usingVolo.Abp.AspNetCore;usingVolo.Abp.AspNetCore.Mvc;usingVolo.Abp.AspNetCore.Mvc.Conventions;usingVolo.Abp.AspNetCore.Mvc.ExceptionHandling;usingVolo.Abp.Autofac;usingVolo.Abp.Modularity;namespaceMyShop.Api{// 注意是依赖于AspNetCoreMvc 而不是 AspNetCore[DependsOn(typeof(AbpAspNetCoreMvcModule),typeof(AbpAutofacModule))][DependsOn(typeof(MyShopApplicationModule),typeof(MyShopEntityFrameworkCoreModule),typeof(MyShopAdminApplicationModule))]publicclassMyShopApiModule:AbpModule{publicoverridevoidConfigureServices(ServiceConfigurationContextcontext){varservice=context.Services;// 配置jwtConfigureJwt(service);// 配置跨域ConfigureCors(service);// 配置swaggerConfigureSwagger(service);service.Configure((AbpAspNetCoreMvcOptionsoptions)=>{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";});});}publicoverridevoidOnApplicationInitialization(ApplicationInitializationContextcontext){varenv=context.GetEnvironment();varapp=context.GetApplicationBuilder();if(env.IsDevelopment()){app.UseDeveloperExceptionPage();}// 跨域app.UseCors("AllowAll");// swaggerapp.UseSwagger();app.UseSwaggerUI(options =>{options.SwaggerEndpoint("/swagger/v1/swagger.json","MyShopApi");});app.UseRouting();//添加jwt验证 注意:必须先添加认证再添加授权中间件,且必须添加在UseRouting 和UseEndpoints之间app.UseAuthentication();app.UseAuthorization();app.UseConfiguredEndpoints();} #region ServicesConfigure privatevoidConfigureJwt(IServiceCollectionservices){varconfiguration=services.GetConfiguration();services.AddAuthentication(options=>{options.DefaultAuthenticateScheme=JwtBearerDefaults.AuthenticationScheme;options.DefaultChallengeScheme=JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(options=>{options.RequireHttpsMetadata=false;// 开发环境为falseoptions.TokenValidationParameters=newMicrosoft.IdentityModel.Tokens.TokenValidationParameters(){ValidateIssuer=true,//是否验证IssuerValidateAudience=true,//是否验证AudienceValidateLifetime=true,//是否验证失效时间ClockSkew=TimeSpan.FromSeconds(30),// 偏移时间,所以实际过期时间 = 给定过期时间+偏移时间ValidateIssuerSigningKey=true,//是否验证SecurityKeyValidAudience=configuration["Jwt:Audience"],//AudienceValidIssuer=configuration["Jwt:Issuer"],//Issuer,这两项和前面签发jwt的设置一致IssuerSigningKey=newSymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecurityKey"]))//拿到SecurityKey};// 事件options.Events=newJwtBearerEvents(){OnAuthenticationFailed= context =>{returnTask.CompletedTask;},OnChallenge= context =>{// 验证失败BaseResult<object>result=newBaseResult<object>(ResponseResultCode.Unauthorized,"未授权",null);context.HandleResponse();context.Response.ContentType="application/json;utf-8";context.Response.StatusCode=StatusCodes.Status401Unauthorized;awaitcontext.Response.WriteAsync(JsonConvert.SerializeObject(result),Encoding.UTF8);},OnForbidden= context =>{returnTask.CompletedTask;},OnMessageReceived= context =>{returnTask.CompletedTask;}};});}privatevoidConfigureCors(IServiceCollectionservices){services.AddCors(options =>{options.AddPolicy("AllowAll", builder =>{builder.AllowAnyOrigin().SetIsOriginAllowedToAllowWildcardSubdomains().AllowAnyHeader().AllowAnyMethod();});});}privatevoidConfigureSwagger(IServiceCollectionservices){services.AddSwaggerGen(options =>{options.SwaggerDoc("v1",newMicrosoft.OpenApi.Models.OpenApiInfo(){Title="MyShopApi",Version="v0.1"});options.DocInclusionPredicate((docName,predicate)=>true);options.CustomSchemaIds(type =>type.FullName);varbasePath=PlatformServices.Default.Application.ApplicationBasePath;options.IncludeXmlComments(Path.Combine(basePath,"MyShop.Application.xml"));options.IncludeXmlComments(Path.Combine(basePath,"MyShop.Application.Contract.xml")); #region 添加请求认证 //Bearer 的scheme定义varsecurityScheme=newOpenApiSecurityScheme(){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头部信息varsecurityRequirement=newOpenApiSecurityRequirement{{newOpenApiSecurityScheme{Reference=newOpenApiReference{Type=ReferenceType.SecurityScheme,Id="MyShopApi"}},newstring[]{}}};options.AddSecurityDefinition("MyShopApi",securityScheme);options.AddSecurityRequirement(securityRequirement); #endregion });} #endregion }}定义全体响应类型父类,并提供基础响应成功及响应失败结果创建静态函数
/// <summary>/// 基础响应信息/// </summary>/// <typeparam name="T">响应数据类型</typeparam>publicclassBaseResult<T>whereT:class{/// <summary>/// 响应码/// </summary>publicResponseResultCodeCode{get;set;}/// <summary>/// 响应消息/// </summary>publicstringMessage{get;set;}/// <summary>/// 响应数据/// </summary>publicvirtualTData{get;set;}/// <summary>/// 响应成功信息/// </summary>/// <param name="data">响应数据</param>/// <returns></returns>publicstaticBaseResult<T>Success(Tdata,stringmessage="请求成功")=>newBaseResult<T>(ResponseResultCode.Success,message,data);/// <summary>/// 响应失败信息/// </summary>/// <param name="message">响应信息</param>/// <returns></returns>publicstaticBaseResult<T>Failed(stringmessage="请求失败!")=>newBaseResult<T>(ResponseResultCode.Failed,message,null);/// <summary>/// 响应异常信息/// </summary>/// <param name="message">响应信息</param>/// <returns></returns>publicstaticBaseResult<T>Error(stringmessage="请求失败!")=>newBaseResult<T>(ResponseResultCode.Error,message,null);/// <summary>/// 构造响应信息/// </summary>/// <param name="code">响应码</param>/// <param name="message">响应消息</param>/// <param name="data">响应数据</param>publicBaseResult(ResponseResultCodecode,stringmessage,Tdata){this.Code=code;this.Message=message;this.Data=data;}}publicenumResponseResultCode{Success=200,Failed=400,Unauthorized=401,Error=500}派生自BaseResult并添加泛型为IEnumerable
/// <summary>/// 列表响应/// </summary>/// <typeparam name="T"></typeparam>publicclassListResult<T>:BaseResult<IEnumerable<T>>whereT:class{publicListResult(ResponseResultCodecode,stringmessage,IEnumerable<T>data):base(code,message,data){}}派生自BaseResult并添加PageData分页数据类型泛型
publicclassPagedResult<T>:BaseResult<PageData<T>>{publicPagedResult(ResponseResultCodecode,stringmessage,PageData<T>data):base(code,message,data){}/// <summary>/// 响应成功信息/// </summary>/// <param name="total">数据总条数</param>/// <param name="list">分页列表信息</param>/// <returns></returns>publicstaticPagedResult<T>Success(inttotal,IEnumerable<T>list,stringmessage="请求成功")=>newPagedResult<T>(ResponseResultCode.Success,message,newPageData<T>(total,list));}/// <summary>/// 分页数据/// </summary>/// <typeparam name="T">数据类型</typeparam>publicclassPageData<T>{/// <summary>/// 构造/// </summary>/// <param name="total">数据总条数</param>/// <param name="list">数据集合</param>publicPageData(inttotal,IEnumerable<T>list){this.Total=total;this.Data=list;}/// <summary>/// 数据总条数/// </summary>publicintTotal{get;set;}/// <summary>/// 数据集合/// </summary>publicIEnumerable<T>Data{get;set;}}在我们添加自定义异常时需要先将abp vNext 默认提供的全局异常过滤器移除。 在Module的ConfigureServices中添加移除代码
// 移除Abp异常过滤器Configure<MvcOptions>(options =>{varindex=options.Filters.ToList().FindIndex(filter =>filterisServiceFilterAttributeattr&&attr.ServiceType.Equals(typeof(AbpExceptionFilter)));if(index>-1)options.Filters.RemoveAt(index);});定义MyShop自定义异常中间件
/// <summary>/// MyShop异常中间件/// </summary>publicclassMyShopExceptionMiddleware{privatereadonlyRequestDelegate_next;publicMyShopExceptionMiddleware(RequestDelegatenext){_next=next;}publicasyncTaskInvokeAsync(HttpContextcontext){try{await_next(context);}catch(Exceptionex){awaitHandleException(context,ex);}finally{awaitHandleException(context);}}privateasyncTaskHandleException(HttpContextcontext,Exceptionex=null){BaseResult<object>result=null;;boolhandle=true;if(context.Response.StatusCode==(int)HttpStatusCode.Unauthorized){result=newBaseResult<object>(ResponseResultCode.Unauthorized,"未授权!",null);}elseif(context.Response.StatusCode==(int)HttpStatusCode.InternalServerError){result=newBaseResult<object>(ResponseResultCode.Error,"服务繁忙!",null);}else{handle=false;}if(handle)awaitcontext.Response.WriteAsync(JsonConvert.SerializeObject(result),Encoding.UTF8);}}为了方便通过IApplicationBuilder调用这里我们添加个扩展方法用于方便添加我们的自定义异常中间件
publicstaticclassMiddlewareExtensions{/// <summary>/// MyShop异常中间件/// </summary>/// <param name="app"></param>/// <returns></returns>publicstaticIApplicationBuilderUseMyShopExceptionMiddleware(thisIApplicationBuilderapp){app.UseMiddleware<MyShopExceptionMiddleware>();returnapp;}}最后在 Module类中 添加对应的中间件
app.UseMyShopExceptionMiddleware();【显示401未授权】