ABP入门系列(16)——通过webapi与系统进行交互

1. 引言

上一节我们讲解了如何创建微信公众号模块,这一节我们就继续跟进,来讲一讲公众号模块如何与系统进行交互。
微信公众号模块作为一个独立的web模块部署,要想与现有的【任务清单】进行交互,我们要想明白以下几个问题:

  1. 如何进行交互?
    ABP模板项目中默认创建了webapi项目,其动态webapi技术允许我们直接访问appservice作为webapi而不用在webapi层编写额外的代码。所以,自然而然我们要通过webapi与系统进行交互。
  2. 通过webapi与系统进行交互,如何确保安全?
    我们知道暴露的webapi如果不加以授权控制,就如同在大街上裸奔。所以在访问webapi时,我们需要通过身份认证来确保安全访问。
  3. 都有哪几种身份认证方式?
    第一种就是大家熟知的cookie认证方式;
    第二种就是token认证方式:在访问webapi之前,先要向目标系统申请令牌(token),申请到令牌后,再使用令牌访问webapi。Abp默认提供了这种方式;
    第三种是基于OAuth2.0的token认证方式:OAuth2.0是什么玩意?建议先看看OAuth2.0 知多少以便我们后续内容的展开。OAuth2.0认证方式弥补了Abp自带token认证的短板,即无法进行token刷新。

基于这一节,我完善了一个demo,大家可以直接访问http://shengjietest.azurewebsites.net/进行体验。

demo

下面我们就以【通过webapi请求用户列表】为例看一看三种认证方式的具体实现。

2. Cookie认证方式

Cookie认证方式的原理就是:在访问webapi之前,通过登录目标系统建立连接,将cookie写入本地。下一次访问webapi的时候携带cookie信息就可以完成认证。

2.1. 登录目标系统

这一步简单,我们仅需提供用户名密码,Post一个登录请求即可。
我们在微信模块中创建一个WeixinController

 

public class WeixinController : Controller
{private readonly IAbpWebApiClient _abpWebApiClient;private string baseUrl = "http://shengjie.azurewebsites.net/";private string loginUrl = "/account/login";private string webapiUrl = "/api/services/app/User/GetUsers";private string abpTokenUrl = "/api/Account/Authenticate";private string oAuthTokenUrl = "/oauth/token";private string user = "admin";private string pwd = "123qwe";public WeixinController(){_abpWebApiClient = new AbpWebApiClient();}
}

其中IAbpWebApiClient是对HttpClient的封装,用于发送 HTTP 请求和接收HTTP 响应。

下面添加CookieBasedAuth方法,来完成登录认证,代码如下:

 

public async Task CookieBasedAuth()
{Uri uri = new Uri(baseUrl + loginUrl);var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None, UseCookies = true };using (var client = new HttpClient(handler)){client.BaseAddress = uri;client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));var content = new FormUrlEncodedContent(new Dictionary<string, string>(){{"TenancyName", "Default"},{"UsernameOrEmailAddress", user},{"Password", pwd }});var result = await client.PostAsync(uri, content);string loginResult = await result.Content.ReadAsStringAsync();var getCookies = handler.CookieContainer.GetCookies(uri);foreach (Cookie cookie in getCookies){_abpWebApiClient.Cookies.Add(cookie);}}
}

这段代码中有几个点需要注意:

  1. 指定HttpClientHandler属性UseCookie = true,使用Cookie;
  2. client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));用来指定接受的返回值;
  3. 使用FormUrlEncodedContent进行传参;
  4. 使用var getCookies = handler.CookieContainer.GetCookies(uri);获取返回的Cookie,并添加到_abpWebApiClient.Cookies的集合中,以便下次直接携带cookie信息访问webapi。

2.2. 携带cookie访问webapi

服务器返回的cookie信息在登录成功后已经填充到_abpWebApiClient.Cookies中,我们只需post一个请求到目标api即可。

 

public async Task<PartialViewResult> SendRequestBasedCookie()
{await CookieBasedAuth();return await GetUserList(baseUrl + webapiUrl);
}private async Task<PartialViewResult> GetUserList(string url)
{try{var users = await _abpWebApiClient.PostAsync<ListResultDto<UserListDto>>(url);return PartialView("_UserListPartial", users.Items);}catch (Exception e){ViewBag.ErrorMessage = e.Message;}return null;
}

3. Token认证方式

