如何新建项目我们在此直接跳过。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来讲解如何配合后台完成登录验证和数据请求。