# CompanyManageAPI **Repository Path**: 2lin/CompanyManageAPI ## Basic Information - **Project Name**: CompanyManageAPI - **Description**: 仓储模式的WebAPI实现 - **Primary Language**: C# - **License**: AFL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2021-03-09 - **Last Updated**: 2022-07-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README --- 仓储模式的WebAPI实现 --- **主要技术:** - **加密方式:** JWT - **API 开发工具:** Swagger4.0.1 - **.netcore版本:** 2.2 - **模式:** 仓储模式 - **实例构造方式:** 注入 - **数据访问层:** Sqlsugar - **数据库:** sqllite **目录:** - 0 系统设计 - 1 项目初始化 - 2 配置swagger - 3 配置JWT Bearer - 4 仓储模式 - 5 依赖注入 - 6 结果预览 - 7 展望 # 0 系统设计 ## 1)数据库设计 员工表:Employee | 字段名 | 数据类型 | 备注 | | ---------- | :------- | -------- | | ID | vachar | 编号 | | NAME | vachar | 姓名 | | SEX | vachar | 性别 | | TITLE | vachar | 职称 | | AGE | number | 年龄 | | COMPANY_ID | vachar | 企业编号 | 企业表:Company | 字段名 | 数据类型 | | ------------ | :------- | | ID | vachar | | NAME | vachar | | INDUSTRYTYPE | vachar | ## 2)接口设计 1、Employee对象的增删改查 2、Company对象增删改查 3、Employee职称比例 4、根据公司名称查询公司内的员工 # 1、项目初始化 项目名称:CompanyManageAPI # 2、配置swagger 【爬坑】 如果提示:nuget.org 无法加载源 https://api.nuget.org/v3/index.json 的服务索引 解决办法:添加一个新的源,Nuget.org取消勾选 在新源中添加地址:https://www.nuget.org/api/v2/ ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160439_b895c14a_717356.png "1580867176274.png") ## 2.1 引用Nuget包 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160456_8e7ba1cb_717356.png "1580867480921.png") ## 2.2 配置服务 ```c# public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "CompanyManageAPI", Description = "说明文档", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "CompanyManage", Email = "", Url = "" } }); }); } ``` ## 2.3 启动Http中间件 ```c# app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1"); }); ``` ## 2.4 配置启动入口 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160543_65ee8136_717356.png "1580869253513.png") 【理解】 `ConfigureServices` 与 `Configure`可以理解为排兵布阵,`ConfigureServices` 是`排兵`,`Configure` 是`布阵` `ConfigureServices` 就是给工作岗位安排人员,`Configure` 是制定工作流程。 ## 2.5 结果 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160600_fb849f2e_717356.png "1580869889540.png") # 3、配置JWT Bearer ## 3.1 JWT Bearer原理 img ### 3.1.1 认证流程 用户首先通过认证服务器提供的登录系统(例如:用户名和密码)登录认证服务器。然后认证服务器生成 JWT 并返回给用户。当用户向应用服务器发起 API 请求时,会附带上 JWT。在这一步中,应用服务器会配置成去验证传入的 JWT 是否由认证服务器生成的(验证过程将在后面详细解释)。所以,当用户携带 JWT 发起 API 请求时,应用可以通过 JWT 来校验 API 请求是否来自认证的用户。 ### 3.1.2 数据结构 ```javascript //”type” 字段的值指明这个对象是个 JWT,”alg” 字段的值说明用什么 hash 算法来生成 JWT 签名模块。 Header={ "typ": "JWT", "alg": "HS256" } //JWT 中保存了用户的信息,如用户 ID。可以理解为这里是claims,userId是一个claim Payload={ "userId": "b08f86af-35da-48f2-8fab-cef3904660bd" } //签名 data = base64urlEncode( header ) + “.” + base64urlEncode( payload )//这些属于编码,不是加密! hashedData = hash( data, secret )//哈希算法是不可逆的,关键在于secret,此处属于对称加密,应用服务器和认证服务器知道 signature = base64urlEncode( hashedData ) JWT 的结构 base64urlEncode(header)+'.'+base64urlEncode(payload)+'.'+signature ``` 【注意】 - 由于 JWT 只是签名和编码数据,**没有加密!**,所有 JWT 不保证敏感数据的任何安全性。 - 签名中使用的是哈希算法,是不可逆的 ### 3.1.3 校验 我们使用的 JWT 是由 HS256 算法签名 —— 这个算法的密钥只有认证服务器和应用服务器知道。当应用服务器设置他的认证过程时,应用服务器从认证服务器那接收密钥。由于应用知道**密钥**,当用户向应用发起携带 JWT 的 API 请求时,应用可以执行和 创建 SIGNATURE一样的签名算法。然后应用可以校验它自己 hash 操作生成的签名是否和 JWT 中的签名匹配(即它匹配认证服务器创建的 JWT 签名)。如果签名匹配,意味着 JWT 是合法的,标明 API 请求是来自可靠的来源。否则,如果签名不匹配,意味着接收的 JWT 是无效的,这也可能说明应用遭受潜在的攻击。所以,通过校验 JWT,应用在和用户之间建立了信任。 ### 3.1.4 疑惑解析 1、jwt 与bearer认证的关系? bearer是认证方式,最流行的Token编码方式便是:json web joken(jwt) 2、claims是什么 payload中申明被JWT标准称为claims,它的一个“属性值对”其实就是一个claim 参考: **jwt:** https://juejin.im/entry/5bb211b7e51d450e5c478e0b **对称加密和非对称加密:** https://zhuanlan.zhihu.com/p/24812173 **Claim:** https://www.cnblogs.com/gaoliangchao/p/9923882.html ## 3.2 生产token令牌 颁发token的方法: ```c# public class JwtHelper { /// /// 颁发JWT字符串 /// /// /// public static string IssueJwt(TokenModelJwt tokenModel) { // 自己封装的 appsettign.json 操作类,看下文 string iss = Appsettings.app(new string[] { "Audience", "Issuer" }); string aud = Appsettings.app(new string[] { "Audience", "Audience" }); string secret = Appsettings.app(new string[] { "Audience", "Secret" }); var claims = new List { /* * 特别重要: 1、这里将用户的部分信息,比如 uid 存到了Claim 中,如果你想知道如何在其他地方将这个 uid从 Token 中取出来,请看下边的SerializeJwt() 方法,或者在整个解决方案,搜索这个方法,看哪里使用了! 2、你也可以研究下 HttpContext.User.Claims ,具体的你可以看看 Policys/PermissionHandler.cs 类中是如何使用的。 */ new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ToString()), new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , //这个就是过期时间,目前是过期1000秒,可自定义,注意JWT有自己的缓冲过期时间 new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Iss,iss), new Claim(JwtRegisteredClaimNames.Aud,aud), //new Claim(ClaimTypes.Role,tokenModel.Role),//为了解决一个用户多个角色(比如:Admin,System),用下边的方法 }; // 可以将一个用户的多个角色全部赋予; // 作者:DX 提供技术支持; claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); //秘钥 (SymmetricSecurityKey 对安全性的要求,密钥的长度太短会报出异常) var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken( issuer: iss, claims: claims, signingCredentials: creds); var jwtHandler = new JwtSecurityTokenHandler(); var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt; } /// /// 解析 /// /// /// public static TokenModelJwt SerializeJwt(string jwtStr) { var jwtHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr); object role; try { jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role); } catch (Exception e) { Console.WriteLine(e); throw; } var tm = new TokenModelJwt { Uid = (jwtToken.Id).ObjToInt(), Role = role != null ? role.ObjToString() : "", }; return tm; } } /// /// 令牌 /// public class TokenModelJwt { /// /// Id /// public long Uid { get; set; } /// /// 角色 /// public string Role { get; set; } /// /// 职能 /// public string Work { get; set; } } ``` 读取Appsettings的方法: ```c# public class Appsettings { static IConfiguration Configuration { get; set; } //static Appsettings() //{ // //ReloadOnChange = true 当appsettings.json被修改时重新加载 // Configuration = new ConfigurationBuilder() // .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true })//请注意要把当前appsetting.json 文件->右键->属性->复制到输出目录->始终复制 // .Build(); //} static Appsettings() { string Path = "appsettings.json"; { //如果你把配置文件 是 根据环境变量来分开了,可以这样写 //Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json"; } //Configuration = new ConfigurationBuilder() //.Add(new JsonConfigurationSource { Path = Path, ReloadOnChange = true })//请注意要把当前appsetting.json 文件->右键->属性->复制到输出目录->始终复制 //.Build(); Configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .Add(new JsonConfigurationSource { Path = Path, Optional = false, ReloadOnChange = true })//这样的话,可以直接读目录里的json文件,而不是 bin 文件夹下的,所以不用修改复制属性 .Build(); } /// /// 封装要操作的字符 /// /// /// public static string app(params string[] sections) { try { var val = string.Empty; for (int i = 0; i < sections.Length; i++) { val += sections[i] + ":"; } return Configuration[val.TrimEnd(':')]; } catch (Exception) { return ""; } } } ``` ## 3.3 JWT授权认证流程 ### 3.3.1 Swagger中开启JWT服务 #region和#endregion之间 ```c# public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "CompanyManageAPI", Description = "说明文档", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "CompanyManage", Email = "", Url = "" } }); #region Token绑定到ConfigureServices // 添加Header验证消息 // c.OperationFilter(); var security = new Dictionary> { { "CompanyManage", new string[] { } }, }; c.AddSecurityRequirement(security); // 方案名称“Blog.Core”可自定义,上下一致即可 c.AddSecurityDefinition("CompanyManage", new ApiKeyScheme { Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)\"", Name = "Authorization", // jwt默认的参数名称 In = "header", // jwt默认存放Authorization信息的位置(请求头中) Type = "apiKey" }); #endregion }); } ``` ### 3.3.2 API接口授权 控制器中,特性上增加 [Authorize(Policy = "Admin")]//这里 ```c# [HttpGet] [Authorize(Policy = "Admin")]//这里 public ActionResult> Get() { return new string[] { "value1", "value2" }; } ``` ConfigureService中增加权限策略 ```c# services.AddAuthorization(options => { options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());//单独角色 options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build()); options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System"));//或的关系 options.AddPolicy("SystemAndAdmin", policy => policy.RequireRole("Admin").RequireRole("System"));//且的关系 }); ``` 【注意】 services.AddAuthorization要在services.AddMvc前面 ### 3.3.3 配置认证服务 在ConfigureServices中添加 ```c# //读取配置文件 var audienceConfig = Configuration.GetSection("Audience"); var symmetricKeyAsBase64 = audienceConfig["Secret"]; var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); var signingKey = new SymmetricSecurityKey(keyByteArray); var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); //认证 services.AddAuthentication(x => { //看这个单词熟悉么?没错,就是上边错误里的那个。 x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; })// 也可以直接写字符串,AddAuthentication("Bearer") .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey,//参数配置在下边 ValidateIssuer = true, ValidIssuer = audienceConfig["Issuer"],//发行人 ValidateAudience = true, ValidAudience = audienceConfig["Audience"],//订阅人 ValidateLifetime = true, ClockSkew = TimeSpan.Zero, RequireExpirationTime = true, }; }); ``` ### 3.3.4 配置官方认证中间件 在 configure 中添加: ```c# app.UseAuthentication(); ``` ## 3.4 总结 - 产生jwt的服务器和认证的服务器不一定在同一台,需要有相同的秘钥(Secret) - 生产token令牌的方法本质上使用了System.IdentityModel.Tokens.Jwt命名空间下的JwtSecurityToken的构造方法 # 4、仓储模式 ## 4.1 为什么要用仓储模式 在控制器中用db操作数据不是很简单,方便,如下方代码 ```c# public class BooksController : Controller { private MyDbContext db = new MyDbContext(); // GET: Books public ActionResult Index() { return View(db.Books.ToList()); } // GET: Books/Delete/5 public ActionResult Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = db.Books.Find(id); if (book == null) { return HttpNotFound(); } return View(book); } } ``` - 在功能方面没什么错,但是db的操作分布在各个Controller或action中,这是后期维护的噩梦。如果要统一加缓存等。 - 控制器中暴露db对象,不同方法都在操作它,容易出问题 那么仓储模式就是用一个对象把db包一下 ## 4.2 创建实体Model数据层 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160708_66fb488b_717356.png "1580974622345.png") ## 4.3 仓储接口与其实现类 Repository就是一个管理数据持久层的,它负责数据的CRUD(Create, Read, Update, Delete) Repository 是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。 ### 4.3.1 IBaseRepository ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160721_e4f15b44_717356.png "1581135296232.png") ### 4.3.2 ICompanyRepository ```c# namespace CompanyManageIRepository { public interface ICompanyRepository:IBaseRepository { } } ``` 集成基础仓储接口 ### 4.3.3 IEmployeeRepository ```c# namespace CompanyManageIRepository { public interface IEmployeeRepository:IBaseRepository { } } ``` ### 4.3.4 BaseDBConfig 数据库连接参数配置 ```c# public class BaseDBConfig { public static string ConnectionString = @"DataSource = E:\MyProjects\CompanyManageAPI\CompanyManageRepository\CompanyManage.db"; public static SqlSugar.DbType DbType = SqlSugar.DbType.Sqlite; } ``` ### 4.3.5 DbContext 生产数据库操作类,使用Sqlsugar获取数据库连接对象,在类库中通过Nuget引入 sqlSugarCore,**一定是Core版本的**。 ### 4.3.6 BaseRepository 实现仓储接口的泛型增删改查 ### 4.3.7 CompanyRepository 实现仓储接口的泛型增删改查 ### 4.3.8 总结 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160739_5c519e8d_717356.png "1581140030705.png") 仓储模式:https://www.cnblogs.com/buyixiaohan/p/5432147.html ## 4.4 服务接口与其实现类 服务层通过操作仓储对象,实现对数据的操作 ### 4.4.1 IBaseServices ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160752_4f21a6bd_717356.png "1581140586999.png") ### 4.4.2 ICompanyServices ```c# public interface ICompanyServices : IBaseServices { } ``` ### 4.4.3 IEmployeeServices ```c# public interface IEmployeeServices: IBaseServices { Task> GetTitleCount(); } ``` ### 4.4.4 BaseServices 实现IBaseServices中的方法 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160810_2af1808a_717356.png "1581141347830.png") ### 4.4.5 CompanyServices ```c# public class CompanyServices:BaseServices, ICompanyServices { ICompanyRepository _dal; public CompanyServices(ICompanyRepository dal) { this._dal = dal; base.baseDal = dal; } } ``` ### 4.4.6 EmployeeServices ```c# public class EmployeeServices : BaseServices, IEmployeeServices { public async Task> GetTitleCount() { var EmployeeList = await baseDal.Query(); var group = EmployeeList.GroupBy(p => p.TITLE).ToList(); var resultList = new List(); for(int i=0;i< group.Count(); i++) { var result = new Result(); result.Title = group[i].Key; result.Count = group[i].Count(); resultList.Add(result); } return resultList; } } ``` ### 4.4.7 总结 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160829_8c5905e6_717356.png "1581141777614.png") ## 4.5 控制器 ### 4.5.1 企业控制器 以查询所有企业接口为例: ```c# [HttpGet()] public Task> GetAll() { ICompanyServices CompanyServices = new CompanyServices(); return CompanyServices.Query(); } ``` ### 4.5.2 总结 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160843_dac54c8b_717356.png "1581142332409.png") ## 4.6 仓储模式总结 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160859_bb2bc75e_717356.png "1581143288129.png") # 5、依赖注入 ## 5.1 什么是依赖注入 【个人理解】对象不是new 出来,而是通过注入来获取。 有什么好处,方便解耦 ## 5.2 实现 ### 5.2.1 注入 在ConfigureServices中注入对象 ```c# services.AddScoped(); services.AddScoped(); services.AddScoped(); ``` ### 5.2.2 获取实例 通构造器中获取 ```c# public class CompanyController : ControllerBase { readonly ICompanyServices _CompanyServices; public CompanyController(ICompanyServices CompanyServices) { _CompanyServices = CompanyServices; } } ``` ### 5.2.3 疑惑解答 1、获取规则是什么,构造器怎么知道传入的变量是要从容器中获取而不是要传参 答:觉得是通过识别数据类型实现的,这个数据类型被注入了,构造器中需要传入这个类型的话,自己会去容器中找。 2、嵌套注入的问题是怎么解决的,如CompanyServices注入到CompanyController中,而CompanyServices对象实例化还需要注入CompanyRepository。如下图所示 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160918_0d1133aa_717356.png "1581145631087.png") 答:不需要嵌套的问题,在构造器中检测到传入对象已经在容器中,就可以用了。 3、可以注入带参数的对象吗 可以带参数如: ```c# services.AddScoped(o => { return new SqlSugarClient(new ConnectionConfig() { ConnectionString = BaseDBConfig.ConnectionString, DbType = BaseDBConfig.DbType, IsAutoCloseConnection = true, IsShardSameThread = true, ConfigureExternalServices = new ConfigureExternalServices() { //DataInfoCacheService = new HttpRuntimeCache() }, MoreSettings = new ConnMoreSettings() { //IsWithNoLockQuery = true, IsAutoRemoveDataCache = true } }); }); ``` # 6、结果预览 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/161047_7fa2592d_717356.png "1581146305511.png") ![输入图片说明](https://images.gitee.com/uploads/images/2020/0208/160942_1b6aa6d2_717356.png "1581147556637.png") # 7、展望 - 解耦还未完全实现 - 跨对象的多表操作是在控制器中实现还是在业务层中实现暂时没想到好的解决方案