Abp默认提供的token认证方式,很简单,我们仅需要post一个请求到/api/Account/Authenticate即可请求到token。然后使用token即可请求目标webapi。
但这其中有一个问题就是,如果token过期,就必须使用用户名密码重写申请token,体验不好。

3.1. 请求token

 

public async Task<string> GetAbpToken()
{var tokenResult = await _abpWebApiClient.PostAsync<string>(baseUrl + abpTokenUrl, new{TenancyName = "Default",UsernameOrEmailAddress = user,Password = pwd});this.Response.SetCookie(new HttpCookie("access_token", tokenResult));return tokenResult;
}

这段代码中我们将请求到token直接写入到cookie中。以便我们下次直接从cookie中取回token直接访问webapi。

3.2. 使用token访问webapi

从cookie中取回token,在请求头中添加Authorization = Bearer token,即可。

 

public async Task<PartialViewResult> SendRequest()
{var token = Request.Cookies["access_token"]?.Value;//将token添加到请求头_abpWebApiClient.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token));return await GetUserList(baseUrl + webapiUrl);
}

这里面需要注意的是,abp中配置app.UseOAuthBearerAuthentication(AccountController.OAuthBearerOptions);使用的是Bearer token,所以我们在请求weiapi时,要在请求头中假如Authorization信息时,使用Bearer token的格式传输token信息(Bearer后有一个空格!)。

4. OAuth2.0 Token认证方式

OAuth2.0提供了token刷新机制,当服务器颁发的token过期后,我们可以直接通过refresh_token来申请token即可,不需要用户再录入用户凭证申请token。

4.1. Abp集成OAuth2.0

在WebApi项目中的Api路径下创建Providers文件夹,添加SimpleAuthorizationServerProviderSimpleRefreshTokenProvider类。
其中SimpleAuthorizationServerProvider用来验证客户端的用户名和密码来颁发token;SimpleRefreshTokenProvider用来刷新token。

 

