spring cloud+.net core搭建微服务架构:Api授权认证(六)

前言

这篇文章拖太久了,因为最近实在太忙了,加上这篇文章也非常长,所以花了不少时间,给大家说句抱歉。好,进入正题。目前的项目基本都是前后端分离了,前端分Web,Ios,Android。。。,后端也基本是Java,.NET的天下,后端渲染页面的时代已经一去不复返,当然这是时代的进步。前端调用后端服务目前大多数基于JSON的HTTP服务,那么就引入的我们今天的内容。客户端访问服务的时候怎么保证安全呢?很多同学都听说过OAuth2.0,都知道这个是用来做第三方登录的,实际上它也可以用来做Api的认证授权。不懂OAuth的同学可以先去看看阮一峰的OAuth的讲解,如果你看不懂的话,那就对了,笔者当初也看了很久,结合实际项目才明白。这章我会结合具体的例子帮助大家理解。同时也也会结合前几章的内容做一个整合,让大家对微服务架构以及API授权有一个更清晰的认识。

业务场景

Api的认证授权,在微服务体系里面它也是一个服务,我们叫做认证授权中心。同时我们再提供一个用户中心和订单中心,构建我们的业务场景。我们模拟一个用户(客户端)是怎么一步一步获取我们的订单数据的,同时也结合前几张的内容搭建一个相对完整的微服务架构的demo。

程序清单列表

  • 服务中心

  • API网关

  • 认证授权中心

  • 用户中心

  • 订单中心

    用户中心和认证授权中心有耦合的情况,访问认证授权的时候要去验证用户的账号密码是否合法

下图是一个简单的架构草图

服务中心和API网关大家看之前的文章来搭建,也可以直接看github上的源代码,没有什么变化。

认证授权中心

一直在说Ids4(IdentityServer4)这个框架,它实际上是一个实现了OAuth+OIDC(OpenId Connect)这两个功能的解决方案。那么OAuth和OIDC又到底是什么东西呢?简单来说OAuth就是帮助我们做授权获取token的,而OIDC就是帮助我们做认证这个token合法性的。一个完整的授权认证系统应该包含这两个功能。那么我们再谈一谈token,Ids4提供2种完全不一样的token加密方式,一种是JWT另一种叫Reference。那么这两种加密方式有何不同呢?JWT就是对这个字符串的一个加密算法,这个字符串包含了用户信息,客户端可以直接解析token,拿到用户信息,不需要和认证服授权务器去交互(程序首次加载的时候交互一次)。Reference更像Session,需要和认证服务器交互,由认证授权服务器去验证是否合法,每一次访问都需要和认证服务器进行交互,并且用户信息也是通过认证成功以后返回的。这两种方式各有优缺点。
JWT是一种加密方式,那么认证服务器不需要对token进行存储,而客户端也不需要找服务端验证,那么对于程序的性能是有很大的提升的,也不用考虑分布式和存储的问题,但是对于生成的token没办法控制,只能通过时效性来过期。
Reference的方式,token需要考虑分布式的存储,而且客户端需要一直和服务端认证,有一定的性能损耗,但是服务端可以对token进行控制,比如登出用户,修改密码都可以作废掉已经生成的token,这个时候再拿这个token是没办法使用的。然而不管是APP还是WEB让用户主动登出操作这是一个非常伪的需求,实际上即使是Reference方式token依然靠时效性来控制。
那么问题来了,当你的上级不懂技术的时候,问你万一我的token泄露了怎么办?你可以这样回答他。如果是在传输过程中的泄露,那么我们可以通过HTTPS的方式加密。程序代码里面用户相关的操作,都应该对传递的UserId参数和token里面解析出来UserId进行比较,如果出现不一致,那么这一定是一个非法请求。例如张三拿着李四的token去修改密码,肯定是修改不成功的。如果是在用户的客户端(WEB,APP)就把token泄露了,那么这个实际上这个客户端已经不止token泄露这么简单了,包括他所有的用户信息都泄露了,这个时候token已经没有了意义。就好比腾讯QQ加密算法做的如何如何牛逼,但是你泄露了你的QQ号和密码...
我们可以在过期时间上尽量短一点,客户端通过刷新token的方式不断获取新的token,而达到用户不用重复的登录,就能一直访问API接口。
至于两种方式的安全性我觉得都一样,微服务中我更倾向JWT这种方式,简单,高效。下面的代码我会模拟这两种模式,至于具体选择哪种方式大家根据实际的业务需求来。

