基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口

网友投稿 577 2022-10-05

基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口

基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口

在基于SqlSugar的开发框架中,我们设计了一些系统服务层的基类,在基类中会有很多涉及到相关的数据处理操作的,如果需要跟踪具体是那个用户进行操作的,那么就需要获得当前用户的身份信息,包括在Web API的控制器中也是一样,需要获得对应的用户身份信息,才能进行相关的身份鉴别和处理操作。本篇随笔介绍基于Principal的用户身份信息的存储和读取操作,以及在适用于Winform程序中的内存缓存的处理方式,从而通过在基类接口中注入用户身份信息接口方式,获得当前用户的详细身份信息。

1、用户身份接口的定义和基类接口注入

为了方便获取用户身份的信息,我们定义一个接口 IApiUserSession 如下所示。

其中的SetInfo是为了在用户身份登录确认后,便于将用户信息存储起来的一个接口方法。其他属性定义用户相关的信息。

由于这个用户身份信息的接口,我们提供给基类进行使用的,默认我们在基类定义一个接口对象,并通过提供默认的NullApiUserSession实现,便于引用对应的身份属性信息。

NullApiUserSession只是提供一个默认的实现,实际在使用的时候,我们会注入一个具体的接口实现来替代它的。

///

/// 提供一个空白实现类,具体使用IApiUserSession的时候,会使用其他实现类 /// public class NullApiUserSession : IApiUserSession { /// /// 单件实例 /// public static NullApiUserSession Instance { get; } = new NullApiUserSession(); public string Channel => null; public int? Id => null; public string Name => null; .................. /// /// 设置信息(保留为空) /// public void SetInfo(LoginUserInfo info, string channel = null) { } }

在之前介绍的SqlSugar框架的时候,我们介绍到数据访问操作的基类定义,如下所示。

///

/// 基于SqlSugar的数据库访问操作的基类对象 /// /// 定义映射的实体类 /// 主键的类型,如int,string等 /// 或者分页信息的条件对象 public abstract class MyCrudService : IMyCrudService where TEntity : class, IEntity, new() where TGetListInput : IPagedAndSortedResultRequest { /// /// 数据库上下文信息 /// protected DbContext dbContext; /// /// 当前Api用户信息 /// public IApiUserSession CurrentApiUser { get; set; } public MyCrudService() { dbContext = new DbContext(); CurrentApiUser = NullApiUserSession.Instance;//空实现

在最底层的操作基类中,我们就已经注入了用户身份信息,这样我们不管操作任何函数处理,都可以通过该用户身份信息接口CurrentApiUser获得对应的用户属性信息了。

在具体的业务服务层中,我们继承该基类,并提供构造函数注入方式,让基类获得对应的 IApiUserSession接口的具体实例。

///

/// 应用层服务接口实现 /// public class CustomerService : MyCrudService, ICustomerService { /// /// 构造函数 /// /// 当前用户接口 public CustomerService(IApiUserSession currentApiUser) { this.CurrentApiUser= currentApiUser; } ........ }

如果有其他服务接口需要引入,那么我们继续增加其他接口注入即可。

///

/// 角色信息 应用层服务接口实现 /// public class RoleService : MyCrudService, IRoleService { private IOuService _ouService; private IUserService _userService; /// /// 默认构造函数 /// /// 当前用户接口 /// 机构服务接口 /// 用户服务接口 public RoleService(IApiUserSession currentApiUser, IOuService ouService, IUserService userService) { this.CurrentApiUser = currentApiUser; this._ouService = ouService; this._userService = userService; }

由于该接口是通过构造函数注入的,因此在系统运行前,我们需要往IOC容器中注册对应的接口实现类(由于IApiUserSession 提供了多个接口实现,我们这里不自动加入它的对应接口,而通过手工加入)。

在Winform或者控制台程序,启动程序的时候,手工加入对应的接口到IOC容器中即可。

///

/// 应用程序的主入口点。/// [STAThread]static void Main(){ // IServiceCollection负责注册 IServiceCollection services = new ServiceCollection(); //services.AddSingleton(); //调用自定义的服务注册 ServiceInjection.ConfigureRepository(services); //添加IApiUserSession实现类 //services.AddSingleton(); //缓存实现方式 services.AddSingleton(); //CurrentPrincipal实现方式

