前言
个人记录,仅供参考
一、创建API项目
项目结构截图
1、打开VS,选择创建新项目,选择ASP.NET Core Web API项目
2、采用多层架构,先创建四个文件夹(01应用程序 02业务逻辑 03数据访问 04基础设施
初始化基类状态,遵循继承层次结构的初始化顺序
/// </summary>
/// <param name="connectionString"></param>
public WPFAPIContext(DbContextOptions options):base(options) {
}
#region Sys
public DbSet<UserInfo> UserInfo { get; set; }
#endregion
/// <summary>
/// 重写OnModelCreating,配置模型关系
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserInfo>().HasQueryFilter(x=>!x.IsDeleted);
base.OnModelCreating(modelBuilder);
}
}
3、写好上下文类后,在Program.cs内注册数据库
builder.Services.AddDbContext<WPFAPIContext>(options => {
//从配置文件中获取PostgreSQL数据库的连接字符串
var connectionString = builder.Configuration.GetConnectionString("PostgresSql");
// 使用Npgsql配置DbContextOptions。这里我使用的数据库是PGSql
options.UseNpgsql(connectionString);
//启用懒加载代理(对性能有影响)
options.UseLazyLoadingProxies();
// 启用敏感数据日志记录(在生产环境需谨慎使用)
//options.EnableSensitiveDataLogging();
});
4、配置好以上后,采用EFCore的Model-First模式,创建数据库表。
1、打开程序包管理控制台。输入 create-migration youMigrationName (add-migration)创建一个数据库迁移,名字自定义
2、创建完成后,执行update-database后,即可创建成功数据库。
3、因为上下文继承的是IdentityContext,所以数据库里面会自动创建默认的权限表以及自己的自定义表。可以修改identity的表名字或者自己添加字段。
4、迁移完成后,数据库里面会多一张__EFMigrationsHistory表,表示每次的迁移记录,有Name和时间组成,可以利用这个进行回滚
三、引用AutoFac,对依赖进行管理
1、在基础设施层,创建一个Commons类库,在里面新建一个类AutoFacManage
2、再次添加包Autofac
1、添加好后,AutoFacManage继承Autofac.Module
2、重写autofac管道,实现通过反射批量注册注入
public class AutoFacManage:Autofac.Module
{
/// <summary>
/// 重写autofac管道,实现反射批量注册注入
/// </summary>
/// <param name="builder"></param>
protected override void Load(ContainerBuilder builder)
{
#region 程序集注册依赖注入服务
//获取的Repository和Service需要跟启动项挂钩,就是有引用关系,才能找到这个程序集
var ApiRepository = Assembly.Load("Repository");
//这里是约定名称以Repository结尾,进行一个依赖
builder.RegisterAssemblyTypes(ApiRepository).Where(t => t.Name.EndsWith("Repository"))
.AsImplementedInterfaces();
var ApiService = Assembly.Load("Service");
builder.RegisterAssemblyTypes(ApiService).Where(t=>t.Name.EndsWith("Service")).AsImplementedInterfaces();
#endregion
#region 单个依赖注入服务
//名字不规范或者不在范围内,可以使用单个依赖注入
//builder.RegisterType<ChatHub>().As<IChatHub>().SingleInstance();
#endregion
base.Load(builder);
}
}
4、最后在Program里面调用这个方法
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
//调用 程序集 注册存入 方法
builder.RegisterModule(new AutoFacManage());
})
四、使用Serilog日志
1、在Commons里新建一个文件夹为Extensions,在里面创建一个SerilogLoggerWrapper类,用来封装保存日志的简单操作。
并且使用单例注入模式,进行一个全局的使用。这里需要安装Serilog的NuGet包。Serilog,Serilog.AspNetCore ,Serilog.Sinks.File 。
然后开始封装方法
public class SerilogLoggerWrapper
{
private readonly LoggerConfiguration _loggerConfiguration;
private string _defaultLogFileName;
private SerilogLoggerWrapper()
{
_loggerConfiguration = new LoggerConfiguration();
_defaultLogFileName = $"{DateTime.Now:yyyy-MM-dd}_{LogLevelEnum.Information}.log";
}
// 配置并获取Serilog日志记录器
private ILogger BuildLogger()
{
//保存目录
string logFolderPath = "Logs";
string fullPath = Path.Combine(logFolderPath, _defaultLogFileName);
_loggerConfiguration.WriteTo.File(new JsonFormatter(), fullPath)
.WriteTo.Console();
return _loggerConfiguration.CreateLogger();
}
// 保存日志的方法。这里采用的是枚举的形式对日志的等级进行保存
//public enum LogLevelEnum
//{
// Verbose = 0,
// Debug = 1,
// Information = 2,
// Warning = 3,
// Error = 4,
// Fatal = 5
//}
public void SaveLog(LogLevelEnum level, string logMessage)
{
ILogger logger = BuildLogger();
switch (level)
{
case LogLevelEnum.Verbose:
logger.Verbose(logMessage);
break;
case LogLevelEnum.Debug:
logger.Debug(logMessage);
break;
case LogLevelEnum.Information:
logger.Information(logMessage);
break;
case LogLevelEnum.Warning:
logger.Warning(logMessage);
break;
case LogLevelEnum.Error:
logger.Error(logMessage);
break;
case LogLevelEnum.Fatal:
logger.Fatal(logMessage);
break;
}
// 关闭并刷新日志记录器,确保日志被写入存储介质
//logger.CloseAndFlush();
// 这里不再通过ILogger实例调用CloseAndFlush,因为它没有这个方法
// 使用Serilog.Log静态类来关闭和刷新日志
Log.CloseAndFlush();
}
// 单例模式相关 - 静态实例字段
private static SerilogLoggerWrapper _instance;
// 单例模式相关 - 静态属性获取单例实例
public static SerilogLoggerWrapper Instance
{
get
{
if (_instance == null)
{
_instance = new SerilogLoggerWrapper();
}
return _instance;
}
}
}
最后在AutoFacManage方法后面 向Autofac容器添加一个单例模式注入即可
// 将SerilogLoggerWrapper以单例模式注入Autofac容器
builder.Register(c => SerilogLoggerWrapper.Instance)
.As<SerilogLoggerWrapper>()
.SingleInstance();
五、使用Identity颁发令牌,并且存储在redis里面
1、引入redis
1、添加redis的连接"RedisConnectString": ":6379,password=123456"
2、在Commons里面新增类RedisInvokerExtension
public static class RedisInvokerExtension
{
public static void AddRedisConfiguration(this IServiceCollection services, IConfiguration configuration)
{
// 读取配置文件中的Redis连接字符串
var redisConnectionString = configuration.GetConnectionString("RedisConnectString");
// 创建Redis连接对象
var redis = ConnectionMultiplexer.Connect(redisConnectionString);
// 将Redis连接对象注入到服务容器中,以便在其他地方可以使用
services.AddSingleton<IConnectionMultiplexer>(redis);
}
}
3、配置好后,在Program里引用
// 引用RedisInvokerExtension类进行Redis配置
builder.Services.AddRedisConfiguration(builder.Configuration);
4、再创建一个redis的操作类RedisOperator,配置一些基础操作
public class RedisOperator
{
private readonly IConnectionMultiplexer _redis;
public RedisOperator(IConnectionMultiplexer redis)
{
_redis = redis;
}
// 设置键值对
public async Task<bool> SetValue(string key, string value)
{
var database = _redis.GetDatabase();
return await database.StringSetAsync(key, value);
}
// 获取键值对
public async Task<string> GetValue(string key)
{
var database = _redis.GetDatabase();
return await database.StringGetAsync(key);
}
// 删除键
public async Task<bool> DeleteKey(string key)
{
var database = _redis.GetDatabase();
return await database.KeyDeleteAsync(key);
}
// 设置带有有效时间限制的键值对
public async Task<bool> SetValueWithExpiration(string key, string value, TimeSpan expiration)
{
var database = _redis.GetDatabase();
return await database.StringSetAsync(key, value, expiration);
}
// 检查键是否存在
public async Task<bool> KeyExists(string key)
{
var database = _redis.GetDatabase();
return await database.KeyExistsAsync(key);
}
}
2、配置仓储基类,实现一个简单的查询接口。
1、创建一个仓储接口基类 IRepositoryDAL(其余的方法可自定义,这里只做查询示范)
/// <summary>
/// 仓储接口基类
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public interface IRepositoryDAL<TEntity>where TEntity : class,new ()
{
/// <summary>
/// 获取 当前实体的查询数据集(未跟踪到数据库)
/// </summary>
/// <value>
/// The entities.
/// </value>
IQueryable<TEntity> Entities { get; }
/// <summary>
///
/// </summary>
/// <param name="IsDeleted">是否查询被软删除的,默认false不查询</param>
/// <returns></returns>
IQueryable<TEntity> Queryable(bool IsDeleted = false);
}
2、创建一个仓储基类实现接口RepositoryDAL。
//这里的IBaseEntity是实体的基类接口,方便查询筛选IsDeleted 条件
//使用的时候还要再定义一个BaseEntity类,使所有实体继承IBaseEntity,BaseEntity实现即可
public class RepositoryDAL<TEntity> where TEntity : class, IBaseEntity, new()
{
private readonly WPFAPIContext _context;
public RepositoryDAL(WPFAPIContext context) {
_context = context;
}
/// <summary>
/// 全查询
/// </summary>
public IQueryable<TEntity> Entities
{
get
{
return _context.Set<TEntity>().AsNoTracking();
}
}
/// <summary>
/// 查询
/// </summary>
/// <param name="IsDeleted"></param>
/// <returns></returns>
public IQueryable<TEntity> Queryable(bool IsDeleted = false){
return _context.Set<TEntity>().AsNoTracking().Where(c => c.IsDeleted == IsDeleted);
}
}
3、仓储层即可这样写
//1、仓储接口
public interface IUserInfoRepository : IRepositoryDAL<UserInfo> { };
//2、仓储实现类
public class UserInfoRepository : RepositoryDAL<UserInfo>, IUserInfoRepository
{
public UserInfoRepository(WPFAPIContext context):base(context) {}
}
//需要其他参数,自行添加即可
4、再定义Service层的接口和实现类,直接使用IRepositoryDAL的方法即可
public class UserInfoService : IUserInfoService
{
private readonly IUserInfoRepository _userInfoRepository;
public UserInfoService(IUserInfoRepository userInfoRepository) {
_userInfoRepository = userInfoRepository;
}
/// <summary>
/// 获取用户信息
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public List<UserInfo> GetUserInfoList()
{
var result = _userInfoRepository.Queryable(true).ToList();
return result;
}
}
5、controller里面依赖注入service后测试接口成功获取到数据后,即完成
3、实现一个注册接口,要求密码为加密储存
1、新建一个String类型的扩展类,将字符串转为Md5(密钥可加可不加。md5很难被破译)
/// <summary>
/// 字符串扩展类
/// </summary>
public static class StringExtensions
{
public static string ToMD5(this string str)
{
//自定义密钥
string secretKey = "111";
// 将密钥和数据拼接
string combinedData = secretKey + str;
using (MD5 md5 = MD5.Create())
{
byte[] inputBytes = Encoding.UTF8.GetBytes(combinedData);
byte[] hashBytes = md5.ComputeHash(inputBytes);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("X2"));
}
return sb.ToString();
}
}
}
2、引用FluentValidation,实现便捷的参数验证
//安装好FluentValidation的包后,新建文件,里面新建类,存放验证规则
namespace Commons.Validator.Sys
{
public class SysUserInfoValidator
{
}
/// <summary>
/// 用户注册验证方法
/// </summary>
public class RegistUserParamesValidator : AbstractValidator<RegistUserParames>
{
public RegistUserParamesValidator()
{
RuleFor(x => x.UserName).NotEmpty().WithMessage("用户名不能为空");
RuleFor(x => x.LoginAccont).NotEmpty().WithMessage("登录账号不能为空");
RuleFor(x => x.VerifyCode).NotEmpty().WithMessage("验证码不能为空");
RuleFor(x => x.PassWord).MinimumLength(4).WithMessage("密码最少为4位");
RuleFor(x => x.AgainPassword).Equal(x => x.PassWord).WithMessage("两次输入的密码不一致");
RuleFor(x => x.Email).EmailAddress().WithMessage("请输入有效的邮箱地址");
}
}
/// <summary>
/// 发送邮件验证码
/// </summary>
public class RegistEmailVerifyCodeParamesValidator : AbstractValidator<EmailVerifyCodeParames> {
public RegistEmailVerifyCodeParamesValidator()
{
RuleFor(x => x.Email).EmailAddress().WithMessage("请输入有效的邮箱地址");
}
}
}
//在autofac容器注册
#region 批量注入FluentValidation验证器
var assembly = Assembly.GetExecutingAssembly();
builder.RegisterAssemblyTypes(assembly)
.Where(t => t.Name.EndsWith("Validator") && t.IsClass && !t.IsAbstract)
.AsImplementedInterfaces()
.SingleInstance();
#endregion
//在controller层使用构造函数注入的方式使用即可
private readonly ISysUserInfoService _sysUserInfoService;
private readonly IValidator<RegistUserParames> _validator;
private readonly IValidator<EmailVerifyCodeParames> _emailValidator;
public UserController(ISysUserInfoService sysUserInfoService, IValidator<RegistUserParames> validator,
IValidator<EmailVerifyCodeParames> emailValidator) {
_sysUserInfoService = sysUserInfoService;
_validator = validator;
_emailValidator = emailValidator;
}
/// <summary>
/// 用户注册
/// </summary>
/// <param name="parames"></param>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public async Task<ApiResult<string>> RegistUser(RegistUserParames parames)
{
var validationResult = _validator.Validate(parames);
//验证失败
if (!validationResult.IsValid)
{
return await ErrorAsync(string.Join(",", validationResult.Errors.Select(e => e.ErrorMessage)));
}
var result = await _sysUserInfoService.RegistUser(parames);
return await SuccessAsync(result);
}
3、实现注册逻辑
/// <summary>
/// 用户注册
/// </summary>
/// <param name="parames"></param>
/// <returns></returns>
public async Task<string> RegistUser(RegistUserParames parames)
{
//验证邮箱与验证码是否一致.这里将发送的邮箱验证码存入了redis里面
var code = await _redisOperator.GetValue(parames.Email);
if (code != parames.VerifyCode) {
return "验证码错误";
}
parames.PassWord = parames.PassWord.ToMD5();
var insertUserInfo = _mapper.Map<Sys_UserInfo>(parames);
insertUserInfo.AccountStatus = AccountStatusEnum.Inactive;
insertUserInfo.RegistrationTime = DateTime.Now;
insertUserInfo.Email = parames.Email;
insertUserInfo.PhoneNumber = "182233";
insertUserInfo.ProfilePicture = "11";
_userInfoRepository.Insert(insertUserInfo);
return "注册成功";
}
4、实现登录接口,并颁发Token。(登录这一块还是打算手写一个JWT生成。权限不使用Identity,后面自己手动写权限这一块的表个逻辑)
1、创建一个JWTHelper类
public class JWTHelper
{
private readonly IConfiguration _configuration;
private readonly RedisOperator _redisOperator;
public JWTHelper(IConfiguration configuration, RedisOperator redisOperator)
{
_configuration = configuration;
_redisOperator = redisOperator;
}
/// <summary>
/// 生成JWT
/// </summary>
/// <param name="tokenModel"></param>
/// <returns></returns>
public async Task<string> IssueJWT(TokenModel tokenModel)
{
var dateTime = DateTime.Now;
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Jti,tokenModel.Uid.ToString()),//用户ID
new Claim(ClaimTypes.Name,tokenModel.UserName),//用户Name
new Claim("Project",tokenModel.Project),//项目
new Claim(JwtRegisteredClaimNames.Iat, ((DateTimeOffset)dateTime).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expiration = int.Parse(_configuration["Jwt:ExpirationMinutes"]);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: dateTime.AddDays(expiration),
signingCredentials: creds
);
var Jwt = new JwtSecurityTokenHandler().WriteToken(token);
var keyForRedis = $"jwt_{tokenModel.Uid}";
var expirationTimeSpan = TimeSpan.FromDays(expiration);
var isSet = await _redisOperator.SetValueWithExpiration(keyForRedis, Jwt, expirationTimeSpan);
if (!isSet)
{
// 这里可以根据实际情况进行错误处理,比如抛出异常或者记录日志等
throw new BusinessException("向redis保存Jwt失败");
}
return Jwt;
}
/// <summary>
/// 验证JWT是否过期
/// </summary>
/// <param name="jwtStr"></param>
/// <returns></returns>
public async Task<bool> KeyExistsJwt(string jwtStr) {
var jwtHandler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
var keyForRedis = $"jwt_{jwtToken.Id}";
var result= await _redisOperator.KeyExists(keyForRedis);
return result;
}
/// <summary>
/// 解析JWT
/// </summary>
/// <returns></returns>
public TokenModel SerializeJWT(string jwtStr) {
var jwtHandler = new JwtSecurityTokenHandler();
var jwt = jwtStr.Substring("Bearer ".Length);
JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwt);
//解析自定义字段
object project = new object();
try
{
jwtToken.Payload.TryGetValue("Project", out project);
}
catch (Exception e)
{
Console.WriteLine("解析JWT报错:");
Console.Write(e);
throw;
}
var name = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
TokenModel tokenModel = new TokenModel()
{
Uid = long.Parse(jwtToken.Id),
UserName = name,
Project = project.ToString()
};
return tokenModel;
}
}
2、登录的业务调用JWT生成即可
/// <summary>
/// 用户登录
/// </summary>
/// <param name="parames"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<string> Login(LoginParames parames)
{
parames.PassWord = parames.PassWord.ToMD5();
var userInfo = _userInfoRepository.Queryable().FirstOrDefault(c => c.LoginAccont == parames.LoginAccont && c.PassWord == parames.PassWord);
if (userInfo != null) {
TokenModel tokenModel = new TokenModel()
{
Uid = userInfo.Id,
UserName = userInfo.UserName,
Project = "DemoAPI"
};
var jwt = await _jWTHelper.IssueJWT(tokenModel);
return jwt;
}
return "未查询到数据";
}
3、对于邮件发送
//1、新建一个邮件的发送帮助类。在autofac注册后,即可使用
public class EmailHelper
{
private readonly IConfiguration _configuration;
public EmailHelper(IConfiguration configuration) {
_configuration = configuration;
}
/// <summary>
/// 邮件发送
/// </summary>
/// <param name="parames"></param>
/// <returns></returns>
public bool SendEmail(SendEmailModel parames) {
// 发件人邮箱地址,str
var fromEmail = _configuration["Email:FromEmail"];
// 发件人邮箱授权码(注意不是邮箱密码,需在QQ邮箱设置中开启SMTP服务后获取)
string fromEmailPassword = _configuration["Email:Pwd"];
// 收件人邮箱地址
string toEmail = parames.ToEmail;
// 邮件主题
string subject = parames.Subject;
// 邮件正文内容
string body = parames.Body;
try
{
// 创建SmtpClient实例,设置SMTP服务器地址和端口
SmtpClient client = new SmtpClient("smtp.qq.com", 587);
// 设置发件人邮箱的用户名和密码(这里的密码是授权码)
client.Credentials = new NetworkCredential(fromEmail, fromEmailPassword);
// 设置启用SSL加密
client.EnableSsl = true;
// 创建MailMessage实例,设置发件人、收件人、主题和正文等信息
MailMessage message = new MailMessage(fromEmail, toEmail, subject, body);
// 添加附件(暂时不加)
//string attachmentPath = @"C:\YourAttachmentPath\example.txt"; // 替换为实际的附件路径
//Attachment attachment = new Attachment(attachmentPath);
//message.Attachments.Add(attachment);
// 发送邮件
client.Send(message);
return true;
}
catch (Exception ex) {
return false;
}
}
}
//autofac
//InstancePerLifetimeScope 在一个特定的生命周期范围,服务只会被创建一次。占用较多内存 会释放
//SingleInstance 对于整个程序的生命周期而言,只会被创建一次。占用内存稳定
builder.RegisterType<EmailHelper>().As<EmailHelper>().InstancePerLifetimeScope();
//调用使用构造函数注入方式即可
5、验证颁发的token有效性。
1、新建一个JwtAuthorizationFilter自定义中间件。用来验证token的有效性
public class JwtAuthorizationFilter
{
private readonly RedisOperator _redisOperator;
private readonly JWTHelper _jWTHelper;
public JwtAuthorizationFilter(RedisOperator redisOperator, JWTHelper jWTHelper) {
_redisOperator = redisOperator;
_jWTHelper = jWTHelper;
}
public async Task Invoke(HttpContext httpContext, RequestDelegate next) {
// 获取当前请求对应的端点信息
var endpoint = httpContext.GetEndpoint();
// 检查端点是否有AllowAnonymous特性
var allowAnonymous = endpoint?.Metadata?.OfType<AllowAnonymousAttribute>().Any();
if (allowAnonymous == true)
{
// 如果有AllowAnonymous特性,直接跳过权限验证逻辑,将请求传递给下一个中间件或处理程序
await next(httpContext);
return;
}
var authorizationHeader = httpContext.Request.Headers["Authorization"].FirstOrDefault();
//判断请求是否不为空带Bearer
if (!string.IsNullOrEmpty(authorizationHeader) && authorizationHeader.StartsWith("Bearer "))
{
var jwt = authorizationHeader.Substring("Bearer ".Length);
if ( !await _jWTHelper.KeyExistsJwt(jwt)) {
throw new BusinessException("权限验证失败,请检查!");
}
}
else {
throw new BusinessException("权限验证失败,请检查!");
}
//验证通过,将信息放入httpContext
var tokenModel = _jWTHelper.SerializeJWT(authorizationHeader);
var claimList = new List<Claim>();
//添加UserId申明
var claimUserId = new Claim(ClaimTypes.NameIdentifier, tokenModel.Uid.ToString());
claimList.Add(claimUserId);
// 添加UserName声明
var claimUserName = new Claim(ClaimTypes.Name, tokenModel.UserName);
claimList.Add(claimUserName);
// 添加UserAge声明,这里假设将年龄转换为字符串添加,你也可以根据需求采用其他合适的方式处理年龄信息
//var claimUserAge = new Claim("UserAge", tokenModel.UserAge.ToString());
//claimList.Add(claimUserAge);
var identity = new ClaimsIdentity(claimList);
var principal = new ClaimsPrincipal(identity);
await next(httpContext);
}
}
2、在program里面加入
// 添加自定义中间件JwtAuthorizationFilter
var jwtAuthorizationFilter = app.Services.GetRequiredService<JwtAuthorizationFilter>();
app.Use(async (context, next) =>
{
await jwtAuthorizationFilter.Invoke(context, next);
});
//添加好后,默认对请求进行权限验证。要对接口不进行权限验证,接口加上 [AllowAnonymous] 特性即可
总结
到此完毕结束。