小插曲:和几位技术大牛经过激烈的讨论,大家一致认为服务与服务之间的通信也是需要认证的,这样虽然增加了一定的性能损耗但是却更加的安全。我觉得有句话说的非常好,原则上内部其它系统都是不可信的。所以微服务之间的访问也得认证。

Reference方式的token,Ids4默认采用的内存做存储,也提供了EF for MS SQL 做分布式存储,而我们这里并不采用这种方式,我们采用redis来作为token的存储。

添加nuget引用
<PackageReference Include="Foundatio.Redis" Version="5.1.1478" />
<PackageReference Include="IdentityServer4" Version="2.0.2" />
<PackageReference Include="Pivotal.Discovery.Client" Version="1.1.0" />
Config.cs

配置Client信息,我们创建2个Client,一个采用JWT,一个采用Reference方式

new Client
{ClientId = "client.jwt",ClientSecrets ={        new Secret("AB2DC090-0125-4FB8-902A-34AFB64B7D9B".Sha256())},AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,AllowOfflineAccess = true,AccessTokenLifetime = accessTokenLifetime,AllowedScopes ={        "api1"},AccessTokenType =AccessTokenType.Jwt
}new Client
{ClientId = "client.reference",ClientSecrets ={        new Secret("A30E6E57-086C-43BE-AF79-67ADECDA0A5B".Sha256())},AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,AllowOfflineAccess = true,AccessTokenLifetime = accessTokenLifetime,AllowedScopes ={        "api1"},AccessTokenType =AccessTokenType.Reference 
},
RedisPersistedGrantStore.cs

实现IPersistedGrantStore接口来支持redis

public class RedisPersistedGrantStore : IPersistedGrantStore{      private readonly ICacheClient _cacheClient;  
 private readonly IConfiguration _configuration;
 
  public RedisPersistedGrantStore(ICacheClient cacheClient, IConfiguration configuration)    {_cacheClient = cacheClient;_configuration = configuration;}
  
   public Task StoreAsync(PersistedGrant grant)    {  
         var accessTokenLifetime = double.Parse(_configuration.GetConnectionString("accessTokenLifetime"));      
           var timeSpan = TimeSpan.FromSeconds(accessTokenLifetime);_cacheClient?.SetAsync(grant.Key, grant, timeSpan);        return Task.CompletedTask;}    
  
  public Task<PersistedGrant> GetAsync(string key)    {      
    if (_cacheClient.ExistsAsync(key).Result){          
     var ss = _cacheClient.GetAsync<PersistedGrant>(key).Result;            return Task.FromResult<PersistedGrant>(_cacheClient.GetAsync<PersistedGrant>(key).Result.Value);}      
       return Task.FromResult<PersistedGrant>((PersistedGrant)null);}  
  
    public Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId){      
     var persistedGrants = _cacheClient.GetAllAsync<PersistedGrant>().Result.Values;        return Task.FromResult<IEnumerable<PersistedGrant>>(persistedGrants.Where(x => x.Value.SubjectId == subjectId).Select(x => x.Value));}    
     
   public Task RemoveAsync(string key)    {_cacheClient?.RemoveAsync(key);    
      return Task.CompletedTask;}  
  
   public Task RemoveAllAsync(string subjectId, string clientId)    {_cacheClient.RemoveAllAsync();        
   return Task.CompletedTask;}  
   
    public Task RemoveAllAsync(string subjectId, string clientId, string type)    {        
    var persistedGrants = _cacheClient.GetAllAsync<PersistedGrant>().Result.Values.Where(x => x.Value.SubjectId == subjectId && x.Value.ClientId == clientId &&x.Value.Type == type).Select(x => x.Value);        foreach (var item in persistedGrants){_cacheClient?.RemoveAsync(item.Key);}        
    return Task.CompletedTask;} }