如果是Web API或者asp- core项目中加入,也是类似的处理方式。

var builder = WebApplication.CreateBuilder(args);//配置依赖注入访问数据库ServiceInjection.ConfigureRepository(builder.Services);//添加IApiUserSession实现类

前面介绍了,IApiUserSession的一个空白实现,是默认的接口实现,我们具体会使用基于Principal或者缓存方式实现记录用户身份的信息实现,如下是它们的类关系。

在上面的代码中,我们注入一个 ApiUserPrincipal 的用户身份接口实现。

2、基于Principal的用户身份信息的存储和读取操作

ApiUserPrincipal 的用户身份接口实现是可以实现Web及Winform的用户身份信息的存储的。

///

/// 定义一些常用的ClaimType存储键 /// public class ApiUserClaimTypes { 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 用户身份接口实现的定义如下代码所示。

///

/// 基于ClaimsPrincipal实现的用户信息接口。 /// [Serializable] public class ApiUserPrincipal : IApiUserSession { /// /// IHttpContextAccessor对象 /// private readonly IHttpContextAccessor_ /// /// 如果IHttpContextAccessor.HttpContext?.User非空获取HttpContext的ClaimsPrincipal,否则获取线程的CurrentPrincipal /// protected ClaimsPrincipal Principal => _?? (Thread.CurrentPrincipal as ClaimsPrincipal); /// /// 默认构造函数 /// ///

而- 提供了一个程序域的方式设置CurrentPrincipal的方法,可以或者各个线程中统一的信息。

AppDomain.CurrentDomain.SetThreadPrincipal(principal);

基于WInform的程序,我们在登录界面中处理用户登录操作

但用户确认登录的时候,测试用户的账号密码,成功则在本地设置用户的身份信息。

///

/// 统一设置登陆用户相关的信息 /// /// 当前用户信息 public async Task SetLoginInfo(LoginResult loginResult) { var info = loginResult.UserInfo; //用户信息 //获取用户的角色集合 var roles = await BLLFactory.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.Instance.SetInfo(Portal.gc.LoginUserInfo); await Task.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的逻辑如下所示。

///

/// 生成JWT用户令牌 /// /// private string GenerateToken(UserInfo userInfo) { var claims = new { new Claim(ApiUserClaimTypes.Id, userInfo.Id.ToString()), new Claim(ApiUserClaimTypes.Email, userInfo.Email), new Claim(ApiUserClaimTypes.Name, userInfo.Name), new Claim(ApiUserClaimTypes.NickName, userInfo.Nickname), new Claim(ApiUserClaimTypes.PhoneNumber, userInfo.MobilePhone), new Claim(ApiUserClaimTypes.Gender, userInfo.Gender), new Claim(ApiUserClaimTypes.FullName, userInfo.FullName), new Claim(ApiUserClaimTypes.Company_ID, userInfo.Company_ID), new Claim(ApiUserClaimTypes.CompanyName, userInfo.CompanyName), new Claim(ApiUserClaimTypes.Dept_ID, userInfo.Dept_ID), new Claim(ApiUserClaimTypes.DeptName, userInfo.DeptName), }; var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"])); var jwt = new JwtSecurityToken ( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: claims, expires: DateTime.Now.AddDays(expiredDays),//有效时间 signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256) ); var token = new JwtSecurityTokenHandler().WriteToken(jwt); return token; }

说生成的一系列字符串,我们可以通过解码工具,可以解析出来对应的信息的。

在登录授权的这个时候,控制器会把相关的Claim信息写入到token中的,我们在客户端发起对控制器方法的调用的时候,这些身份信息会转换成对象信息。

我们调试控制器的方法入口,如可以通过Fiddler的测试接口的调用情况。

可以看到CurrentApiUser的信息就是我们发起用户身份信息,如下图所示。

在监视窗口中查看IApiUserSession对象,可以查看到对应的信息。

3、基于内存缓存的用户身份接口实现处理方式

在前面介绍的IApiUserSession的接口实现的时候,我们也提供了另外一个基于MemoryCache的缓存实现方式,和基于Principal凭证信息处理不同,我们这个是基于MemoryCache的存储方式。

