基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口
在基于SqlSugar的开发框架中,我们设计了一些系统服务层的基类,在基类中会有很多涉及到相关的数据处理操作的,如果需要跟踪具体是那个用户进行操作的,那么就需要获得当前用户的身份信息,包括在Web API的控制器中也是一样,需要获得对应的用户身份信息,才能进行相关的身份鉴别和处理操作。本篇随笔介绍基于Principal的用户身份信息的存储和读取操作,以及在适用于Winform程序中的内存缓存的处理方式,从而通过在基类接口中注入用户身份信息接口方式,获得当前用户的详细身份信息。
1、用户身份接口的定义和基类接口注入
为了方便获取用户身份的信息,我们定义一个接口 IApiUserSession 如下所示。
/// <summary> ///API接口授权获取的用户身份信息-接口/// </summary> public interfaceIApiUserSession
{/// <summary> ///用户登录来源渠道,0为网站,1为微信,2为安卓APP,3为苹果APP/// </summary> string Channel { get; }/// <summary> ///用户ID/// </summary> int? Id { get; }/// <summary> ///用户名称/// </summary> string Name { get; }/// <summary> ///用户邮箱(可选)/// </summary> string Email { get; }/// <summary> ///用户手机(可选)/// </summary> string Mobile { get; }/// <summary> ///用户全名称(可选)/// </summary> string FullName { get; }/// <summary> ///性别(可选)/// </summary> string Gender { get; }/// <summary> ///所属公司ID(可选)/// </summary> string Company_ID { get; }/// <summary> ///所属公司名称(可选)/// </summary> string CompanyName { get; }/// <summary> ///所属部门ID(可选)/// </summary> string Dept_ID { get; }/// <summary> ///所属部门名称(可选)/// </summary> string DeptName { get; }/// <summary> ///把用户信息设置到缓存中去/// </summary> /// <param name="info">用户登陆信息</param> /// <param name="channel">默认为空,用户登录来源渠道:0为网站,1为微信,2为安卓APP,3为苹果APP</param> void SetInfo(LoginUserInfo info, string channel = null);
}
其中的SetInfo是为了在用户身份登录确认后,便于将用户信息存储起来的一个接口方法。其他属性定义用户相关的信息。
由于这个用户身份信息的接口,我们提供给基类进行使用的,默认我们在基类定义一个接口对象,并通过提供默认的NullApiUserSession实现,便于引用对应的身份属性信息。
NullApiUserSession只是提供一个默认的实现,实际在使用的时候,我们会注入一个具体的接口实现来替代它的。
/// <summary> ///提供一个空白实现类,具体使用IApiUserSession的时候,会使用其他实现类/// </summary> public classNullApiUserSession : IApiUserSession
{/// <summary> ///单件实例/// </summary> public static NullApiUserSession Instance { get; } = newNullApiUserSession();public string Channel => null;public int? Id => null;public string Name => null;
..................
/// <summary> ///设置信息(保留为空)/// </summary> public void SetInfo(LoginUserInfo info, string channel = null)
{
}
}
在之前介绍的SqlSugar框架的时候,我们介绍到数据访问操作的基类定义,如下所示。
/// <summary> ///基于SqlSugar的数据库访问操作的基类对象/// </summary> /// <typeparam name="TEntity">定义映射的实体类</typeparam> /// <typeparam name="TKey">主键的类型,如int,string等</typeparam> /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam> public abstract class MyCrudService<TEntity, TKey, TGetListInput>:
IMyCrudService<TEntity, TKey, TGetListInput> where TEntity : class, IEntity<TKey>, new()whereTGetListInput : IPagedAndSortedResultRequest
{/// <summary> ///数据库上下文信息/// </summary> protectedDbContext dbContext;
/// <summary> ///当前Api用户信息/// </summary> public IApiUserSession CurrentApiUser { get; set; }publicMyCrudService()
{
dbContext= newDbContext();
CurrentApiUser= NullApiUserSession.Instance;//空实现 }
在最底层的操作基类中,我们就已经注入了用户身份信息,这样我们不管操作任何函数处理,都可以通过该用户身份信息接口CurrentApiUser获得对应的用户属性信息了。
在具体的业务服务层中,我们继承该基类,并提供构造函数注入方式,让基类获得对应的 IApiUserSession接口的具体实例。
/// <summary> ///应用层服务接口实现/// </summary> public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{/// <summary> ///构造函数/// </summary> /// <param name="currentApiUser">当前用户接口</param> publicCustomerService(IApiUserSession currentApiUser)
{this.CurrentApiUser=currentApiUser;
}
........
}
如果有其他服务接口需要引入,那么我们继续增加其他接口注入即可。
/// <summary> ///角色信息 应用层服务接口实现/// </summary> public class RoleService : MyCrudService<RoleInfo,int, RolePagedDto>, IRoleService
{privateIOuService _ouService;privateIUserService _userService;/// <summary> ///默认构造函数/// </summary> /// <param name="currentApiUser">当前用户接口</param> /// <param name="ouService">机构服务接口</param> /// <param name="userService">用户服务接口</param> publicRoleService(IApiUserSession currentApiUser, IOuService ouService, IUserService userService)
{this.CurrentApiUser = currentApiUser;this._ouService =ouService;this._userService =userService;
}
由于该接口是通过构造函数注入的,因此在系统运行前,我们需要往IOC容器中注册对应的接口实现类(由于
IApiUserSession
提供了多个接口实现,我们这里不自动加入它的对应接口,而通过手工加入)。
在Winform或者控制台程序,启动程序的时候,手工加入对应的接口到IOC容器中即可。
/// <summary> ///应用程序的主入口点。/// </summary> [STAThread]static voidMain()
{//IServiceCollection负责注册 IServiceCollection services = newServiceCollection();//services.AddSingleton<IDictDataService, DictDataService>();//调用自定义的服务注册 ServiceInjection.ConfigureRepository(services);//添加IApiUserSession实现类//services.AddSingleton<IApiUserSession, ApiUserCache>();//缓存实现方式 services.AddSingleton<IApiUserSession, ApiUserPrincipal>(); //CurrentPrincipal实现方式
如果是Web API或者asp.net core项目中加入,也是类似的处理方式。
var builder =WebApplication.CreateBuilder(args);//配置依赖注入访问数据库 ServiceInjection.ConfigureRepository(builder.Services);//添加IApiUserSession实现类 builder.Services.AddSingleton<IApiUserSession, ApiUserPrincipal>();
前面介绍了,IApiUserSession的一个空白实现,是默认的接口实现,我们具体会使用基于Principal或者缓存方式实现记录用户身份的信息实现,如下是它们的类关系。
在上面的代码中,我们注入一个 ApiUserPrincipal 的用户身份接口实现。
2、基于Principal的用户身份信息的存储和读取操作
ApiUserPrincipal 的用户身份接口实现是可以实现Web及Winform的用户身份信息的存储的。
首先我们先定义一些存储声明信息的键,便于统一处理。
/// <summary> ///定义一些常用的ClaimType存储键/// </summary> public classApiUserClaimTypes
{public const string Id =JwtClaimTypes.Id;public const string Name =JwtClaimTypes.Name;public const string NickName =JwtClaimTypes.NickName;public const string Email =JwtClaimTypes.Email;public const string PhoneNumber =JwtClaimTypes.PhoneNumber;public const string Gender =JwtClaimTypes.Gender;public const string FullName = "FullName";public const string Company_ID = "Company_ID";public const string CompanyName = "CompanyName";public const string Dept_ID = "Dept_ID";public const string DeptName = "DeptName";public const string Role =ClaimTypes.Role;
}
ApiUserPrincipal 用户身份接口实现的定义如下代码所示。
/// <summary> ///基于ClaimsPrincipal实现的用户信息接口。/// </summary> [Serializable]public classApiUserPrincipal : IApiUserSession
{/// <summary> ///IHttpContextAccessor对象/// </summary> private readonlyIHttpContextAccessor_httpContextAccessor;/// <summary> ///如果IHttpContextAccessor.HttpContext?.User非空获取HttpContext的ClaimsPrincipal,否则获取线程的CurrentPrincipal/// </summary> protected ClaimsPrincipal Principal => _httpContextAccessor?.HttpContext?.User ?? (Thread.CurrentPrincipal asClaimsPrincipal);/// <summary> ///默认构造函数/// </summary> /// <param name="httpContextAccessor"></param> publicApiUserPrincipal(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor=httpContextAccessor;
}/// <summary> ///默认构造函数/// </summary> public ApiUserPrincipal() { }
基于Web API的时候,用户身份信息是基于
IHttpContextAccessor
注入的接口获得 httpContextAccessor?.HttpContext?.User 的 ClaimsPrincipal 属性操作的。
我们获取用户身份的属性的时候,直接通过这个属性判断获取即可。
/// <summary> ///用户ID/// </summary> public int? Id => this.Principal?.FindFirst(ApiUserClaimTypes.Id)?.Value.ToInt32();/// <summary> ///用户名称/// </summary> public string Name => this.Principal?.FindFirst(ApiUserClaimTypes.Name)?.Value;
而上面同时也提供了一个基于Windows的线程Principal 属性(Thread.CurrentPrincipal )的声明操作,操作模型和Web 的一样的,因此Web和WinForm的操作是一样的。
在用户登录接口处理的时候,我们需要统一设置一下用户对应的声明信息,存储起来供查询使用。
/// <summary> ///主要用于Winform写入Principal的ClaimsIdentity/// </summary> public void SetInfo(LoginUserInfo info, string channel = null)
{//new WindowsPrincipal(WindowsIdentity.GetCurrent()); var claimIdentity = new ClaimsIdentity("login");
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Id, info.ID ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Name, info.UserName ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Email, info.Email ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.PhoneNumber, info.MobilePhone ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Gender, info.Gender ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.FullName, info.FullName ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Company_ID, info.CompanyId ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.CompanyName, info.CompanyName ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.Dept_ID, info.DeptId ?? ""));
claimIdentity.AddClaim(new Claim(ApiUserClaimTypes.DeptName, info.DeptName ?? ""));//此处不可以使用下面注释代码//this.Principal?.AddIdentity(claimIdentity);//Thread.CurrentPrincipal设置会导致在异步线程中设置的结果丢失//因此统一采用 AppDomain.CurrentDomain.SetThreadPrincipal中设置,确保进程中所有线程都会复制到信息 IPrincipal principal = new GenericPrincipal(claimIdentity, null);
AppDomain.CurrentDomain.SetThreadPrincipal(principal);
}
在上面中,我特别声明“
Thread.CurrentPrincipal设置会导致在异步线程中设置的结果丢失
” ,这是我在反复测试中发现,不能在异步方法中设置Thread.CurrentPrincipal的属性,否则属性会丢失,因此主线程的Thread.CurrentPrincipal 会赋值替换掉异步线程中的Thread.CurrentPrincipal属性。
而.net 提供了一个程序域的方式设置CurrentPrincipal的方法,可以或者各个线程中统一的信息。
AppDomain.CurrentDomain.SetThreadPrincipal(principal);
基于WInform的程序,我们在登录界面中处理用户登录操作
但用户确认登录的时候,测试用户的账号密码,成功则在本地设置用户的身份信息。
/// <summary> ///统一设置登陆用户相关的信息/// </summary> /// <param name="info">当前用户信息</param> public asyncTask SetLoginInfo(LoginResult loginResult)
{var info = loginResult.UserInfo; //用户信息//获取用户的角色集合 var roles = await BLLFactory<IRoleService>.Instance.GetRolesByUser(info.Id);//判断用户是否超级管理员||公司管理员 var isAdmin = roles.Any(r => r.Name == RoleInfo.SuperAdminName || r.Name ==RoleInfo.CompanyAdminName);//初始化权限用户信息 Portal.gc.UserInfo = info; //登陆用户 Portal.gc.RoleList = roles;//用户的角色集合 Portal.gc.IsUserAdmin = isAdmin;//是否超级管理员或公司管理员 Portal.gc.LoginUserInfo = this.ConvertToLoginUser(info); //转换为窗体可以缓存的对象//设置身份信息到共享对象中(Principal或者Cache) BLLFactory<IApiUserSession>.Instance.SetInfo(Portal.gc.LoginUserInfo);awaitTask.CompletedTask;
}
通过SetInfo,我们把当前用户的信息设置到了域的Principal中,进程内的所有线程共享这份用户信息数据。
跟踪接口的调用,我们可以查看到对应的用户身份信息了。
可以看到,这个接口已经注入到了服务类中,并且获得了相应的用户身份信息了。
同样在Web API的登录处理的时候,会生成相关的JWT token的信息的。
var loginResult = await this._userService.VerifyUser(dto.LoginName, dto.Password, ip);if (loginResult != null && loginResult.UserInfo != null)
{var userInfo =loginResult.UserInfo;
authResult.AccessToken= GenerateToken(userInfo); //令牌 authResult.Expires = expiredDays * 24 * 3600; //失效秒数 authResult.Succes = true;//成功//设置缓存用户信息//SetUserCache(userInfo); }else{
authResult.Error= loginResult?.ErrorMessage;
}
其中生成的JWT token的逻辑如下所示。
/// <summary> ///生成JWT用户令牌/// </summary> /// <returns></returns> private stringGenerateToken(UserInfo userInfo)
{var claims = new List<Claim>{newClaim(ApiUserClaimTypes.Id, userInfo.Id.ToString()),newClaim(ApiUserClaimTypes.Email, userInfo.Email),newClaim(ApiUserClaimTypes.Name, userInfo.Name),newClaim(ApiUserClaimTypes.NickName, userInfo.Nickname),newClaim(ApiUserClaimTypes.PhoneNumber, userInfo.MobilePhone),newClaim(ApiUserClaimTypes.Gender, userInfo.Gender),newClaim(ApiUserClaimTypes.FullName, userInfo.FullName),newClaim(ApiUserClaimTypes.Company_ID, userInfo.Company_ID),newClaim(ApiUserClaimTypes.CompanyName, userInfo.CompanyName),newClaim(ApiUserClaimTypes.Dept_ID, userInfo.Dept_ID),newClaim(ApiUserClaimTypes.DeptName, userInfo.DeptName),
};var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]));var jwt = newJwtSecurityToken
(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddDays(expiredDays),//有效时间 signingCredentials: newSigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);var token = newJwtSecurityTokenHandler().WriteToken(jwt);returntoken;
}
说生成的一系列字符串,我们可以通过解码工具,可以解析出来对应的信息的。
在登录授权的这个时候,控制器会把相关的Claim信息写入到token中的,我们在客户端发起对控制器方法的调用的时候,这些身份信息会转换成对象信息。
我们调试控制器的方法入口,如可以通过Fiddler的测试接口的调用情况。
可以看到CurrentApiUser的信息就是我们发起用户身份信息,如下图所示。
在监视窗口中查看IApiUserSession对象,可以查看到对应的信息。
3、基于内存缓存的用户身份接口实现处理方式
在前面介绍的IApiUserSession的接口实现的时候,我们也提供了另外一个基于MemoryCache的缓存实现方式,和基于Principal凭证信息处理不同,我们这个是基于MemoryCache的存储方式。
它的实现方法也是类似的,我们这里也一并介绍一下。
/// <summary> ///基于MemeoryCache实现的用户信息接口/// </summary> public classApiUserCache : IApiUserSession
{/// <summary> ///内存缓存对象/// </summary> private static readonly ObjectCache Cache =MemoryCache.Default;/// <summary> ///默认构造函数/// </summary> publicApiUserCache()
{
}/// <summary> ///把用户信息设置到缓存中去/// </summary> /// <param name="info">用户登陆信息</param> public void SetInfo(LoginUserInfo info, string channel = null)
{
SetItem(ApiUserClaimTypes.Id, info.ID);
SetItem(ApiUserClaimTypes.Name, info.UserName);
SetItem(ApiUserClaimTypes.Email, info.Email);
SetItem(ApiUserClaimTypes.PhoneNumber, info.MobilePhone);
SetItem(ApiUserClaimTypes.Gender, info.Gender);
SetItem(ApiUserClaimTypes.FullName, info.FullName);
SetItem(ApiUserClaimTypes.Company_ID, info.CompanyId);
SetItem(ApiUserClaimTypes.CompanyName, info.CompanyName);
SetItem(ApiUserClaimTypes.Dept_ID, info.DeptId);
SetItem(ApiUserClaimTypes.DeptName, info.DeptName);
}/// <summary> ///设置某个属性对象/// </summary> /// <param name="key"></param> /// <param name="value"></param> private void SetItem(string key, objectvalue)
{if (!string.IsNullOrEmpty(key))
{
Cache.Set(key, value?? "", DateTimeOffset.MaxValue, null);
}
}/// <summary> ///用户ID/// </summary> public int? Id => (Cache.Get(ApiUserClaimTypes.Id) as string)?.ToInt32();/// <summary> ///用户名称/// </summary> public string Name => Cache.Get(ApiUserClaimTypes.Name) as string;/// <summary> ///用户邮箱(可选)/// </summary> public string Email => Cache.Get(ApiUserClaimTypes.Email) as string;
..............
}
我们通过
MemoryCache.Default
构造一个内存缓存的对象,然后在设置信息的时候,把用户信息按照键值方式设置即可。在Winform中我们可以采用内存缓存的方式存储用户身份信息,而基于Web方式的,则会存在并发多个用户的情况,不能用缓存来处理。
一般情况下,我们采用 ApiUserPrincipal 来处理用户身份信息就很好了。
4、单元测试的用户身份处理
在做单元测试的时候,我们如果需要设置测试接口的用户身份信息,那么就需要在初始化函数里面设置好用户信息,如下所示。
[TestClass]public classUnitTest1
{private static IServiceProvider Provider = null;/*带有[ClassInitialize()] 特性的方法在执行类中第一个测试之前调用。
带有[TestInitialize()] 特性的方法在执行每个测试前都会被调用,一般用来初始化环境,为单元测试配置一个特定已知的状态。
带有[ClassCleanup()] 特性的方法将在类中所有的测试运行完后执行。*/ //[TestInitialize]//每个测试前调用 [ClassInitialize] //测试类第一次调用 public static voidSetup(TestContext context)
{//IServiceCollection负责注册 IServiceCollection services = newServiceCollection();//调用自定义的服务注册 ServiceInjection.ConfigureRepository(services);//注入当前Api用户信息处理实现,服务对象可以通过IApiUserSession获得用户信息//services.AddSingleton<IApiUserSession, ApiUserCache>();//缓存实现方式 services.AddSingleton<IApiUserSession, ApiUserPrincipal>(); //CurrentPrincipal实现方式//IServiceProvider负责提供实例 Provider =services.BuildServiceProvider();//模拟写入登录用户信息 WriteLoginInfo();
}/// <summary> ///写入用户登陆信息,IApiUserSession接口才可使用获取身份/// </summary> static voidWriteLoginInfo()
{var mockUserInfo = newLoginUserInfo()
{
ID= "1",
Email= "wuhuacong@163.com",
MobilePhone= "18620292076",
UserName= "admin",
FullName= "伍华聪"};//通过使用全局IServiceProvider的接口获得服务接口实例 Provider.GetService<IApiUserSession>().SetInfo(mockUserInfo);
}
上面的方法初始化了测试类的信息,方法调用的时候,我们获得对应的接口实例处理即可,如下测试代码所示。
/// <summary> ///测试查找记录/// </summary> /// <returns></returns> [TestMethod]public asyncTask TestMethod1()
{var input = newDictTypePagedDto()
{
Name= "客户"}; var service = Provider.GetService<IDictTypeService>();var count = await service.CountAsync(s => true);
Assert.AreNotEqual(0, count);var list = awaitservice.GetAllAsync();
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count> 0);
list= awaitservice.GetListAsync(input);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count> 0);var ids = list.Items.Select(s => { return s.Id; }).Take(2);
list= awaitservice.GetAllByIdsAsync(ids);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count> 0);var id = list.Items[0].Id;var info = awaitservice.GetAsync(id);
Assert.IsNotNull(info);
Assert.AreEqual(id, info.Id);
info= await service.GetFirstAsync(s => true);
Assert.IsNotNull(info);awaitTask.CompletedTask;
}
系列文章:
《
基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用
》
《
基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理
》
《
基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发
》
《
基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理
》
《
基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转
》
《
基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口
》
《
基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传
》
《
基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录
》
《
基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制
》
《
基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理
》
《
基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结
》
《
基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理
》
《
基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用
》
《
基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用
》
《
基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成
》
《
基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍
》
《
基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理
》
《
基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面
》