ResourceOwnerPasswordValidator.cs

实现IResourceOwnerPasswordValidator接口实现自定义的用户验证逻辑

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator{   
 private readonly DiscoveryHttpClientHandler _handler;  
  private const string UserApplicationName = "user";    
  
  public ResourceOwnerPasswordValidator(IDiscoveryClient client)    {_handler = new DiscoveryHttpClientHandler(client);}  
  
   public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)    {        //调用用户中心的验证用户名密码接口var client = new HttpClient(_handler);    
      var url = $"http://{UserApplicationName}/search?name={context.UserName}&password={context.Password}";      
        var result = await client.GetAsync(url);    
        if (result.IsSuccessStatusCode){            var user = await result.Content.ReadAsObjectAsync<dynamic>();        
        var claims = new List<Claim>() { new Claim("role", user.role.ToString()) };context.Result = new GrantValidationResult(user.id.ToString(), OidcConstants.AuthenticationMethods.Password, claims);}      
        else{context.Result = new GrantValidationResult(null);}} }
var claims = new List<Claim>() { new Claim("key", "value") };
这里可以传递自定义的用户信息,在客户端通过User.Claims.FirstOrDefault(x => x.Type == "key")来获取

这里需要注意一下,因为这里走的是http所以,授权服务中心和用户中心存在耦合,我个人建议如果走JWT的方式,用户中心和认证授权中心可以合并成一个服务,如果采用Reference的方式,建议还是拆分。