它的实现方法也是类似的,我们这里也一并介绍一下。

///

/// 基于MemeoryCache实现的用户信息接口 /// public class ApiUserCache : IApiUserSession { /// /// 内存缓存对象 /// private static readonly ObjectCache Cache = MemoryCache.Default; /// /// 默认构造函数 /// public ApiUserCache() { } /// /// 把用户信息设置到缓存中去 /// /// 用户登陆信息 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); } /// /// 设置某个属性对象 /// /// /// private void SetItem(string key, object value) { if (!string.IsNullOrEmpty(key)) { Cache.Set(key, value ?? "", DateTimeOffset.MaxValue, null); } } /// /// 用户ID /// public int? Id => (Cache.Get(ApiUserClaimTypes.Id) as string)?.ToInt32(); /// /// 用户名称 /// public string Name => Cache.Get(ApiUserClaimTypes.Name) as string; /// /// 用户邮箱(可选) /// public string Email => Cache.Get(ApiUserClaimTypes.Email) as string; .............. }

我们通过 MemoryCache.Default构造一个内存缓存的对象,然后在设置信息的时候,把用户信息按照键值方式设置即可。在Winform中我们可以采用内存缓存的方式存储用户身份信息,而基于Web方式的,则会存在并发多个用户的情况,不能用缓存来处理。

一般情况下,我们采用 ApiUserPrincipal 来处理用户身份信息就很好了。

4、单元测试的用户身份处理

在做单元测试的时候,我们如果需要设置测试接口的用户身份信息,那么就需要在初始化函数里面设置好用户信息,如下所示。

[TestClass] public class UnitTest1 { private static IServiceProvider Provider = null; /* 带有[ClassInitialize()] 特性的方法在执行类中第一个测试之前调用。 带有[TestInitialize()] 特性的方法在执行每个测试前都会被调用,一般用来初始化环境,为单元测试配置一个特定已知的状态。 带有[ClassCleanup()] 特性的方法将在类中所有的测试运行完后执行。 */ //[TestInitialize] //每个测试前调用 [ClassInitialize] //测试类第一次调用 public static void Setup(TestContext context) { // IServiceCollection负责注册 IServiceCollection services = new ServiceCollection(); //调用自定义的服务注册 ServiceInjection.ConfigureRepository(services); //注入当前Api用户信息处理实现,服务对象可以通过IApiUserSession获得用户信息 //services.AddSingleton(); //缓存实现方式 services.AddSingleton(); //CurrentPrincipal实现方式 // IServiceProvider负责提供实例 Provider = services.BuildServiceProvider(); //模拟写入登录用户信息 WriteLoginInfo(); } ///

/// 写入用户登陆信息,IApiUserSession接口才可使用获取身份 /// static void WriteLoginInfo() { var mockUserInfo = new LoginUserInfo() { ID = "1", Email = "wuhuacong@163.com", MobilePhone = "18620292076", UserName = "admin", FullName = "伍华聪" }; //通过使用全局IServiceProvider的接口获得服务接口实例 Provider.GetService().SetInfo(mockUserInfo); }

上面的方法初始化了测试类的信息,方法调用的时候,我们获得对应的接口实例处理即可,如下测试代码所示。

///

/// 测试查找记录 /// /// [TestMethod] public async Task TestMethod1() { var input = new DictTypePagedDto() { Name = "客户" }; var service = Provider.GetService(); var count = await service.CountAsync(s => true); Assert.AreNotEqual(0, count); var list = await service.GetAllAsync(); Assert.IsNotNull(list); Assert.IsNotNull(list.Items); Assert.IsTrue(list.Items.Count > 0); list = await service.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 = await service.GetAllByIdsAsync(ids); Assert.IsNotNull(list); Assert.IsNotNull(list.Items); Assert.IsTrue(list.Items.Count > 0); var id = list.Items[0].Id; var info = await service.GetAsync(id); Assert.IsNotNull(info); Assert.AreEqual(id, info.Id); info = await service.GetFirstAsync(s => true); Assert.IsNotNull(info); await Task.CompletedTask; }

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:微信小程序传递参数以及接收数据的方法(微信小程序传递参数以及接收数据的方法是)
下一篇:小程序中页面兼容h5标签的解析(小程序html标签解析)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~