public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider, ITransientDependency
{private readonly LogInManager _logInManager;public SimpleAuthorizationServerProvider(LogInManager logInManager){_logInManager = logInManager;}public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context){string clientId;string clientSecret;if (!context.TryGetBasicCredentials(out clientId, out clientSecret)){context.TryGetFormCredentials(out clientId, out clientSecret);}var isValidClient = string.CompareOrdinal(clientId, "app") == 0 &&string.CompareOrdinal(clientSecret, "app") == 0;if (isValidClient){context.OwinContext.Set("as:client_id", clientId);context.Validated(clientId);}else{context.SetError("invalid client");}return Task.FromResult<object>(null);}public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context){var tenantId = context.Request.Query["tenantId"];var result = await GetLoginResultAsync(context, context.UserName, context.Password, tenantId);if (result.Result == AbpLoginResultType.Success){//var claimsIdentity = result.Identity;                var claimsIdentity = new ClaimsIdentity(result.Identity);claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));var ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());context.Validated(ticket);}}public override  Task GrantRefreshToken(OAuthGrantRefreshTokenContext context){var originalClient = context.OwinContext.Get<string>("as:client_id");var currentClient = context.ClientId;// enforce client binding of refresh tokenif (originalClient != currentClient){context.Rejected();return Task.FromResult<object>(null);}// chance to change authentication ticket for refresh token requestsvar newId = new ClaimsIdentity(context.Ticket.Identity);newId.AddClaim(new Claim("newClaim", "refreshToken"));var newTicket = new AuthenticationTicket(newId, context.Ticket.Properties);context.Validated(newTicket);return Task.FromResult<object>(null);}private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(OAuthGrantResourceOwnerCredentialsContext context,string usernameOrEmailAddress, string password, string tenancyName){var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);switch (loginResult.Result){case AbpLoginResultType.Success:return loginResult;default:CreateExceptionForFailedLoginAttempt(context, loginResult.Result, usernameOrEmailAddress, tenancyName);//throw CreateExceptionForFailedLoginAttempt(context,loginResult.Result, usernameOrEmailAddress, tenancyName);return loginResult;}}private void CreateExceptionForFailedLoginAttempt(OAuthGrantResourceOwnerCredentialsContext context, AbpLoginResultType result, string usernameOrEmailAddress, string tenancyName){switch (result){case AbpLoginResultType.Success:throw new ApplicationException("Don't call this method with a success result!");case AbpLoginResultType.InvalidUserNameOrEmailAddress:case AbpLoginResultType.InvalidPassword:context.SetError(L("LoginFailed"), L("InvalidUserNameOrPassword"));break;//    return new UserFriendlyException(("LoginFailed"), ("InvalidUserNameOrPassword"));case AbpLoginResultType.InvalidTenancyName:context.SetError(L("LoginFailed"), L("ThereIsNoTenantDefinedWithName", tenancyName));break;//    return new UserFriendlyException(("LoginFailed"), string.Format("ThereIsNoTenantDefinedWithName{0}", tenancyName));case AbpLoginResultType.TenantIsNotActive:context.SetError(L("LoginFailed"), L("TenantIsNotActive", tenancyName));break;//    return new UserFriendlyException(("LoginFailed"), string.Format("TenantIsNotActive {0}", tenancyName));case AbpLoginResultType.UserIsNotActive:context.SetError(L("LoginFailed"), L("UserIsNotActiveAndCanNotLogin", usernameOrEmailAddress));break;//    return new UserFriendlyException(("LoginFailed"), string.Format("UserIsNotActiveAndCanNotLogin {0}", usernameOrEmailAddress));case AbpLoginResultType.UserEmailIsNotConfirmed:context.SetError(L("LoginFailed"), L("UserEmailIsNotConfirmedAndCanNotLogin"));break;//    return new UserFriendlyException(("LoginFailed"), ("UserEmailIsNotConfirmedAndCanNotLogin"));//default: //Can not fall to default actually. But other result types can be added in the future and we may forget to handle it//    //Logger.Warn("Unhandled login fail reason: " + result);//    return new UserFriendlyException(("LoginFailed"));}}private static string L(string name, params object[] args){//return new LocalizedString(name);return IocManager.Instance.Resolve<ILocalizationService>().L(name, args);}
}

 

public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider, ITransientDependency
{private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();public Task CreateAsync(AuthenticationTokenCreateContext context){var guid = Guid.NewGuid().ToString("N");// maybe only create a handle the first time, then re-use for same client// copy properties and set the desired lifetime of refresh tokenvar refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary){IssuedUtc = context.Ticket.Properties.IssuedUtc,ExpiresUtc = DateTime.UtcNow.AddYears(1)};var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);//_refreshTokens.TryAdd(guid, context.Ticket);_refreshTokens.TryAdd(guid, refreshTokenTicket);// consider storing only the hash of the handlecontext.SetToken(guid);return Task.FromResult<object>(null);}public Task ReceiveAsync(AuthenticationTokenReceiveContext context){AuthenticationTicket ticket;if (_refreshTokens.TryRemove(context.Token, out ticket)){context.SetTicket(ticket);}return Task.FromResult<object>(null);}public void Create(AuthenticationTokenCreateContext context){throw new NotImplementedException();}public void Receive(AuthenticationTokenReceiveContext context){throw new NotImplementedException();}
}

以上两段代码我就不做过多解释,请自行走读。

紧接着我们在Api目录下创建OAuthOptions类用来配置OAuth认证。

 

public class OAuthOptions
{/// <summary>/// Gets or sets the server options./// </summary>/// <value>The server options.</value>private static OAuthAuthorizationServerOptions _serverOptions;/// <summary>/// Creates the server options./// </summary>/// <returns>OAuthAuthorizationServerOptions.</returns>public static OAuthAuthorizationServerOptions CreateServerOptions(){if (_serverOptions == null){var provider = IocManager.Instance.Resolve<SimpleAuthorizationServerProvider>();var refreshTokenProvider = IocManager.Instance.Resolve<SimpleRefreshTokenProvider>();_serverOptions = new OAuthAuthorizationServerOptions{TokenEndpointPath = new PathString("/oauth/token"),Provider = provider,RefreshTokenProvider = refreshTokenProvider,AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(30),AllowInsecureHttp = true};}return _serverOptions;}
}

从中我们可以看出,主要配置了以下几个属性:

  • TokenEndpointPath :用来指定请求token的路由;
  • Provider:用来指定创建token的Provider;
  • RefreshTokenProvider:用来指定刷新token的Provider;
  • AccessTokenExpireTimeSpan :用来指定token过期时间,这里我们指定了30s,是为了demo 如何刷新token。
  • AllowInsecureHttp:用来指定是否允许http连接。

创建上面三个类之后,我们需要回到Web项目的Startup类中,配置使用集成的OAuth2.0,代码如下:

 

public void Configuration(IAppBuilder app)
{//第一步:配置跨域访问app.UseCors(CorsOptions.AllowAll);app.UseOAuthBearerAuthentication(AccountController.OAuthBearerOptions);//第二步:使用OAuth密码认证模式app.UseOAuthAuthorizationServer(OAuthOptions.CreateServerOptions());//第三步:使用Abpapp.UseAbp();//省略其他代码
}

其中配置跨越访问时,我们需要安装Microsoft.Owin.CorsNuget包。

至此,Abp集成OAuth的工作完成了。

4.2. 申请OAuth token

我们在Abp集成OAuth配置的申请token的路由是/oauth/token,所以我们将用户凭证post到这个路由即可申请token:

 

public async Task<string> GetOAuth2Token()
{Uri uri = new Uri(baseUrl + oAuthTokenUrl);var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None };using (var client = new HttpClient(handler)){client.BaseAddress = uri;client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));var content = new FormUrlEncodedContent(new Dictionary<string, string>(){{"grant_type", "password"},{"username", user },{"password", pwd },{"client_id", "app" },{"client_secret", "app"},});//获取token保存到cookie,并设置token的过期日期                    var result = await client.PostAsync(uri, content);string tokenResult = await result.Content.ReadAsStringAsync();var tokenObj = (JObject)JsonConvert.DeserializeObject(tokenResult);string token = tokenObj["access_token"].ToString();string refreshToken = tokenObj["refresh_token"].ToString();long expires = Convert.ToInt64(tokenObj["expires_in"]);this.Response.SetCookie(new HttpCookie("access_token", token));this.Response.SetCookie(new HttpCookie("refresh_token", refreshToken));this.Response.Cookies["access_token"].Expires = Clock.Now.AddSeconds(expires);return tokenResult;}
}