Startup.cs
public void ConfigureServices(IServiceCollection services){services.AddDiscoveryClient(Configuration);   
 var redisconnectionString = Configuration.GetConnectionString("RedisConnectionString");  
  var config = new Config(Configuration);services.AddMvc();services.AddIdentityServer({x.IssuerUri = "http://identity";x.PublicOrigin = "http://identity";}).AddDeveloperSigningCredential().AddInMemoryPersistedGrants().AddInMemoryApiResources(config.GetApiResources()).AddInMemoryClients(config.GetClients());services.AddSingleton(ConnectionMultiplexer.Connect(redisconnectionString));services.AddTransient<ICacheClient, RedisCacheClient>();//注入redisservices.AddSingleton<IPersistedGrantStore, RedisPersistedGrantStore>();services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>(); }
  public void Configure(IApplicationBuilder app, IHostingEnvironment env){    if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseMvc();app.UseDiscoveryClient();app.UseIdentityServer();}

因为是采用服务发现的方式,所以我们这里要修改IssuerUri和PublicOrigin。不要让发现服务暴露自己的具体URL地址,否则这里就负载不均衡了。

appsettings.json
"ConnectionStrings": {      
 "RedisConnectionString": "localhost",        "AccessTokenLifetime": 3600 //token过期时间 单位秒},    "spring": {        "application": {            "name": "identity"}},    "eureka": {        "client": {            "serviceUrl": "http://localhost:5000/eureka/"},        "instance": {          "port": 8010}}

用户中心

用户中心主要实现2个接口,一个给授权中心验证用户使用,还有一个是给客户端登录的时候返回token使用

nuget引用
<PackageReference Include="IdentityModel" Version="2.14.0" />
<PackageReference Include="Pivotal.Discovery.Client" Version="1.1.0" />
appsettings.json
{  "spring": {  
 "application": {      "name": "user"}},  "eureka": {    "client": {      "serviceUrl": "http://localhost:5000/eureka/"},    "instance": {      "port": 8040,      "hostName": "localhost"}},  "IdentityServer": {//jwt    "ClientId": "client.jwt",    "ClientSecrets": "AB2DC090-0125-4FB8-902A-34AFB64B7D9B"//reference//"ClientId": "client.reference",//"ClientSecrets": "A30E6E57-086C-43BE-AF79-67ADECDA0A5B"} }  
ValuesController.cs
[Route("/")]
public class ValuesController : Controller{  
   private const string IdentityApplicationName = "identity";             private readonly DiscoveryHttpClientHandler _handler;  
   private readonly IConfiguration _configuration;  
   public ValuesController(IDiscoveryClient client, IConfiguration configuration)    {_configuration = configuration;_handler = new DiscoveryHttpClientHandler(client);}
[HttpGet("search")]  
 public IActionResult Get(string name, string password)    {        var account = Account.GetAll().FirstOrDefault(x => x.Name == name && x.Password == password);      
   if (account != null){          
    return Ok(account);}      
     else{        
        return NotFound();}}
        [HttpPost("Login")]  
   public async Task<IActionResult> Login([FromBody] LoginRequest input)    {        var discoveryClient = new DiscoveryClient($"http://{IdentityApplicationName}", _handler){Policy = new DiscoveryPolicy { RequireHttps = false }};      
 var disco = await discoveryClient.GetAsync();  
      if (disco.IsError)
      throw new Exception(disco.Error);        var clientId = _configuration.GetSection("IdentityServer:ClientId").Value;        if (string.IsNullOrEmpty(clientId))
       throw new Exception("clientId is not value.");    
      
        var clientSecrets = _configuration.GetSection("IdentityServer:ClientSecrets").Value;        if (string.IsNullOrEmpty(clientSecrets))
         throw new Exception("clientSecrets is not value.");    
           var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, clientSecrets, _handler);        var response = await tokenClient.RequestResourceOwnerPasswordAsync(input.Name, input.Password, "api1 offline_access");//如果需要刷新token那么这里要多传递一个offline_access参数,不传的话RefreshToken为nullvar response = await tokenClient.RequestResourceOwnerPasswordAsync(input.Name, input.Password, "api1");      
          if (response.IsError) throw new Exception(response.Error);    
             return Ok(new LoginResponse(){AccessToken = response.AccessToken,ExpireIn = response.ExpiresIn,RefreshToken = response.RefreshToken});} }

这里offline_access这个参数很重要,如果你需要刷新token必须传这个参数,传递了这个参数以后redis服务器会记录,通过refreshToken来获取一个新的accessToken,这里就不做演示了,Ids4的东西太多了,更细节的东西大家去关注Ids4的内容

Account.cs

提供2个用户,各有不同的角色

 public class Account{      
  public string Name { get; set; }    
     public string Password { get; set; }    
     
     public int Id { get; set; }    
     
     public string Role { get; set; }        
     
     public static List<Account> GetAll()        {            return new List<Account>(){              
       new Account(){Id = 87654,Name = "leo",Password = "123456",Role = "admin"},                new Account(){Id = 45678,Name = "mickey",Password = "123456",Role = "normal"}};}}

订单中心

nuget引用
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.1.0" />
<PackageReference Include="Pivotal.Discovery.Client" Version="1.1.0" />
Startup.cs
public void ConfigureServices(IServiceCollection services){services.AddDiscoveryClient(Configuration);    var discoveryClient = services.BuildServiceProvider().GetService<IDiscoveryClient>();    var handler = new DiscoveryHttpClientHandler(discoveryClient);services.AddAuthorization();services.AddAuthentication(x =>{x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;}).AddIdentityServerAuthentication(x =>{x.ApiName = "api1";x.ApiSecret = "secret";x.Authority = "http://identity";x.RequireHttpsMetadata = false;x.JwtBackChannelHandler = handler;x.IntrospectionDiscoveryHandler = handler;x.IntrospectionBackChannelHandler = handler;});services.AddMvc();
}

这里需要注意的一点是handler,Ids4竟然在参数里面有handler的参数,这样我们接入微服务里面的服务发现简直太easy了。同时这里也给大家一个启发,我们再做第三方接口的时候,一定要参数齐全,哪怕这个参数并不会被大多数情况下使用,如果Ids4没提供这个参数,那么我就需要重写一套验证逻辑了。

ValuesController.cs

添加4个接口,针对不同的角色用户

[Route("/")]
public class ValuesController : Controller{    // admin role
 [HttpGet("admin")]  
  [Authorize(Roles = "admin")]
   public IActionResult Get1()    {      
     var userId = User.Claims.FirstOrDefault(x => x.Type == "sub")?.Value;        
     var role = User.Claims.FirstOrDefault(x => x.Type == "role")?.Value;      
      return Ok(new { userId, role });}    
      // normal role[HttpGet("normal")][Authorize(Roles = "normal")]  
     public IActionResult Get2()    {    
         var userId = User.Claims.FirstOrDefault(x => x.Type == "sub")?.Value;      
           return Ok(new { role = "normal", userId = userId });}  
            // any role[HttpGet("any")][Authorize]  
     public IActionResult Get3()    {    
         var userId = User.Claims.FirstOrDefault(x => x.Type == "sub")?.Value;      
          return Ok(new { role = "any", userId = userId });}  
   // Anonymous[HttpGet][AllowAnonymous]  
    public IActionResult Get()    {  
         return Ok(new { role = "allowAnonymous" });} }

演示部分

JWT

分别运行这个5个应用程序,访问http://localhost:5000

如图表示,全部运行成功。
通过postman模拟用户登录,通过api网关地址访问。
url:http://localhost:5555/user/login
method:post
requestBody:
{
"name":"leo",
"password":"123456"
}

拿到token后,我们再访问订单中心的地址。
url:http://locahost:5555/order/admin
mothod:get
header: Authorization:bearer token(bearer和token中间有一个空格)

成功返回userId和role信息
我们随意修改一下token的字符串再访问,会返回401,认证不会通过。

这里需要注意的是zuul默认不支持header的传递,需要在网关服务里面增加一个配置zuul.sensitive-headers=true

这个时候我们修改url地址http://locahost:5555/order/normal

返回了403表示这个接口没有权限
再修改地址访问http://locahost:5555/order/any

这个接口只要授权用户都可以访问。
最后这个接口http://locahost:5555/order就比较容易理解是一个匿名用户都可以访问的接口不用做身份验证,我们去掉header信息

我们可以再试试另一个用户mickey/123456试试,篇幅有限,这里就不再做描述了,mickey这个用户拥有http://locahost:5555/order/normal这个接口的访问权限。

Reference

切换一下配置文件,来支持reference,修改User项目的appsettings.json文件

 "IdentityServer": {    //"ClientId": "client.jwt",    //"ClientSecrets": "AB2DC090-0125-4FB8-902A-34AFB64B7D9B",    "ClientId": "client.reference",    "ClientSecrets": "A30E6E57-086C-43BE-AF79-67ADECDA0A5B"}

重新运行程序
通过postman模拟用户登录,通过api网关地址访问。
url:http://localhost:5555/user/login
method:post
requestBody:
{
"name":"leo",
"password":"123456"
}


我们可以看到accessToken和JWT的完全不一样,很短的一个字符串,这个时候我们打开redis客户端可以找个这个信息

用户信息是保存在了redis里面。这里的key是通过加密的方式生成的。
拿到token后,我们再访问订单中心的地址。
url:http://locahost:5555/order/admin
mothod:get
header: Authorization:bearer token

验证成功,后面的几个接口和上面一样,同学们自己来演示。

后记

通过上面的例子,我们把整个授权认证流程都走了一遍(JWT和Reference),通过Postman来模拟客户端的请求,Ids4的东西实在是太多,我没办法在这里写的太全,大家可以参考一下园子里面关于Ids4的文章。这篇文章例子比较多,强烈建议大家先下载代码,跟着博客的流程走一次,然后自己再按照步骤写一遍,这样才能加深理解。顺便给自己打个广告,笔者目前正在考虑新的工作机会,如果贵公司需要使用.NET core来搭建微服务平台,我想我非常合适。我的邮箱240226543@qq.com。

关于授权认证部分大家可以看看园子里面雨夜朦胧的博客,他通过源代码分析写的非常透彻。

示例代码

所有代码均上传github。代码按照章节的顺序上传,例如第一章demo1,第二章demo2以此类推。
求推荐,你们的支持是我写作最大的动力,我的QQ群:328438252,交流微服务。

相关文章:

  • 手把手教你使用spring cloud+dotnet core搭建微服务架构:服务治理(-)

  • spring cloud+dotnet core搭建微服务架构:服务发现(二)

  • spring cloud+dotnet core搭建微服务架构:Api网关(三)

  • 微服务~Eureka实现的服务注册与发现及服务之间的调用

  • spring cloud+dotnet core搭建微服务架构:配置中心(四)

原文地址:http://www.cnblogs.com/longxianghui/p/7800316.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

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

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

相关文章

如何用spring boot写一个注册页面

环境准备&#xff1a; java集成开发环境&#xff1a;IDEA 数据库&#xff1a;Mysql Maven 最好在安装有个navicat&#xff08;数据库可视化界面&#xff09; 安装好上述几个软件后 总结下&#xff1a;五步 1、创建新的工程 2、创建建applicatiom.yml 3、创建entity层 4、创建r…

IDEA创建包不是树形

创建包的时候和别人的不一样&#xff0c;不是树形结构 可以点击图中的齿轮改变选项 把两个对勾取消掉就可以了 现在就是树形结构了

.NET Core跨平台的奥秘[下篇]:全新的布局

从本质上讲&#xff0c;按照CLI规范设计的.NET从其出生的那一刻就具有跨平台的基因&#xff0c;这与Java别无二致。由于采用了统一的中间语言&#xff0c;微软只需要针对不同的平台设计不同的虚拟机&#xff08;运行时&#xff09;就能弥合不同操作系统与处理器架构之间的差异&…

漫画:什么是拜占庭将军问题

转载自 漫画&#xff1a;什么是拜占庭将军问题 什么是拜占庭将军问题&#xff1f; 在很久很久以前&#xff0c;拜占庭是东罗马帝国的首都。那个时候罗马帝国国土辽阔&#xff0c;为了防御目的&#xff0c;因此每个军队都分隔很远&#xff0c;将军与将军之间只能靠信使传递消息…

SQL Server 审计

审计&#xff08;Audit&#xff09;用于追踪和记录SQL Server实例或数据库中发生的事件&#xff0c;审计主要包括审计对象&#xff08;Audit&#xff09;和审计规范&#xff08;Audit Specification&#xff09;&#xff0c;创建审计首先需要创建一个SQL Server 实例级的审计对…

ASP.NET Core中的OWASP Top 10 十大风险-失效的访问控制与Session管理

本博文翻译自&#xff1a; https://dotnetcoretutorials.com/2017/10/16/owasp-top-10-asp-net-core-broken-authentication-session-management/ 在我们之前关于OWASP Top 10的文章中&#xff0c;我们讨论了SQL注入。SQL注入有一个非常明确的解释和例子&#xff0c;但这次我们…

Docker部署运行微服务

1、环境准备&#xff1a; 主机&#xff1a; X-shell X-ftp jar包 这里只说下jar包&#xff0c;另外两个到官网下载即可 Idea打包jar包流程 先按这四步走 先点击左下的框框&#xff0c;再点击maven&#xff0c;出现右边的窗口&#xff0c;点击clean&#xff0c;再点击package&…

.net ef core 领域设计代码转换(上篇)

一、前言 .net core 2.0正式版已经发布几个月了&#xff0c;经过研究&#xff0c;决定把项目转移过来&#xff0c;新手的话可以先看一些官方介绍 传送门&#xff1a;https://docs.microsoft.com/zh-cn/dotnet/core/ 由于在领域设计模型上遇到了一些坑&#xff0c;故给大家分享出…

Debug ASP.NET Core 2.0源代码

首先你的VS必须为VS 2017 15.3或以上版本。 打开你的Startup类&#xff0c;在ConfigureServices方法上设置个断点&#xff0c;按F5 Debug应用。 在Call Stack&#xff08;调用堆栈&#xff09;窗口&#xff0c;我们只能看到自己的代码。打开VS tools&#xff08;工具&#xff…

我心中的ASP.NET Core 新核心对象WebHost(一)

以本系列文章向Fish 前辈的那篇我心中的ASP.NET 核心对象致敬。&#xff08;虽然不知道前辈现在在干什么&#xff09;。一晃就6年过去了&#xff0c;那首 郝云 的《回到那一天》怎么唱来着&#xff1f; 时光一晃&#xff0c;你就三十了。 而我们都变成了老了的程序员 ASP.NET …

Scaffolding Template on Asp.Net Core Razor Page

Scaffolding Template Intro 我们知道在Asp.Net MVC中&#xff0c;如果你使用的EF的DBContext的话&#xff0c;你可以在vs中通过右键解决方案-添加控制器-添加包含视图的控制器&#xff0c;然后vs会根据你选择的Model自动生成相应的CURD的控制器和View&#xff0c;非常便利&…

Hadoop生态hive(一)介绍

一、Hive是什么 起源自facebook由Jeff Hammerbacher领导的团队&#xff0c;构建在Hadoop上的数据仓库框架。设计目的是让SQL技能良好&#xff0c;但Java技能较弱的分析师可以查询海量数据。2008年facebook把hive项目贡献给Apache。 由facebook开源&#xff0c;最初用于解决海量…

使用混合云的SQL Server

近期发布的Microsoft SQL Server 2017&#xff0c;表明Microsoft公司正寻求不断降低其所交付的工具对平台的绑定。在SQL Server 2017中&#xff0c;这一趋势可以从“混合云”&#xff08;Hybrid Cloud&#xff09;术语和多平台本质这两个方面得以证实。下面让我们分别一窥这两个…

.NET Core多平台项目模板eShopOnContainers编译手记

之前写了一个功能性的文件上传asp.net core的小程序&#xff0c;加上点七七八八的东西&#xff0c;勉强能够应付了&#xff0c;打算学习一下微软的官方.NET CORE微服务示例https://github.com/dotnet-architecture/eShopOnContainers。这个例子很全面地展现了微服务、docker以及…

如何改变Idea的背景

进入Idea 按下ctrlshifta 点击set background 选择自己准备好的图片地址&#xff0c;就可以更改背景了 更改后效果 关于接口的插件 GsonFormat插件 安装还是和别的插件一样&#xff0c;在plugin里搜索GsonFormat&#xff0c;下载并重启 然后在model里的类 按下alts 出现 将…

TFS在项目中Devops落地进程(上)

作为一名开发,经过近2年折腾,基于TFS的Devops主线工程大体落地完毕。 在此大体回忆下中间的各种历程。 开始之前简单说下什么是TFS(Team Foundation Server)。 TFS是微软推出的一款ALM&#xff08;Application Lifecycle Management)管理工具。 透过TFS你将能获取到从代码版本管…

Hadoop生态hive(三)Hive QL介绍

一、表 创建表 CREATE [EXTERNAL] TABLE [IF NOT EXISTS] table_name CREATE TABLE 创建一个指定名字的表。Hive 创建内部表时&#xff0c;会将数据移动到数据仓库指向的路径&#xff1b; EXTERNAL 关键字可以让用户创建一个外部表&#xff0c;在建表的同时指定一个指向实…

抖音上非常火的旋转图快速部署

本教程不需要你有服务器&#xff01; 本教程不需要你有服务器&#xff01; 本教程不需要你有服务器&#xff01; 点击我看旋转图 总共两步 1、注册一个码云账号 2、新建一个仓库&#xff0c;上传文件即可 1、注册账号的话&#xff0c;直接浏览器搜索码云&#xff0c;手机号…

Microsoft的现代数据管理

PASS 2017峰会是面向SQL Server及Microsoft相关数据技术用户的大会。在大会的第一天&#xff0c;Microsoft的Rohan Kumar先生到场做了开幕式的主题演讲&#xff0c;并借此机会展示了Microsoft在SQL Server和Azure数据库方面的最新进展。 Kumar的演讲涉及数据、人工智能和云这三…

vue 3.4x以上如何改变项目运行端口号

我用3.4版本的vue-cli构建了一个vue项目&#xff0c;然后我想修改项目运行之后的访问端口 在网上查了一下&#xff0c;很多都是2.0的版本&#xff0c;查来查去都说是什么在 config目录下index.js文件中修改端口号。。。。。。。 都抄来抄去的 新的vue项目目录结构下并没有con…