ASP.NET Core Web API 依赖注入及JWT认证配置

如何新建项目我们在此直接跳过。Target framework我选的是.NET6.0,但是starpup我还是沿用了老的书写方式,当然采用新的书写方式代码会更精简,都是可以的。

前端采用React开发,请参阅:React中利用axios进行Jwt登录认证

program.cs代码简单明了:

public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup()
                    .UseDefaultServiceProvider(options =>
                    {
                    }); 
                });

    }

startup.cs代码有点多:

 public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;

            var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, true)
                .AddEnvironmentVariables();

            Configuration = builder.Build();
        }

        public IConfiguration Configuration { get; set; }
        
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                // 采用的是Jwt认证方式,必要的配置项在appsettings.json中设置
                // Jwt认证的配置项比较多,此处只做了最基本的。有需要的话可以查询相关文档
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.Zero,
                        ValidateIssuerSigningKey = false,
                        ValidAudience = Configuration.GetSection("JwtSettings")["Audience"],
                        ValidIssuer = Configuration.GetSection("JwtSettings")["Issuer"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.
                                    GetBytes(Configuration.GetSection("JwtSettings")["SecretKey"])),
                    };

                    options.Events = new JwtBearerEvents()
                    {
                        OnAuthenticationFailed = (context) =>
                        {
                            var x = context.Exception;
                            if (context.Exception?.GetType() == typeof(SecurityTokenExpiredException))
                            {
                            }
                            return Task.CompletedTask;
                        }
                        ,
                        OnTokenValidated = (context) =>
                        {
                            // UserToken是在Jwt的Playload中传递的,请查看下文login方法中的代码;
                            // UserToken与用户表的主键UserId是一一对应关系,作用类似于UserId
                            string userToken = context.Principal.Claims.ToList().
                            FirstOrDefault(m => m.Type == "UserToken").Value;
                            // 之所以需要将userToken添加到Request.Header中,
                            // 是因为在后续的鉴权中间件中需要根据UserToken去读取相应的用户数据
                            context.Request.Headers["userToken"] = userToken;

                            return Task.CompletedTask;
                        },
                        OnForbidden = (context) =>
                        {
                            return Task.CompletedTask;
                        },

                        OnChallenge = (context) =>
                        {
                            return Task.CompletedTask;
                        }
                    };
                });

            // 指定Controller中Action返回结果时的序列化方式
            // 系统默认额序列化方式和NewtonsoftJson有一些差别,
            // 考虑到更容易和前端用的React协调,此处采用NewtonsoftJson
            services.AddControllers().AddNewtonsoftJson(options =>
            {                
                options.SerializerSettings.ContractResolver = new DefaultContractResolver();
            });
            
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
                {
                    Version = "v1",
                    Title = "Swagger Web API",
                    Description = "Swagger Web API",
                });
            });

            // 用户鉴权时因为要调用数据库数据,此处采用MemoryCache进行缓存
            // 如果不在此处设置,也可以在下方的ConfigureDependencyInjection方法中
            // 利用以下代码进行注入,效果一样:services.AddSingleton()
            services.AddMemoryCache();

            ConfigureDependencyInjection(services);
        }
        
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {            
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger(options =>
                {
                    options.SerializeAsV2 = true;
                });
                app.UseSwaggerUI(options =>
                {                
                    options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
                    options.RoutePrefix = "doc";
                });
            }
            else
            {
                app.UseMiddleware();
            }

            // app.UseHttpsRedirection();

            // 以下use的顺序不能颠倒
            app.UseRouting();       
            app.UseAuthentication();
            app.UseAuthorization();

            // PermissionHandler用户鉴权,即用户是否有权限访问某个页面或资源
            // 这个动作是在Authentication之后,因为鉴权时需要访问Jwt中包含的UserToken
            app.UseMiddleware();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
        public void ConfigureDependencyInjection(IServiceCollection services)
        {
            // 为什么需要这些注入,我们会在后续相关代码中解释
            // 为了统一Log记录,BLL或DAL各个项目中都将需要用到Configuration中的相关配置
            services.AddSingleton(Configuration);
            services.AddScoped();
            services.AddScoped();
            services.AddScoped();
            services.AddScoped();            
            services.AddSingleton(NLog.Web.NLogBuilder.ConfigureNLog("NLog.config").
                GetCurrentClassLogger());
        }
    }