在这段代码中我们指定的grant_type = password,这说明我们使用的是OAuth提供的密码认证模式。其中{"client_id", "app" }, {"client_secret", "app"}(搞过微信公众号开发的应该对这个很熟悉)用来指定客户端的身份和密钥,这边我们直接写死。
通过OAuth的请求的token主要包含四部分:

  • token:令牌
  • refreshtoken:刷新令牌
  • expires_in:token有效期
  • token_type:令牌类型,我们这里是Bearer

为了演示方便,我们直接把token信息直接写入到cookie中,实际项目中建议写入数据库。

4.3. 刷新token

如果我们的token过期了怎么办,咱们可以用refresh_token来重新获取token。

 

public async Task<string> GetOAuth2TokenByRefreshToken(string refreshToken)
{Uri uri = new Uri(baseUrl + oAuthTokenUrl);var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None, UseCookies = true };using (var client = new HttpClient(handler)){client.BaseAddress = uri;client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));var content = new FormUrlEncodedContent(new Dictionary<string, string>(){{"grant_type", "refresh_token"},{"refresh_token", refreshToken},{"client_id", "app" },{"client_secret", "app"},});//获取token保存到cookie,并设置token的过期日期                    var result = await client.PostAsync(uri, content);string tokenResult = await result.Content.ReadAsStringAsync();var tokenObj = (JObject)JsonConvert.DeserializeObject(tokenResult);string token = tokenObj["access_token"].ToString();string newRefreshToken = tokenObj["refresh_token"].ToString();long expires = Convert.ToInt64(tokenObj["expires_in"]);this.Response.SetCookie(new HttpCookie("access_token", token));this.Response.SetCookie(new HttpCookie("refresh_token", newRefreshToken));this.Response.Cookies["access_token"].Expires = Clock.Now.AddSeconds(expires);return tokenResult;}
}

这段代码较直接使用用户名密码申请token的差别主要在参数上,{"grant_type", "refresh_token"},{"refresh_token", refreshToken}

4.4. 使用token访问webapi

有了token,访问webapi就很简单了。

 

public async Task<ActionResult> SendRequestWithOAuth2Token()
{var token = Request.Cookies["access_token"]?.Value;if (token == null){//throw new Exception("token已过期");string refreshToken = Request.Cookies["refresh_token"].Value;var tokenResult = await GetOAuth2TokenByRefreshToken(refreshToken);var tokenObj = (JObject)JsonConvert.DeserializeObject(tokenResult);token = tokenObj["access_token"].ToString();}_abpWebApiClient.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token));return await GetUserList(baseUrl + webapiUrl);
}

这段代码中,我们首先从cookie中取回access_token,若access_token为空说明token过期,我们就从cookie中取回refresh_token重新申请token。然后构造一个Authorization将token信息添加到请求头即可访问目标webapi。

5. 总结

本文介绍了三种不同的认证方式进行访问webapi,并举例说明。文章不可能面面俱到,省略了部分代码,请直接参考源码。若有纰漏之处也欢迎大家留言指正。

本文主要参考自以下文章:
使用OAuth打造webapi认证服务供自己的客户端使用
ABP中使用OAuth2(Resource Owner Password Credentials Grant模式)
Token Based Authentication using ASP.NET Web API 2, Owin, and Identity



作者:圣杰
链接:https://www.jianshu.com/p/d14733432dc2
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438933.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python嵩天第二版第五章_如何避免从入门到放弃——python小组学习复盘

2019年春节python学习行动复盘2019-02-09为了主攻python&#xff0c;没有参加心理学晨读。对心理学也不敢兴趣&#xff0c;怕耽误学习python的时间。那么没学习心理学的情况下&#xff0c;python学的怎么样&#xff1f;是否达到自己的预期&#xff1f;一、预期目标&#xff1a;…

ABP入门系列(17)——使用ABP集成的邮件系统发送邮件

1.Abp集成的邮件模块是如何实现的 ABP中对邮件的封装主要集成在Abp.Net.Mail和Abp.Net.Mail.Smtp命名空间下&#xff0c;相应源码在此。 分析可以看出主要由以下几个核心类组成&#xff1a; EmailSettingNames&#xff1a;静态常量类&#xff0c;主要定义了发送邮件需要的相关…

cdn转发防攻击_高防CDN和高防服务器的区别?

越来越多的网络攻击需要处理&#xff0c;而高防CDN和高防服务器是很好的选择&#xff0c;那么如何选择呢&#xff1f;我们就来分析一下关于这两者之间的选择。首先从价格上看的话&#xff0c;高防御CDN的价格相对高一些&#xff0c;防御上看&#xff0c;高防御CDN的防御效果也更…

ABP入门系列(18)—— 使用领域服务

1.引言 自上次更新有一个多月了&#xff0c;发现越往下写&#xff0c;越不知如何去写。特别是当遇到DDD中一些概念术语的时候&#xff0c;尤其迷惑。如果只是简单的去介绍如何去使用ABP&#xff0c;我只需参照官方文档&#xff0c;实现到任务清单Demo中去就可以了&#xff0c;…

mysql文件类型_MyCat教程:实现MySql主从复制

原文&#xff1a;http://iii75.cn/mwQhBW 作者&#xff1a;波波烤鸭历史相关文章Mycat入门教程单个mysql数据库在处理业务的时候肯定是有限的&#xff0c;这时我们扩展数据库的第一种方式就是对数据库做读写分离(主从复制),本文我们就先来介绍下怎么来实现mysql的主从复制操作。…

截屏当前界面_电脑屏幕怎么截取,常见的几种电脑截屏方法

随着科技的快速发展电脑已经逐渐渗入到我们的工作和生活中&#xff0c;我们需要使用电脑的地方也越来越多&#xff0c;电脑已经成为了一种新式的办公工具。今天小编不是向大家介绍电脑的应用&#xff0c;而是想要和大家分享一下关于电脑截图的几种方法。1、Print Screen SysRqP…

ABP入门系列(19)——使用领域事件

1.引言 最近刚学习了下DDD中领域事件的理论知识&#xff0c;总的来说领域事件主要有两个作用&#xff0c;一是解耦&#xff0c;二是使用领域事件进行事务的拆分&#xff0c;通过引入事件存储&#xff0c;来实现数据的最终一致性。若想了解DDD中领域事件的概念&#xff0c;可参…

扩容是元素还是数组_Java中对数组的操作

数组对于每一门编程语言来说都是重要的数据结构之一&#xff0c;当然不同语言对于数组的实现及处理也不尽相同。Java语言中提供的数组是用来存储固定大小的同类型元素。如&#xff1a;声明一个数组变量&#xff0c;numbers[100]来代替直接声明100个独立变量number0,number1,...…

ABP入门系列(20)——使用后台作业和工作者

1.引言 说到后台作业&#xff0c;你可能条件反射的想到BackgroundWorker&#xff0c;但后台作业并非是后台任务&#xff0c;后台作业用一种队列且持久稳固的方式安排一些待执行后台任务。 为执行长时间运行的任务而用户无需等待&#xff0c;以提高用户体验。为创建可重试且持…

加载中_GIS地图在项目中的加载显示

下面我们就来说说如何在应用程序中加载显示GIS地图&#xff0c;首先我们在SuperMap iDesktop 9D(10i)中编辑好我们需要的地图&#xff0c;如下图所示&#xff1a;如上图所示&#xff0c;这是我编辑好的一幅天河区的地图&#xff0c;下面我就以这幅地图为例来说说如何把这样一幅…

ABP入门系列(21)——切换MySQL数据库

1. 引言 Abp支持MySql已经不是什么新鲜事了&#xff0c;但按照官方文档&#xff1a;Entity Framework - MySql Integration来&#xff0c;你未必能成功切换&#xff0c;本文就记录下切换MySql数据库遇到的一些坑&#xff0c;供后人乘凉&#xff01; 2. 环境准备 MySql数据库…

ABP开发框架前后端开发系列---(1)框架的总体介绍

ABP是ASP.NET Boilerplate的简称&#xff0c;ABP是一个开源且文档友好的应用程序框架。ABP不仅仅是一个框架&#xff0c;它还提供了一个最徍实践的基于领域驱动设计(DDD)的体系结构模型。学习使用ABP框架也有一段时间了,一直想全面了解下这个框架的整个来龙去脉&#xff0c;并把…

c++ pdflib输出表格_DescrTab2包,输出SCI级别的描述统计表

今天浏览R包&#xff0c;发现一个不错的包——DescrTab2包。看R包介绍&#xff0c;这个包可以绘制出版物质量级别的描述统计表。看起来很不错。下面来学习下。1. R包安装和加载install.packages("DescrTab2") # 安装包library(DescrTab2) # 加载包2. 加载演示数据集l…

服务器怎么控制忽略样式_使用JavaScript来编写你的CSS样式代码——JSS

介绍JSS是CSS的创作工具&#xff0c;它允许你使用JavaScript以声明&#xff0c;无冲突和可重用的方式描述样式。它可以在浏览器&#xff0c;服务器端或在构建时在Node中编译。JSS与框架无关。它由多个包组成&#xff1a;核心部分&#xff0c;插件以及框架集成等。Githubhttps:/…

Java设计模式、框架、架构、平台之间的关系

1、设计模式 为什么要先说设计模式?因为设计模式在这些概念中是最基本的&#xff0c;而且也比较简单。那么什么是设计模式呢?说的直白点&#xff0c;设计模式就是告诉你针对特定问题如何组织类、对象和接口之间的关系&#xff0c;是前人总结的经验。比如我要在代码中实现一个…

如何学习(记住)linux命令(常用选项)

作者&#xff1a;林果皞 链接&#xff1a;https://www.zhihu.com/question/21690166/answer/66721478 来源&#xff1a;知乎 著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 Unix & Linux 命令行特别之处在于&#xff0c;一些选项的设…

增效工具_【危中寻机】降本增效生存之道 运用IE基础工具提升制造效率

效率提升的利器工业工程IE作为一门学科诞生于美国&#xff0c;却首先在日本得到了最大程度的践行与推广&#xff0c;成为了丰田生产方式TPS及精益制造LP的核心现场IE中的4大核心(工程分析、动作分析、时间分析、布局分析)仍是所有IE的入门工具&#xff0c;被笔者称为“基础IE”…

as将安卓应用打包_Android Studio打包生成apk的方法(超级简单哦)

释放双眼&#xff0c;带上耳机&#xff0c;听听看~&#xff01;打包文件是需要生成APK文件&#xff0c;其他人可以通过APK安装和使用&#xff0c;一般来说&#xff0c;包是指APK生成的发布版本&#xff0c;下文技术狗小编还介绍了Android Studio 超级简单的打包生成apk的方法&a…

Linux中常用的命令都是哪些单词的缩写

作者&#xff1a;蓬岸 Dr.Quest 链接&#xff1a;https://www.zhihu.com/question/49073893/answer/114986798 来源&#xff1a;知乎 著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 417个命令缩写&#xff1a;https://www.abbreviations.co…

ubuntu mysql 防火墙_mysql、ubuntu系统防火墙常规操作

mysql、ubuntu系统防火墙常规操作编辑&#xff1a;006 时间&#xff1a;2020-02-11mysql&#xff1a;数据库操作连接数据库&#xff1a;mysql -u username -p创建数据库&#xff1a;create database databasename;删除数据库&#xff1a;drop database databasename;指定数…