依赖注入会在Controller的构造函数中用到,比如UserController中:

   public class UserController : BaseController
    {
        IUserManager userManager;
        ISystemManager systemManager;
        public UserController( IConfiguration configuration, ILogger logger, IUserManager userManager,
            ISystemManager systemManager, ICommonManager commonManager, IMemoryCache memoryCache) 
            : base(configuration, logger,commonManager, memoryCache)
        {
            this.userManager = userManager;
            this.systemManager = systemManager;
        }

所有的Controller都继承自BaseController。在BaseController中包含了依赖注入和用户登录认证的逻辑:

    [ApiController]
    public class BaseController :Controller
    {
        protected ICommonManager commonManager;
        protected IConfiguration configuration;
        protected ILogger logger;
        protected IMemoryCache memoryCache;
        protected int companyId;
        protected int pageSize;
    
        public BaseController(IConfiguration configuration,ILogger logger, 
            ICommonManager commonManager, IMemoryCache memoryCache)
        {
            this.configuration = configuration;
            this.logger = logger;
            this.pageSize =int.Parse(configuration.GetSection("AppSettings")["PageSize"]);
            this.commonManager = commonManager;
            this.memoryCache = memoryCache;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            // 此项目计划用到多租户的SaaS系统,所以此处会获取用户的companyId;请忽略
            companyId = 0;

            //Headers["userToken"]是在Jwt验证通过以后添加的,此处会被获取
            string userToken = Request?.Headers?["userToken"];
            
            if (!string.IsNullOrWhiteSpace(userToken))
            {
                // 正常登录用户每个请求都会执行用户信息读取,所以此处用到了MemoryCache;可忽略
                data.User user = memoryCache.Get(MemoryCacheKeys.UserTokenKeyPre + userToken);

                if (user == null)
                {
                    user = commonManager.GetUserByToken(userToken);
                    if (user != null)
                    {
                        memoryCache.Set(MemoryCacheKeys.UserTokenKeyPre + user.UserToken, user,
                       new MemoryCacheEntryOptions { 
                           AbsoluteExpiration = DateTime.Now.AddMinutes(int.Parse(configuration.GetSection
                           ("AppSettings")["MemoryCacheExpiredInMinute"])) });
                        companyId = user.CompanyId;
                    }     
                    else
                    {
                        // 如果通过userToken找不到用户,可能是因为用户已经被删除,或者userToken值被更改;
                        // 另外,此时需要判断Action是否允许匿名访问,以防止将login的合法请求拒绝
                        ControllerActionDescriptor descriptor = 
                            context.ActionDescriptor as ControllerActionDescriptor;

                        // 需要using System.Reflection,否则无法访问GetCustomAttributes方法
                        if (!descriptor.MethodInfo.GetCustomAttributes().
                            Any())
                        {
                            context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized);
                            return;
                        }                            
                    }
                }
                else
                {
                    companyId = user.CompanyId;
                }
            }
        }
    }

用户登录及Jwt相关代码:

(其中的一些JsonMessage这些类,都是自己定义的,可以自行调整)

        [HttpPost]
        [Route("authorize/login")]
        //AllowAnonymous必须要写,因为后续鉴权中间件中会假定前端请求中带有必要的UserToken
        [AllowAnonymous]
        public async Task Login(User loginUser)
        {
            string msgLoginFailed = "错误的用户名或密码";

            if (string.IsNullOrWhiteSpace(loginUser.LoginId) ||
                string.IsNullOrWhiteSpace(loginUser.Password))
            {                
                return new JsonMessage() { code = JsonResponseCode.Failed, message = msgLoginFailed };
            }
            
            User user=  await userManager.GetUserByLoginId(loginUser.LoginId);
            if (user == null)
            {
                return new JsonMessage() {code= JsonResponseCode.Failed,message= msgLoginFailed };
            }           
            
            using (var md5 = MD5.Create())
            {                
                if (BitConverter.ToString(md5.ComputeHash(
                    Encoding.UTF8.GetBytes(loginUser.Password)))==user.Password)
                {
                    // UserToken会作为Jwt的playload,后续做验证时会被解出来识别用户身份
                    var claims = new Claim[] {
                        new Claim("UserToken", user.UserToken),
                        new Claim("UserName", user.UserName)                        
                    };
                    
                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
                        configuration.GetSection("JwtSettings")["SecretKey"]));
                    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                    var token = new JwtSecurityToken(
                        configuration.GetSection("JwtSettings")["Issuer"],
                        configuration.GetSection("JwtSettings")["Audience"],
                        claims,
                        DateTime.Now,
                        DateTime.Now.AddYears(1),// 过期的逻辑暂时没写
                        creds);                    

                    return new JsonMessage { code= JsonResponseCode.Success,
                        data= new JwtSecurityTokenHandler().WriteToken(token),
                        addition=new User { LoginId=user.LoginId,UserName=user.UserName,
                            CompanyName=user.CompanyName,PermissionIds=user.PermissionIds} };
                }
                else
                {
                    return new JsonMessage() { code = JsonResponseCode.Failed, message = msgLoginFailed };
                }                
            }             
        }

以上代码比较多,看起来也会比较乱,还有很多模块的代码没法都贴上去。这个示例只是用于演示最基本的Jwt和依赖注入。后续我会再写一篇React来讲解如何配合后台完成登录验证和数据请求。

你可能感兴趣的:(asp.net,core,asp.net,core)