
一个安静的程序猿~
原文 浅谈SQL Server内部运行机制 对于已经很熟悉T-SQL的读者,或者对于较专业的DBA来说,逻辑的增删改查,或者较复杂的SQL语句,都是非常简单的,不存在任何挑战,不值得一提,那么,SQL的哪些方面是他们的挑战 或者软肋呢? 那就是sql优化。然而,要向成为一个好的Sql优化高手,首先要做的一件事无疑就是了解sql语句在SQL Server中是如何执行的。在这一系列中,我们将开始sqlserver优化系列讲解,本 讲为优化系列的开篇文章, 在本篇文章中,我们将重点讲解SQL Server体系结构 在正式讲解之前,我们先来看看如下问题,你是否遇到过,若你遇到过且成功解决,那么这篇文章,你可以跳过。 为了测试需要,我们先模拟插入5亿3千多万条数据。 ? 1 SELECT COUNT(1) FROM BigDataTest (一)查询缓慢问题 *,临时表,表连接,子查询等造成的查询缓慢问题,你能解决吗? (二)内存泄漏 如下查询了8分2秒,然后内存溢出,你知道问题吗? ? 1 SELECT * FROM BigDataTest (三)经常听说如下概念,你都能解决吗? 事务与锁(请参考我另一篇文章:浅谈SQL Server事务与锁(上篇)),ACID,隔离级别,脏读,分表分库,水平拆分,垂直拆分,高并发等 一 SQL Server体系结构抽象 二 SQL Server体系结构概述 SQL Server核心体系结构,大致包括六大部分:客户端访问工具、SQL Server 网络接口(SQL Server Network Interface,SNI)、关系引擎、存储引擎、 磁盘和缓冲池。下图为SQL Server核心体系大致轮廓图。 (一)SQL Server客户端访问工具 SQL Server客户端访问工具,提供了远程访问技术,它与SQL Server服务端基于一定的协议,使其能够远程访问数据库,就像在本地操作数据库一样,如我们经常用的 Microsoft SQL Server Management Studio。 SQL Server客户端访问工具是比较多的,其中比较流行的要数Microsoft SQL Server Management Studio 和Navicat(Navicat在MySQL中也是比较常用的)了,至于其他工具, 本篇文章就不列举了,感兴趣的读者朋友,可以查询一下。 (二)SQL Server网络协议 SQL Server网络协议,又叫SQL Server网络接口(SNI),它是构成客户端和服务端通信的桥梁,它与SQL Server服务端基于一定协议,方可通信, 如我们在客户端输入一条查询语句SELECT * FROM BigDataTest,这条语句,只有客户端和服务端基于一定协议,方可被服务端解析,否则,被视为无 效语句。 SQL Server网络协议,由一组API构成,这些API供SQL Server数据库引擎和SQL Server本地客户端调用,如实现最基本的CRUD通信。 SQL Server 网络接口(SQL Server Network Interface,SNI)只需要在客户端和服务端配置网络协议即可,它支持一下协议: (1)共享内存 (2)TCP/IP (3)命名管道 (4)VIA (三)关系引擎 关系引擎,也叫查询引擎,其主要功能是负责处理SQL语句,其核心组件由三部分组成:命令分析器、查询优化器和查询执行器。 (1)命令分析器:负责解析客户端传递过来的T-SQL语句,如客户端传递一条SQL语句:SELECT * FROM BigDataTest,它会检查该语句的语法结构,若语法 错误,它会将错误返回给协议层,然后协议层将错误返回给客户端;如果语法结构正确,它会根据查询命令生成查询计划或寻找一个已存在的查询计划(先在缓冲池计划缓 存中查找,若找到,则直接给查询执行器执行,若未找到,则会生成基于T-SQL的查询树,然后交给查询优化器优化) (2)查询优化器:负责优化命令解析器生成的T-SQL查询树(基于资源的优化,而非基于时间的优化),然后将最终优化结果传递给查询执行器执行。查询优化器是基于 “资源开销”的优化器,这种算法评估多种可执行的查询方式,并从中选择开销最低的方案作为优化结果,然后将该结果生成查询计划输出给查询执行器。注意,查询优化器是 “基于资源开销最优”而非“基于方案最优”,也就是,查询优化器的最终优化结果未必是最好的方案,但一定是资源开销最低的方案。 (3)查询执行器:负责执行查询。假若查询执行器接收到命令解析器或查询优化器传递过来的SQL语句:SELECT * FROM BigDataTest,它通过OLE DB接口传递到存储 引擎,再传递到存储引擎的访问方法。 (四)存储引擎 存储引擎,本质就是管理资源存储的,它的核心组件包括三部分:访问方法、事务管理器和缓冲区管理器。 (1)访问方法:访问方法本质是一个接口,供查询执行器调用(该接口提供了所有检索数据的代码,接口的实际执行是由缓冲区管理器来执行的),假若查询执行器传递一条SQL语句: SELECT * FROM BigDataTest,访问方法接收到该请求命令后,就会调用缓冲区管理器,缓冲区管理器就会调用缓冲池的计划缓存,在计划缓存中寻找到相应的结果集,然后返回给关系 引擎。 (2)缓冲区管理器:供访问方法调用,管理缓冲池,在缓冲池中查询相应资源并返回结果集,供访问方法返回给关系引擎。 (3)事务管理器:主要负责事务的管理(ACID管理)和高并发管理(锁),它包括两个核心组件(日志管理器和锁管理器),锁管理器负责提供并发数据访问,设置隔离级别等;日志管理器负责 记录所有访问方法操作动作,如基本的CRUD。 (五)缓冲池 缓冲池驻于内存中,是磁盘和缓冲区管理器的桥梁SQL Server中,所有资源的查询都是在内存中进行的,即在缓冲池中进行的,假若缓冲池 接收到缓冲区管理器传递过来的的一条SQL语句:SELECT * FROM BigDataTest,缓冲区管理器数据缓存先从磁盘数据库中取满足条件的结果集, 然后放在缓冲池数据缓冲中,然后以结果集的形式返回给缓冲区管理器,供访问方法返回给关系引擎的查询执行器,然后返回给协议层,协议层再 返回给客户端。注意,这里操作的是缓冲池中数据,而不是磁盘DB中的数据,并且操作的缓冲池数据不会立即写入磁盘,因此就会造成查询到结果 与BD中的结果不一致,这就是所谓的脏读。 缓冲池主要包括两部分:计划缓存(生成执行计划是非常耗时耗资源的,计划缓存主要用来存储执行计划,以备后续使用)和数据缓存(通常是缓存池 中容量最大的,消耗内存最大,从磁盘中读取的数据页只要放在这里,方可调用) (六)磁盘 磁盘主要是用来存储持久化资源的,如日志资源,数据库资源和缓存池持久化支援等。 三 一个查询的完整流程 如下为一个比较完善的查询过程,即第二部分查询语句:SELECT * FROM BigDataTest 整个过程。 四 参考文献 【01】《SQL Server 2012 深入解析与性能优化 第3版》Christian Bolton,Justin Langford,Glenn Berry,Gavin Payne,Amit Banerjee,Rob Farley著 五 版权区 感谢您的阅读,若有不足之处,欢迎指教,共同学习、共同进步。 博主网址:http://www.cnblogs.com/wangjiming/。 极少部分文章利用读书、参考、引用、抄袭、复制和粘贴等多种方式整合而成的,大部分为原创。 如您喜欢,麻烦推荐一下;如您有新想法,欢迎提出,邮箱:2098469527@qq.com。 可以转载该博客,但必须著名博客来源。
原文 【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权 【.NET Core项目实战-统一认证平台】开篇及目录索引 上篇文章介绍了如何使用Dapper持久化IdentityServer4(以下简称ids4)的信息,并实现了sqlserver和mysql两种方式存储,本篇将介绍如何使用ids4进行客户端授权。 .netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。 一、如何添加客户端授权? 在了解如何进行客户端授权时,我们需要了解详细的授权流程,在【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析一篇中我大概介绍了客户端的授权方式,本篇再次回忆下客户端的授权方式,老规则,上源码。 首先查看获取token的方式,核心代码如下。 private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context) { _logger.LogDebug("Start token request."); // 1、验证客户端及授权信息结果 var clientResult = await _clientValidator.ValidateAsync(context); if (clientResult.Client == null) { return Error(OidcConstants.TokenErrors.InvalidClient); } // 2、验证请求结果 var form = (await context.Request.ReadFormAsync()).AsNameValueCollection(); _logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName); var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult); if (requestResult.IsError) { await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult)); return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse); } // 3、创建输出结果 _logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName); var response = await _responseGenerator.ProcessAsync(requestResult); await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult)); LogTokens(response, requestResult); // 4、返回结果 _logger.LogDebug("Token request success."); return new TokenResult(response); } 我们需要详细分析下第一步客户端授权信息是如何验证的?核心代码如下。 /// <summary> ///验证客户端授权结果 /// </summary> /// <param name="context">请求上下文</param> /// <returns></returns> public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context) { _logger.LogDebug("Start client validation"); var fail = new ClientSecretValidationResult { IsError = true }; //通过请求上下文和配置信息获取校验方式,从这里我们可以知道客户端请求的几种方式。 var parsedSecret = await _parser.ParseAsync(context); if (parsedSecret == null) { await RaiseFailureEventAsync("unknown", "No client id found"); _logger.LogError("No client identifier found"); return fail; } // 根据客户端ID获取客户端相关信息。(配合持久化篇查看) var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id); if (client == null) { await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client"); _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id); return fail; } SecretValidationResult secretValidationResult = null; if (!client.RequireClientSecret || client.IsImplicitOnly()) { _logger.LogDebug("Public Client - skipping secret validation success"); } else { //校验客户端授权和请求的是否一致 secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets); if (secretValidationResult.Success == false) { await RaiseFailureEventAsync(client.ClientId, "Invalid client secret"); _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId); return fail; } } _logger.LogDebug("Client validation success"); var success = new ClientSecretValidationResult { IsError = false, Client = client, Secret = parsedSecret, Confirmation = secretValidationResult?.Confirmation }; await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type); return success; } 这里几个方法可以从写的说明备注里就可以知道什么意思,但是 var parsedSecret = await _parser.ParseAsync(context);这句话可能不少人有疑问,这段是做什么的?如何实现不同的授权方式? 这块就需要继续理解Ids4的实现思路,详细代码如下。 /// <summary> /// 检查上下文获取授权信息 /// </summary> /// <param name="context">The HTTP context.</param> /// <returns></returns> public async Task<ParsedSecret> ParseAsync(HttpContext context) { // 遍历所有的客户端授权获取方式,提取当前哪一个满足需求 ParsedSecret bestSecret = null; foreach (var parser in _parsers) { var parsedSecret = await parser.ParseAsync(context); if (parsedSecret != null) { _logger.LogDebug("Parser found secret: {type}", parser.GetType().Name); bestSecret = parsedSecret; if (parsedSecret.Type != IdentityServerConstants.ParsedSecretTypes.NoSecret) { break; } } } if (bestSecret != null) { _logger.LogDebug("Secret id found: {id}", bestSecret.Id); return bestSecret; } _logger.LogDebug("Parser found no secret"); return null; } 就是从注入的默认实现里检测任何一个实现ISecretParser接口方法,通过转到实现,可以发现有PostBodySecretParser、JwtBearerClientAssertionSecretParser、BasicAuthenticationSecretParser三种方式,然后再查看下注入方法,看那些实现被默认注入了,这样就清楚我们使用Ids4时支持哪几种客户端授权方式。 /// <summary> /// 添加默认的授权分析 /// </summary> /// <param name="builder">The builder.</param> /// <returns></returns> public static IIdentityServerBuilder AddDefaultSecretParsers(this IIdentityServerBuilder builder) { builder.Services.AddTransient<ISecretParser, BasicAuthenticationSecretParser>(); builder.Services.AddTransient<ISecretParser, PostBodySecretParser>(); return builder; } 从上面代码可以发现,默认注入了两种分析器,我们就可以通过这两个方式来做客户端的授权,下面会分别演示两种授权方式的实现。 BasicAuthenticationSecretParser public Task<ParsedSecret> ParseAsync(HttpContext context) { _logger.LogDebug("Start parsing Basic Authentication secret"); var notfound = Task.FromResult<ParsedSecret>(null); var authorizationHeader = context.Request.Headers["Authorization"].FirstOrDefault(); if (authorizationHeader.IsMissing()) { return notfound; } if (!authorizationHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { return notfound; } var parameter = authorizationHeader.Substring("Basic ".Length); string pair; try { pair = Encoding.UTF8.GetString( Convert.FromBase64String(parameter)); } catch (FormatException) { _logger.LogWarning("Malformed Basic Authentication credential."); return notfound; } catch (ArgumentException) { _logger.LogWarning("Malformed Basic Authentication credential."); return notfound; } var ix = pair.IndexOf(':'); if (ix == -1) { _logger.LogWarning("Malformed Basic Authentication credential."); return notfound; } var clientId = pair.Substring(0, ix); var secret = pair.Substring(ix + 1); if (clientId.IsPresent()) { if (clientId.Length > _options.InputLengthRestrictions.ClientId || (secret.IsPresent() && secret.Length > _options.InputLengthRestrictions.ClientSecret)) { _logger.LogWarning("Client ID or secret exceeds allowed length."); return notfound; } var parsedSecret = new ParsedSecret { Id = Decode(clientId), Credential = secret.IsMissing() ? null : Decode(secret), Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret }; return Task.FromResult(parsedSecret); } _logger.LogDebug("No Basic Authentication secret found"); return notfound; } 由于代码比较简单,就不介绍了,这里直接模拟此种方式授权,打开PostMan,在Headers中增加Authorization的Key,并设置Value为Basic YXBwY2xpZW50JTNBc2VjcmV0,其中Basic后为client_id:client_secret值使用Base64加密。然后请求后显示如图所示结果,奈斯,得到我们授权的结果。 PostBodySecretParser public async Task<ParsedSecret> ParseAsync(HttpContext context) { _logger.LogDebug("Start parsing for secret in post body"); if (!context.Request.HasFormContentType) { _logger.LogDebug("Content type is not a form"); return null; } var body = await context.Request.ReadFormAsync(); if (body != null) { var id = body["client_id"].FirstOrDefault(); var secret = body["client_secret"].FirstOrDefault(); // client id must be present if (id.IsPresent()) { if (id.Length > _options.InputLengthRestrictions.ClientId) { _logger.LogError("Client ID exceeds maximum length."); return null; } if (secret.IsPresent()) { if (secret.Length > _options.InputLengthRestrictions.ClientSecret) { _logger.LogError("Client secret exceeds maximum length."); return null; } return new ParsedSecret { Id = id, Credential = secret, Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret }; } else { // client secret is optional _logger.LogDebug("client id without secret found"); return new ParsedSecret { Id = id, Type = IdentityServerConstants.ParsedSecretTypes.NoSecret }; } } } _logger.LogDebug("No secret in post body found"); return null; } 此种认证方式就是从form_data提取client_id和client_secret信息,我们使用PostMan继续模拟客户端授权,测试结果如下,也可以得到我们想要的结果。 有了前面的两个授权方式,我们清楚了首先验证客户端的授权信息是否一致,再继续观察后续的执行流程,这时会发现TokenRequestValidator中列出了客户端授权的其他信息验证,详细定义代码如下。 switch (grantType) { case OidcConstants.GrantTypes.AuthorizationCode: return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters); //客户端授权 case OidcConstants.GrantTypes.ClientCredentials: return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters); case OidcConstants.GrantTypes.Password: return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters); case OidcConstants.GrantTypes.RefreshToken: return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters); default: return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters); } 详细的授权验证代码如下,校验客户端授权的一般规则。 private async Task<TokenRequestValidationResult> ValidateClientCredentialsRequestAsync(NameValueCollection parameters) { _logger.LogDebug("Start client credentials token request validation"); ///////////////////////////////////////////// // 校验客户端Id是否开启了客户端授权 ///////////////////////////////////////////// if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.ClientCredentials)) { LogError("{clientId} not authorized for client credentials flow, check the AllowedGrantTypes of the client", _validatedRequest.Client.ClientId); return Invalid(OidcConstants.TokenErrors.UnauthorizedClient); } ///////////////////////////////////////////// // 校验客户端是否有请求的scopes权限 ///////////////////////////////////////////// if (!await ValidateRequestedScopesAsync(parameters, ignoreImplicitIdentityScopes: true, ignoreImplicitOfflineAccess: true)) { return Invalid(OidcConstants.TokenErrors.InvalidScope); } if (_validatedRequest.ValidatedScopes.ContainsOpenIdScopes) { LogError("{clientId} cannot request OpenID scopes in client credentials flow", _validatedRequest.Client.ClientId); return Invalid(OidcConstants.TokenErrors.InvalidScope); } if (_validatedRequest.ValidatedScopes.ContainsOfflineAccessScope) { LogError("{clientId} cannot request a refresh token in client credentials flow", _validatedRequest.Client.ClientId); return Invalid(OidcConstants.TokenErrors.InvalidScope); } _logger.LogDebug("{clientId} credentials token request validation success", _validatedRequest.Client.ClientId); return Valid(); } 最终输出详细的校验结果数据,现在整个客户端授权的完整逻辑已经介绍完毕,那如何添加我们的自定义客户端授权呢?比如我要给客户端A开放一个访问接口访问权限,下面就开通客户端A为案例讲解。 开通客户端授权 根据前面介绍的验证流程,我们清楚首先需要增加客户端信息,这里起名叫clienta,密码设置成secreta。上一篇我们介绍了Dapper持久化IdentityServer4的授权信息,所以这里我就直接以SQL语句的方式来演示添加配置信息。详细的语句如下: /* 添加客户端脚本 */ --1、添加客户端信息 INSERT INTO Clients(AccessTokenLifetime,ClientId,ClientName,Enabled) VALUES(3600,'clienta','测试客户端A',1); --2、添加客户端密钥,密码为(secreta) sha256 INSERT INTO ClientSecrets VALUES(21,'',null,'SharedSecret','2tytAAysa0zaDuNthsfLdjeEtZSyWw8WzbzM8pfTGNI='); --3、增加客户端授权权限 INSERT INTO ClientGrantTypes VALUES(21,'client_credentials'); --4、增加客户端能够访问scope INSERT INTO ClientScopes VALUES(21,'mpc_gateway'); 然后我们来测试下新开通的客户端授权,如下图所示,可以正常获取授权信息了,另外一种Basic授权方式可自行测试。 二、如何配合网关认证和授权? 前面使用的是项目自己进行验证的,正式项目运行时,我们会把请求放到网关中,统一由网关进行认证和授权等操作,内部api无需再次进行认证和授权,那如何实现网关认证和授权呢? 我们可以回忆下之前介绍网关篇时认证篇章,里面介绍的非常清楚。这里我们参照刚才添加的客户端A为案例增加网关授权,因为我们对外暴露的是网关地址,而不是内部具体认证项目地址。 1、添加网关授权路由 本项目的网关端口为7777,所以网关授权的地址为http://localhost:7777/connect/token,由于为添加网关路由,直接访问报401,我们首先增加网关的路由信息。 -- 1、插入认证路由(使用默认分类) insert into AhphReRoute values(1,'/connect/token','[ "POST" ]','','http','/connect/token','[{"Host": "localhost","Port": 6611 }]', '','','','','','','',0,1); --2、加入全局配置 INSERT INTO AhphConfigReRoutes VALUES(1,3) --3、增加认证配置地址路由 insert into AhphReRoute values(1,'/.well-known/openid-configuration','[ "GET" ]','','http','/.well-known/openid-configuration','[{"Host": "localhost","Port": 6611 }]', '','','','','','','',0,1); --4、加入全局配置 INSERT INTO AhphConfigReRoutes VALUES(1,4); --5、增加认证配置地址路由 insert into AhphReRoute values(1,'/.well-known/openid-configuration/jwks','[ "GET" ]','','http','/.well-known/openid-configuration/jwks','[{"Host": "localhost","Port": 6611 }]', '','','','','','','',0,1); --6、加入全局配置 INSERT INTO AhphConfigReRoutes VALUES(1,5); 通过PostMan测试,可以得到我们预期的授权信息结果。 然后继续访问我们之前配置的授权路由,提示401未授权,这块就涉及到前面网关篇的知识了,因为我们的网关增加了授权,所以需要增加客户端授权才能访问。 2、添加客户端授权访问 还记得是如何添加客户端授权的吗?详细介绍参考[【.NET Core项目实战-统一认证平台】第六章 网关篇-自定义客户端授权 ,我直接把授权的脚本编写如下: --7、插入把客户端加入测试路由组2 INSERT INTO AhphClientGroup VALUES(21,2) 使用我们刚授权的信息,再次访问之前配置的需要认证的路由,可以得到我们预期的结果,奈斯,和网关篇的内容完全一致。 注意:在配置完信息后需要清理缓存,因为我们之前做网关时,很多配置信息的读取使用了缓存。 三、如何统一输出结果? 作为一块准备应用到生产环境的产品,可能为各种第三方提供应用支持,那么统一的输出结果是必须要实现的,比如我们使用微信sdk或其他第三方sdk时,会发现它们都会列出出现错误的统一提示,由标识代码和说明组成,这里我们就需要解决如何标准化输出问题,自己业务系统输出标准结果很容易,因为都是自己控制的结果输出,那么我们网关集成Ocelot、认证集成IdentityServer4,这两块如何进行标准化输出呢? 那开始我们的改造之旅吧,首先我们要明确如果遇到错误如何进行输出,我们定义一个输出基类BaseResult,详细的定义如下: /// <summary> /// 金焰的世界 /// 2018-12-10 /// 信息输出基类 /// </summary> public class BaseResult { public BaseResult(int _errCode,string _errMsg) { errCode = _errCode; errMsg = _errMsg; } public BaseResult() { } /// <summary> /// 错误类型标识 /// </summary> public int errCode { get; set; } /// <summary> /// 错误类型说明 /// </summary> public string errMsg { get; set; } } /// <summary> /// 金焰的世界 /// 2018-12-10 /// 默认成功结果 /// </summary> public class SuccessResult : BaseResult { public SuccessResult() : base(0, "成功") { } } 1、网关默认输出改造 网关这段需要改造错误提示的代码和内容以及异常的输出结果,首先改造错误情况的输出结果,使用BaseResult统一输出,这里就需要重写输出中间件ResponderMiddleware,下面就开始重写之旅吧。 新增自定义输出中间件CzarResponderMiddleware,详细代码如下: using Czar.Gateway.Configuration; using Microsoft.AspNetCore.Http; using Ocelot.Errors; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responder; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; namespace Czar.Gateway.Responder.Middleware { /// <summary> /// 金焰的世界 /// 2018-12-10 /// 统一输出中间件 /// </summary> public class CzarResponderMiddleware: OcelotMiddleware { private readonly OcelotRequestDelegate _next; private readonly IHttpResponder _responder; private readonly IErrorsToHttpStatusCodeMapper _codeMapper; public CzarResponderMiddleware(OcelotRequestDelegate next, IHttpResponder responder, IOcelotLoggerFactory loggerFactory, IErrorsToHttpStatusCodeMapper codeMapper ) : base(loggerFactory.CreateLogger<CzarResponderMiddleware>()) { _next = next; _responder = responder; _codeMapper = codeMapper; } public async Task Invoke(DownstreamContext context) { await _next.Invoke(context); if (context.IsError) {//自定义输出结果 var errmsg = context.Errors[0].Message; int httpstatus = _codeMapper.Map(context.Errors); var errResult = new BaseResult() { errcode = httpstatus, errmsg = errmsg }; var message = errResult.ToJson(); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK; await context.HttpContext.Response.WriteAsync(message); return; } else if (context.HttpContext.Response.StatusCode != (int)HttpStatusCode.OK) {//标记失败,不做任何处理,自定义扩展时预留 } else if (context.DownstreamResponse == null) {//添加如果管道强制终止,不做任何处理,修复未将对象实例化错误 } else {//继续请求下游地址返回 Logger.LogDebug("no pipeline errors, setting and returning completed response"); await _responder.SetResponseOnHttpContext(context.HttpContext, context.DownstreamResponse); } } private void SetErrorResponse(HttpContext context, List<Error> errors) { var statusCode = _codeMapper.Map(errors); _responder.SetErrorResponseOnContext(context, statusCode); } } } 然后添加中间件扩展,代码如下。 using Ocelot.Middleware.Pipeline; namespace Czar.Gateway.Responder.Middleware { /// <summary> /// 金焰的世界 /// 2018-12-10 /// 应用输出中间件扩展 /// </summary> public static class CzarResponderMiddlewareExtensions { public static IOcelotPipelineBuilder UseCzarResponderMiddleware(this IOcelotPipelineBuilder builder) { return builder.UseMiddleware<CzarResponderMiddleware>(); } } } 最后使用此扩展来接管默认的输出中间件,详细代码如下。 //builder.UseResponderMiddleware(); builder.UseCzarResponderMiddleware(); 好了,网关统一输出中间件就完成了,是不是很简单呢?我们来测试下效果吧,PostMan闪亮登场, 奈斯,这才是我们需要的结果,那如何异常会输出什么呢??我们来模拟下结果,我直接在服务端抛出异常测试。 默认情况会支持输出异常的堆栈信息。那如何捕获服务端异常信息呢?我们需要了解在哪里发送了后端请求,通过源码分析,发现是由HttpRequesterMiddleware中间件做后端请求,这时我们只需要改造下此中间件即可完成统一异常捕获。改造核心代码如下: public async Task Invoke(DownstreamContext context) { var response = await _requester.GetResponse(context); if (response.IsError) { Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); SetPipelineError(context, response.Errors); return; } else if(response.Data.StatusCode != System.Net.HttpStatusCode.OK) {//如果后端未处理异常,设置异常信息,统一输出,防止暴露敏感信息 var error = new InternalServerError($"请求服务异常"); Logger.LogWarning($"路由地址 {context.HttpContext.Request.Path} 请求服务异常. {error}"); SetPipelineError(context, error); return; } Logger.LogDebug("setting http response message"); context.DownstreamResponse = new DownstreamResponse(response.Data); } 修改测试后端服务代码如下, // GET api/values/5 [HttpGet("{id}")] public ActionResult<string> Get(int id) { throw new Exception("测试异常"); } 然后通过网关访问路由地址http://localhost:7777/ctr/values/1,输出为{"errcode":500,"errmsg":"请求服务异常"},得到了预期的所有目标,网关统一输出全部改造完毕。 2、认证的统一输出改造 这里为了统一风格,我们先查看下Ids4的错误提示方式和输出结果,然后配合源码可以发现到输出都是继承IEndpointResult接口,并定义了各种方式的输出,且校验失败时,输出的状态码都不是200,那么我们可以从这里下手,在网关层增加独立的判断,来兼容自定义的输出。改造代码如下: using Czar.Gateway.Errors; using Newtonsoft.Json.Linq; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Requester; using System.Threading.Tasks; namespace Czar.Gateway.Requester.Middleware { /// <summary> /// 金焰的世界 /// 2018-12-10 /// 自定义请求中间件 /// </summary> public class CzarHttpRequesterMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; private readonly IHttpRequester _requester; public CzarHttpRequesterMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpRequester requester) : base(loggerFactory.CreateLogger<CzarHttpRequesterMiddleware>()) { _next = next; _requester = requester; } public async Task Invoke(DownstreamContext context) { var response = await _requester.GetResponse(context); if (response.IsError) { Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); SetPipelineError(context, response.Errors); return; } else if(response.Data.StatusCode != System.Net.HttpStatusCode.OK) {//如果后端未处理异常,设置异常信息,统一输出,防止暴露敏感信息 if (response.Data.StatusCode == System.Net.HttpStatusCode.BadRequest) {//提取Ids4相关的异常(400) var result = await response.Data.Content.ReadAsStringAsync(); JObject jobj = JObject.Parse(result); var errorMsg = jobj["error"]?.ToString(); var error = new IdentityServer4Error(errorMsg??"未知异常"); SetPipelineError(context, error); return; } else { var error = new InternalServerError($"请求服务异常"); Logger.LogWarning($"路由地址 {context.HttpContext.Request.Path} 请求服务异常. {error}"); SetPipelineError(context, error); return; } } Logger.LogDebug("setting http response message"); context.DownstreamResponse = new DownstreamResponse(response.Data); } } } 改造完成后,我们随时请求认证记录,最终显示效果如下。 奈斯,输出风格统一啦,这样就完美的解决了两个组件的输出问题,终于可以开心的使用啦。 四、内容总结 本篇我们详细的介绍了客户端授权的原理和支持的两个授权的方式,并各自演示了调用方式,然后知道了如何在数据库端新开通一个客户端的信息,然后介绍了配合网关实现客户端的授权和认证,并再次介绍了网关端的路由配置情况,最后介绍了如何把网关和认证统一输出格式,便于我们在正式环境的使用,涉及内容比较多,如果中间实现的有不对的地方,也欢迎大家批评指正。
原文 基于Token认证的多点登录和WebApi保护 在文章中有错误的地方,或是有建议或意见的地方,请大家多多指正,邮箱: linjie.rd@gmail.com 一天张三,李四,王五,赵六去动物园,张三没买票,李四制作了个假票,王五买了票,赵六要直接FQ进动物园 到了门口,验票的时候,张三没有买票被拒绝进入动物园,李四因为买假票而被补,赵六被执勤人员抓获,只有张三进去了动物园 后来大家才知道,当一个用户带着自己的信息去买票的时候,验证自己的信息是否正确,那真实的身份证(正确的用户名和密码),验证通过以后通过身份证信息和票据打印时间(用户登录时间)生成一个新的动物园参观票(Token令牌),给了用户一个,在动物园门口也保存了票据信息(相当与客户端和服务端都保存一份),在进动物园的时候两个票据信息对比,正确的就可以进动物园玩了 这就是我理解的Token认证.当然可能我的比喻不太正确,望大家多多谅解 下面是我们在服务端定义的授权过滤器 思路是根据切面编程的思想,相当于二战时期城楼门口设立的卡,当用户想api发起请求的时候,授权过滤器在api执行动作之前执行,获取到用户信息 如果发现用户没有登录,我们会判断用户要访问的页面是否允许匿名访问 用户没有登录但是允许匿名访问,放行客户端的请求 用户没有登录且不允许匿名访问,不允许通过,告诉客户端,状态码403或401,请求被拒绝了 如果发现用户登录,判断用户的良民证(Token令牌)是真的还是假的 用户登录,且良民证是真的,放行 发现良民证造价,抓起来,不允许访问 当然,这里可以加权限,验证是否有某个操作的权限 好了,服务端有验证了,客户端也不能拉下啊,客户端使用了动作过滤器,在用户操作之前或用户操作之后验证登录信息(这里可以加权限,验证是否有某个操作的权限) 客户端验证思路和服务端验证差不多 下面是客户端验证代码: 但是有良民证也不能也不能无限制的待在城里啊,我们做了一个时效性,在城市里什么时也不做到达一定的时长后得驱逐出城啊(类似与游戏中的挂机超过一定时间后T出本局游戏) 在这里使用的Redis记录良民证(Token),思路是用户登录之后生成的新的Token保存在Redis上,设定保存时间20分钟,当有用户有动作之后更新Redis保存有效期 下面是服务端验证token的,token有效,从新写入到Redis 以上就是Token认证 现在说说单点登录的思路 张三登录了qq:123456,生成了一个Token以键值对的方式保存在了数据库,键就是qq号,值就是qq信息和登录时间生成的一个Token 李四也登录了qq123456,qq信息是一致的,但是qq登录时间不同,生成了一个新的Token,在保存的时候发现Redis里已经存在这个qq的键了,说明这是已经有人登录了,在这里可以判断是否继续登录,登录后新的Token信息覆盖了张三登录QQ生成的Token,张三的Token失效了,当他再次请求的时候发现Token对应不上,被T下线了 多点登录也是,可以通过qq号加客户端类型作为键,这样手机qq登录的键是 123456_手机,电脑登录的键是123456_电脑,这样在保存到Redis的时候就不会发生冲突,可以保持手机和电脑同时在线 但是有一个人用手机登录qq 123456了,就会覆盖redis中键为123456_手机的Token信息,导致原先登录那个人的信息失效,被强制下线 来展示代码 判断是否可以登录 客户端类型实体 这是我们的客户端类型: 获取token需要的用户信息和登录时间的实体Model 这是我们的用户Model 生成Token用的实体 登录成功,通过JWT非对称加密生成Token 下面是JWT加密和解密的代码 将获取到的Token保存到Redis
原文:一文读懂阻塞、非阻塞、同步、异步IO 介绍 在谈及网络IO的时候总避不开阻塞、非阻塞、同步、异步、IO多路复用、select、poll、epoll等这几个词语。在面试的时候也会被经常问到这几个的区别。本文就来讲一下这几个词语的含义、区别以及使用方式。Unix网络编程一书中作者给出了五种IO模型:1、BlockingIO - 阻塞IO2、NoneBlockingIO - 非阻塞IO3、IO multiplexing - IO多路复用4、signal driven IO - 信号驱动IO5、asynchronous IO - 异步IO这五种IO模型中前四个都是同步的IO,只有最后一个是异步IO。信号驱动IO使用的比较少,重点介绍其他几种IO以及在Java中的应用。 阻塞、非阻塞、同步、异步以及IO多路复用 在进行网络IO的时候会涉及到用户态和内核态,并且在用户态和内核态之间会发生数据交换,从这个角度来说我们可以把IO抽象成两个阶段:1、用户态等待内核态数据准备好,2、将数据从内核态拷贝到用户态。之所以会有同步、异步、阻塞和非阻塞这几种说法就是根据程序在这两个阶段的处理方式不同而产生的。 同步阻塞 当在用户态调用read操作的时候,如果这时候kernel还没有准备好数据,那么用户态会一直阻塞等待,直到有数据返回。当kernel准备好数据之后,用户态继续等待kernel把数据从内核态拷贝到用户态之后才可以使用。这里会发生两种等待:一个是用户态等待kernel有数据可以读,另外一个是当有数据可读时用户态等待kernel把数据拷贝到用户态。 在Java中同步阻塞的实现对应的是传统的文件IO操作以及Socket的accept的过程。在Socket调用accept的时候,程序会一直等待知道有描述符就绪,并且把就绪的数据拷贝到用户态,然后程序中就可以拿到对应的数据。 同步非阻塞 对比第一张同步阻塞IO的图就会发现,在同步非阻塞模型下第一个阶段是不等待的,无论有没有数据准备好,都是立即返回。第二个阶段仍然是需要等待的,用户态需要等待内核态把数据拷贝过来才能使用。对于同步非阻塞模式的处理,需要每隔一段时间就去询问一下内核数据是不是可以读了,如果内核说可以,那么就开始第二阶段等待。 IO多路复用 IO多路复用也是同步的。 IO多路复用的方式看起来跟同步阻塞是一样的,两个阶段都是阻塞的,但是IO多路复用可以实现以较小的代价同时监听多个IO。通常情况下是通过一个线程来同时监听多个描述符,只要任何一个满足就绪条件,那么内核态就返回。IO多路复用使得传统的每请求每线程的处理方式得到解耦,一个线程可以同时处理多个IO请求,然后交到后面的线程池里处理,这也是netty等框架的处理方式,所谓的reactor模式。IO多路复用的实现依赖于操作系统的select、poll和epoll,后面会详细介绍这几个系统调用。 IO多路复用在Java中的实现方式是在Socket编程中使用非阻塞模式,然后配置感兴趣的事件,通过调用select函数来实现。select函数就是对应的第一个阶段。如果给select配置了超时参数,在指定时间内没有感兴趣事件发生的话,select调用也会返回,这也是为什么要做非阻塞模式下运行。 异步IO 异步模式下,前面提到的两个阶段都不会等待。使用异步模式,用户态调用read方法的时候,相当于告诉内核数据发送给我之后告诉我一声我先去干别的事情了。在这两个阶段都不会等待,只需要在内核态通知数据准备好之后使用即可。通常情况下使用异步模式都会使用callback,当数据可用之后执行callback函数。 IO多路复用 现在用Java开发的网络服务器通常采用IO多路复用的方式来加快网络IO操作,例如Netty、Tomcat等。IO多路复用的基础是select、poll和epoll。这三个函数是从操作系统的角度上支持的IO多路复用的操作,下面就分别来看一下这三个函数。 select 函数签名如下: int select(int maxfdp1, fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout) maxfdp1为指定的待监听的描述符的个数,因为描述符是从0开始的,所以需要加1readset为要监听的读描述符writeset为要监听的写描述符exceptset为要监听的异常描述符timeout监听没有准备好的描述符的话,多久可以返回,支持按照秒或者毫秒来配置时间 select操作的逻辑是首先将要监听的读、写以及异常描述符拷贝到内核空间,然后遍历所有的描述符,如果有感兴趣的事件发生,那么就返回。select在使用的过程中有三个问题:1、被监控的fds(描述符)集合限制为1024,1024太小了2、需要将描述符集合从用户空间拷贝到内核空间3、当有描述符可操作的时候都需要遍历一下整个描述符集合才能知道哪个是可操作的,效率很低。 poll 函数签名如下: int poll(struct pollfd[] fds, unsigned int nfds, int timeout); poll操作与select操作类似,仍旧避免不了描述符从用户空间拷贝到内核空间,但是poll不再有1024个描述符的限制。对于事件的触发通知还是使用遍历所有描述符的方式,因此在大量连接的情况下也存在遍历低效的问题。poll函数在传递参数的时候统一的将要监听的描述符和事件封装在了pollfd结构体数组中。 epoll epoll有三个方法:epoll_create、epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。 通过这三个方法epoll解决了select的三个问题。1、1024数量限制的问题通过epoll_create方法来创建一个epoll句柄,这个句柄监听的描述符的数量不再有限制。2、文件描述符频繁从用户空间拷贝到内核空间的问题通过观察select的操作会发现描述符从用户空间到内核空间拷贝发生在调用select方法的时候,只要没有注册新的事件或者取消注册事件,每次拷贝的描述符都是一样的。因此epoll引入了epoll_ctl调用,该方法用于注册新事件和取消注册事件。而在epoll_wait的时候并不会拷贝描述符,描述符始终存在于内核空间,当需要修改的时候只要调用epoll_ctl修改一下内核的描述符即可。如此一来便省去了描述符来回拷贝的开销。3、文件描述符可操作的时候遍历整个描述符集合的问题在调用epoll_ctl注册感兴趣的事件的时候,实际上会为设置的事件添加一个回调函数,当对应的感兴趣的事件发生的时候,回调函数就会触发,然后将自己加到一个链表中。epoll_wait函数的作用就是去查看这个链表中有没有已经准备就绪的事件,如果有的话就通知应用程序处理,如此操作epoll_wait只需要遍历就绪的事件描述符即可。 epoll在Java中的使用 目前针对Java服务器的非阻塞编程基本都是基于epoll的。在进行非阻塞编程的时候有两个步骤:1、注册感兴趣的事情;2、调用select方法,查找感兴趣的事件。 注册感兴趣的事件 我们在编写Socket的非阻塞代码的时候需要在Selector上注册感兴趣的事情,通常写法是serverSocketChannel.register(selector, SelectionKey.XXX)。来看一下这行代码背后的执行逻辑是什么样的。 注册的时候实际执行的是EPollSelectorImp。该方法主要有以下三步:1、implRegister方法。在fdToKey的Map中插入channel对应的文件描述法和SelectionKey的映射,当做注册Channel、关闭Channel、取消注册等操作是都是操作此Map。2、往pollWrapper[Epoll实例]中放入channel实例。3、往keys[HashSet]中放入SelectionKey select方法 通过Java的Selector.select方法来获取准备好的键的时候实际执行的代码如下: 首先调用EPollArrayWrapper的poll方法,该方法做两件事:1、调用epollCtl方法向epoll中注册感兴趣的事件;2、调用epollWait方法返回已就绪的文件描述符集合然后调用updateSelectedKeys方法调用把epoll中就绪的文件描述符加到ready队列中等待上层应用处理, updateSelectedKeys通过fdToKey查找文件描述符对应的SelectionKey,并在SelectionKey对应的channel中添加对应的事件到ready队列。 水平触发LT与边缘触发ET epoll支持两种触发模式,分别是水平触发和边缘触发。 LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。 ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核会通知你一次,并且除非你做了某些操作导致那个文件描述符不再为就绪状态了,否则不会再次发送通知。 可以看到,本来内核在被DMA中断,捕获到IO设备来数据后,只需要查找这个数据属于哪个文件描述符,进而通知线程里等待的函数即可,但是,LT要求内核在通知阶段还要继续再扫描一次刚才所建立的内核fd和io对应的那个数组,因为应用程序可能没有真正去读上次通知有数据后的那些fd,这种沟通方式效率是很低下的,只是方便编程而已; JDK并没有实现边缘触发,关于边缘触发和水平触发的差异简单列举如下,边缘触发的性能更高,但编程难度也更高,netty就重新实现了Epoll机制,采用边缘触发方式;另外像nginx等也采用的是边缘触发。 ---------------------------------------------------------------- 欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。
原文:如何构建高性能MySQL索引 介绍 上一篇文章中介绍了MySQL的索引基本原理以及常见的索引种类,这边文章的重点在于如何构建一个高性能的MySQL索引,从中你可以学到如何分析一个索引是不是好索引,以及如何构建一个好的索引。 索引误区 多列索引 一个索引的常见误区是为每一列创建一个索引,如下面创建的索引: CREATE TABLE `t` ( `c1` varchar(50) DEFAULT NULL, `c2` varchar(50) DEFAULT NULL, `c3` varchar(50) DEFAULT NULL, KEY `c1` (`c1`), KEY `c2` (`c2`), KEY `c3` (`c3`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; t表里有三列,并且为每列创建了一个索引。创建索引的人为了能够快速访问表中的任何一列,因此为每一列添加了一个单独的索引。在多个列上创建索引通常并不能很好的提高MySQL查询性能,虽然说MySQL 5.0之后引入了索引合并策略,可以将多个单列索引合并成一个索引,但这并不总是有效的。同时创建多个索引的时候还会增加数据插入的成本,在插入数据的时候需要同时维护多个索引的写入操作。 索引的计算 看下面这条sql语句: select name from student where id + 1 = 5 即使我们在student表的id列上建立索引,上面的这条SQL语句也无法使用索引。SQL语句中索引字段不能是表达式的一部分,也不能是函数的参数。 索引的长度以及选择性 尽量不要在一个很长的列上使用索引,否则会导致索引占用的空间很大,同时在进行数据的插入和更新的时候意味着更慢的速度。因此使用uuid列作为索引并不是一个好的选择。从上一篇文章中我们可以知道,为了加快数据的访问索引是需要常驻内存的,假如说我们把64位uuid作为索引,那么随着表中数据量的增加索引的大小也在急剧增加。同时因为uuid并没有顺序性,因此在数据插入的时候都需要从根节点找到当前索引的插入位置,如果同一个节点中的索引大小达到上限,还会导致节点分裂,更加降低了插入速度。 创建索引另外一个需要考虑的是索引的选择性,通常情况下我们会使用选择性高的列作为索引,但是也不一定一直是这样,下一节会介绍如何权衡索引的选择性。 创建高性能索引 选择正确的索引顺序 在选择索引的顺序的时候有一个原则:将索引选择性最高的列放在左侧,同时索引的顺序要与查询索引的顺序一致,并且要兼顾考虑排序和分组的需要。在一个多列B树多列中索引的顺序意味着索引首先按照最左侧的列进行排序,其次是第二列。所以无论是where语句还是order by语句都需要尽量满足这个顺序,这样才能更好的使用索引。 索引的选择性 列的选择性高的含义是通过这一列能够更多的过滤掉无用的数据,举个极端的例子,如果把自增id建成索引那么它的选择性是最高的,因为会把无用的数据都过滤掉,只会剩下一条有效数据。我们可以通过下面的方式来简单衡量某一个列的选择性: select count(distinct columnA)/count(*) as selectivity from table 当上面的数据越大的时候意味着columnA的选择性越高。这种方式提供了一个衡量平均选择性的办法,但是也不一定是有效的,需要具体情况具体分析。 前缀索引 当遇到特别长的列,但又必须要建立索引的时候可以考虑建立前缀索引。前缀索引的含义是把某一列的前N个字符作为索引,创建前缀索引的方式如下: alter table test add key(columnA(5)); 上面这个语句就是把columnA的前5个字符创建为前缀索引。前缀索引是一种使索引更小、更快的有效办法。但是前缀所有有一个缺点:MySQL无法使用前缀索引来做order by和group by,也无法使用前缀索引做覆盖扫描。 聚簇索引和非聚簇索引 聚簇索引 聚簇索引代表一种数据的存储方式,表示同一个结构中保存了B-Tree索引和数据行。也就是说当建立聚簇索引的时候实际的数据行存放在索引的叶子节点上。这也决定了每个表只能有一个聚簇索引。聚簇索引组织数据的方式如下图所示: 从图中可以看到索引的叶子节点和数据行是存放在一起的,这样的好处是可以直接读取到数据行。在创建表的时候如果我们不显式指定聚簇索引,那么MySQL将会按照下面的逻辑来选择聚簇索引:首先会通过主键列来聚集数据,如果没有主键列那么会选择唯一的非空索引来替代。如果还没有这样的索引那么会隐式的创建一个主键列来作为聚簇索引。 聚簇索引优点:1、相关数据存放在一起,检索的时候降低IO的次数2、数据访问更快3、使用覆盖索引扫描的查询可以直接使用节点中的主键值 在使用上面的优点的时候聚簇索引也有一定的缺点:1、聚簇索引将数据聚集在一起限制了插入速度,插入速度比较依赖于主键的顺序2、更新索引的时候代价会变高3、二级索引的访问的时候需要查找两次 非聚簇索引 非聚簇索引通常被称为二级索引,与聚簇索引的不同在于,非聚簇索引的叶子节点存放的是数据的行指针或者是一个主键值。这样在查找数据的时候首先定位到叶子节点上的主键值(或者行指针),然后通过主键值再到聚簇索引中查找到对应的数据。从中我们可以看到对于非聚簇索引的查询需要走两次索引。下图是一个非聚簇索引: 这个索引是InnoDB中的耳机索引,叶子节点中存储的是索引和主键。对于MyISAM叶子节点存储的是索引和行指针。 覆盖索引 如果一个索引包含或者说覆盖所有需要查询的字段的值,那么就称为覆盖索引。覆盖索引可以极大的提高查询的效率,如果我们的查询中只查询索引,而不用去回表那应该最好不过了。 通常我们使用explain关键字来查看一个查询语句的执行计划,通过执行计划我们可以了解到查询的细节。如果是覆盖索引,我们会看到执行计划的Extra列里有”Using Index”的信息。在查询语句中一般我们希望是where条件中的语句尽量能被覆盖,并且顺序要跟索引的保持一致。还有一个需要注意的点是MySQL不能在索引中使用like操作,这样会导致后面的索引失效。 后记 本文主要讲了几种索引的原理以及如何构建一个高性能的索引。索引的优先是一个渐进的过程,随着数据量和查询语句的不同而发生变化,重要的是了解索引的原理,这样做出正确的优化。下一篇文章中将会介绍explain关键字,教你如何来看执行计划,以及如何判断一个查询语句是否需要优化的。 ---------------------------------------------------------------- 欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。
原文:MySQL索引基础 介绍 索引用于加快数据访问的速度。把计算机的磁盘比作一本字典,索引就是字段的目录,当我们想快速查到某个词语的时候只需要通过查询目录找到词语所在的页数,然后直接打开某页就可以。MySQL最常用的索引是B+树索引,为什么使用B+作为MySQL的索引,这是许多面试官必问的问题。 为什么B+树 硬件相关知识 计算机的磁盘是一个圆盘的接口,圆盘上有一个个的圆圈,数据就是记录在这些圆圈的扇区上。如下图所示当计算机系统读取数据的时候要通过以下几个步骤:1、首先移动臂根据柱面号使磁头移动到所需要的柱面上,这一过程被称为寻道。所耗费的时间叫寻道时间(ts)。2、目标扇区旋转到磁头下,这个过程耗费的时间叫旋转时间。 因此访问磁盘的时间由三部分构成: 寻道时间+旋转时间+数据传输时间 第一部分寻道时间延迟最高,最大可达到100ms,旋转时间取决于磁盘的转速,转速在7200转/分钟的磁盘平均旋转时间在5ms左右。磁盘的读取是以block(盘块)为单位的,位于同一个盘块的数据可以一次性读取出来。在读写数据的时候尽量减少磁头来回移动的次数,避免过多的查找时间。如果每次从磁盘上读取数据的时候都要经历上面的几个过程那么效率上无疑是极低的。 为什么B+树 从上面可以看到,如果随机访问磁盘的速度是很慢的,因此需要设计一个合理的数据结构来减少随机访问磁盘的次数。B树就是这样一种数据结构。 B树、B+树介绍 B树 B树是为存储设备而设计的一种多叉平衡查找树。它与红黑树类似,但是在降低IO操作方面B树的表现要更好一些,B树与红黑树最大的区别在于B树可以有多个子节点,红黑树最多是有两个子节点,这就决定了大多数情况下B树的高度要比红黑树低很多,因此在查找的时候能够降低IO次数。下图是一棵B树:B 树又叫平衡多路查找树。一棵m阶的B树的特性如下: a.树中每个结点最多含有m个孩子(m>=2); b.除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数); c.若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点); d.所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息 e.每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中: a) Ki (i=1…n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。 b) Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。 c) 关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1。 B树中的每个节点都尽可能存储多的关键字信息和分支信息,但是不会超过磁盘块的大小。这样在有效降低了树的高度,在查找的时候可以快速定位在指定的磁盘块。假如要从上图中找到79这个数字,首先从根节点开始扫描,79大于35所以选择P3指针,指向磁盘块4,在磁盘块4中79在65和87之间,因此选择P2指针,选择磁盘块10,这时候就可以从磁盘块10中找到79。整个过程只需要3次IO,如果这棵树被缓存在内存中,那么只需要一次IO就可以读到79这个数字。 B+树 B+树是B的变种,一颗m阶B+树和m阶B树的异同点在于: 1、有n棵子树的节点中有n-1个关键字(与B树n棵子树有n-1个关键字,保持一致) 2、所有的叶子节点中包含了全部的关键字的信息,以及指向含有这些关键字记录的指针,且叶子节点本身依关键字的大小而自小而大顺序链接(而B树的叶子节点并没有包含全部需要查找的信息) 3、所有的非终端节点可以看成索引部分,节点中仅含有其子树根节点中最大或者最小的关键字(而B树的非终节点也要包含需要查找的有效信息) 由于B+树的叶子节点是连接在一起的,因此相对于使用B树作为索引,对于MySQL的范围查询更加优化。同时由于叶子节点包含所有关键字信息,因此有的查询语句就不需要回表,只需要查询索引就可以查到需要的数据。 索引类型 B树索引 虽然是叫B树索引,但是数据库实际上使用的是B+树来组织数据。B树索引意味着所有值都是按照顺序存储的,并且每个叶子节点到根节点的距离是相同的。假如有如下数据表: CREATE TABLE `people` ( `last_name` varchar(50) DEFAULT NULL, `first_name` varchar(50) DEFAULT NULL, `dob` date DEFAULT NULL, `gender` enum('m','f') DEFAULT NULL, KEY `last_name` (`last_name`,`first_name`,`dob`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 该表对last_name,first_name,dob三列建立了索引,索引的组织方式如下:当同时对多列进行索引的时候,索引的顺序是非常重要的,上面的索引首先是按照last_name进行索引,在last_name相同的情况下在对first_name进行排序,最后是dob字段。 B树索引适用于全键值、键值范围和最左前缀查找:全键值 查找名字为Allen Kim,出生日期为1930-07-12的人,这样的查找方式匹配了索引中的所有字段,依次扫描索引中的last_name、first_name和dob字段,找到对应的数据。键值范围 查找姓名在Allen和Barrymore之间的人,这种查找方式也会使用到索引。需要注意的是这里只能是索引中的第一列,也就是last_name的范围查找。前缀匹配 查找last_name是以Al开头的人,这种查询会以此扫描索引中的节点,然后选出来对应的复合条件的行。也是只能使用第一列的前缀查询。如果是说想查first_name的前缀匹配,那么是无法使用到索引的,意味着要进行全表扫描。精确匹配某一列,范围批量另外一列 精确匹配的列必须是所以中的第一列,范围匹配的列是第二列,这样才能使用到上面的索引。 B树索引的使用限制:1、不是按照最左列开始查询的,无法使用索引。2、不能跳过索引的列进行查询。3、如果使用到了范围匹配,那么范围匹配右边的列都无法使用索引查询。 哈希索引 哈希索引使用哈希表来实现,只有是精确匹配的时候才会生效。存储引擎会对索引列计算出一个哈希值,然后保存一个哈希值到行数据的指针。哈希索引由于其特殊的组织方式,限制了其使用场景。哈希索引只适合值比较少的情况,例如枚举类型。在以下几种方式中是不适合使用哈希索引的:1、哈希索引只包含哈希值和指针,不存储字段值,因此使用哈希索引避免不了要进行回表查询。2、哈希索引数据并不是按照值的顺序进行排序的,因此哈希索引无法用来排序3、哈希索引不支持部分索引列匹配。比如说在(A,B)两列上简历哈希索引,那么只有在同时使用A、B两列查询的时候才会使用哈希索引,只使用A列查询无法使用哈希索引。4、哈希索引只支持等值比较,不支持像between and这种范围查询。5、使用哈希索引的时候应该尽量避免哈希冲突。 后记 数据库的索引机制解决的问题是在访问内存数据与磁盘数据的速度差别很大的情况下,如何快速访问数据的问题。只有了解了索引的原理才可以更好的设计表的索引字段以及写出性能更优的查询语句。在我们写SQL语句的时候头脑中应该大体上能规划出查询数据以及如何使用索引的过程。下一篇会介绍一下高性能索引的策略,带你设计出更优的索引。 ---------------------------------------------------------------- 欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。
原文:Mysql 多主一从数据备份 Mysql 多主一从数据备份 概述 对任何一个数据库的操作都自动应用到另外一个数据库,始终保持两个数据库中的数据一致。 这样做有如下几点好处: 可以做灾备,其中一个坏了可以切换到另一个。 可以做负载均衡,可以将请求分摊到其中任何一台上,提高网站吞吐量。 对于异地热备,尤其适合灾备。 MySQL 主从复制的方式 1 Master 数据库操作的任何数据库的时候,都会将操作记录写入到biglog日志文件当中 2 Slave 数据库通过网络获取到主数据库的日志文件,写入本地日志系统 ,然后一条条的将数据库事件在数据库中完成 3 Slave 重做中继日志中的事件,将 Master 上的改变反映到它自己的数据库中,所以两端的数据是完全一样的。 环境 操作系统:CentOS MySQL版本:mysql-5.6.26 (主从两者数据库版本必须保持一致) Master1 配置 1 开启binlog日志功能 vim /etc/my.cnf server-id=6 log-bin=mysql-bin 2 重启mysql 登陆并授权 mysql -uroot -p123456 grant replication slave, replication client on *.* to 'repl'@'10.211.55.7' identified by '123456'; ip地址为slave服务器的ip地址 3 查看日志状态 show master status; Master2 配置 1 开启binlog日志功能 vim /etc/my.cnf server-id=8 log-bin=mysql-bin 2 重启mysql 登陆并授权 mysql -uroot -p123456 grant replication slave, replication client on *.* to 'repl'@'10.211.55.7' identified by '123456'; ip地址为slave服务器的ip地址 3 查看日志状态 show master status; Slave 配置 1 修改配置文件 (注意 slave的默认数据库启动的端口必须关闭 service mysql stop) vim /etc/my.cnf [mysqld] binlog-ignore-db=mysql binlog_format=mixed expire_logs_days=7 slave_skip_errors=1062 relay_log=mysql-relay-bin log_slave_updates=1 [mysqld_muliti] mysqld=/usr/bin/mysqld_safe mysqladmin=/usr/bin/mysqladmin user=root password=123456 [mysqld6] port=3306 datadir=/home/mysql/data6 pid-file=/home/mysql/data6/mysql.pid socket=/home/mysql/data6/mysql.sock user=mysql server-id=7 [mysqld8] port=3307 datadir=/home/mysql/data8 pid-file=/home/mysql/data8/mysql.pid socket=/home/mysql/data8/mysql.sock user=mysql server-id=7 2 初始化生成目录 /usr/local/mysql/scripts/mysql_install_db --user=mysql --basedir=/usr/local/mysql --datadir=/home/mysql/data6 & /usr/local/mysql/scripts/mysql_install_db --user=mysql --basedir=/usr/local/mysql --datadir=/home/mysql/data8 & 3 修改目录权限 chown -R mysql /home/mysql/data6 chown -R mysql /home/mysql/data8 4 启动服务 mysqld_multi --defaults-file=/etc/my.cnf start 6 mysqld_multi --defaults-file=/etc/my.cnf start 8 5 登录测试(并分别做授权) mysql -P 3306 -S /home/mysql/data6/mysql.sock mysql> change master to master_host='10.211.55.6', master_user='repl', master_password='123456', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos=120; mysql> start slave; mysql -P 3307 -S /home/mysql/data8/mysql.sock mysql> change master to master_host='10.211.55.8', master_user='repl', master_password='123456', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos=120; mysql> start slave; ok 就这样 完成了
原文:以Windows服务方式运行ASP.NET Core程序 我们对ASP.NET Core的使用已经进行了相当一段时间了,大多数时候,我们的Web程序都是发布到Linux主机上的,当然了,偶尔也有需求要发布到Windows主机上,这样问题就来了,难道直接以控制台形式运行这个Web程序吗? 直接以控制台形式运行程序当然是可以的,但有以下问题: 需要敲命令行(这个可以通过制作一个快捷方式解决) 用户也许会说有一个“黑黑的DOS窗口”,很奇怪 用户可能会随手把这个“黑黑的DOS窗口”关掉导致程序结束 不能够自动地随系统启动,得用户登录后再跑程序 如果我们把程序以Windows服务的方式来运行,那以上这些问题都没有了。 微软官方有篇文章,关于如何做这个事情的:https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/windows-service?view=aspnetcore-2.1 我也主要是参考了这篇文章,但其中还有些小小的坑,本文将会提到。OK,Let's get down to work. 包与生成目标 生成目标当然选择最新的.NET Core 2.1了,包则需要引入Microsoft.AspNetCore.Hosting.WindowsServices,最新版是2.1.1(2018/6/19更新,不就是今天嘛),如果你尝试使用最新的2.1.1版,那会出问题,报依赖冲突,你根据报错内容把ASP.NET 2.1.0升级到ASP.NET 2.1.1去,但依旧会出现问题: It was not possible to find any compatible framework versionThe specified framework 'Microsoft.AspNetCore.All', version '2.1.1' was not found. 看来需要安装新的SDK才行,但微软官网最新的正式版SDK就是2.1.300啊,这个还是等等吧,好,果断把版本降回2.1.0,这回没问题了。 修改Main入口 简单,非常简单,把Run改为RunAsService即可。 但这样会导致新的问题,你在开发过程中调试程序的时候会出现这样的错误:Cannot start service from the command line or a debugger. A Windows Service must first be installed and then started with the ServerExplorer, Windows Services Administrative tool or the NET START command. 程序跑不起来,意思显而易见,所以我们在调试中还是要以普通的Run方式。所以改成这样: IWebHost host = WebHost.CreateDefaultBuilder(args) .UseKestrel(options => { options.Listen(IPAddress.Any, 5201); }) //5201是我用的端口,你改成自己的 .UseStartup<Startup>() //使用Startup配置类 .Build(); bool isService = !(Debugger.IsAttached || args.Contains("--console")); //Debug状态或者程序参数中带有--console都表示普通运行方式而不是Windows服务运行方式 if (isService) { host.RunAsService(); } else { host.Run(); } 配置服务 ASP.NET Core publish生成的目标中并不包含exe文件,所以按照官网文档去弄的话可能会卡住,这是正确的姿势: sc create MyService binPath= "\"C:\program files\dotnet\dotnet.exe\" \"D:\Services\MyService.dll\"" DisplayName= "MyService" start= auto 注意1:必须以管理员身份运行上面命令注意2:binPath参数、DisplayName参数及start参数的等号后面必须带一个空格(官网文档也特别提起了这点) 启动服务: sc run MyService 停止服务: sc stop MyService 卸载服务: sc delete MyService 其它 默认情况下,Windows系统服务是以system用户身份运行的,如果你需要切换身份运行,可以在“控制面板 - 管理工具 - 服务”中找到MyService,打开属性面板,在“登录”Tab中指定特定身份。
原文:以Windows服务方式运行.NET Core程序 在之前一篇博客《以Windows服务方式运行ASP.NET Core程序》中我讲述了如何把ASP.NET Core程序作为Windows服务运行的方法,而今,我们又遇到了新的问题,那就是:我们的控制台程序,也就是普通的.NET Core程序(而不是ASP.NET Core程序)如何以服务的方式运行呢? 这个问题我们在.NET Core之前早就遇到过,那是是.NET Framework的时代(其实距今也没多远啦),我们是用一个第三方的组件——Topshelf,来解决这个问题的,Topshelf的官网是:http://topshelf-project.com/,它的使用很简单,官网上有具体的描述,对于一个普通的控制台程序而言(通常是一个不需要图形界面的服务),开发和调试的时候,把它当做一个普通的控制台程序来使用,十分方便;而实际部署的时候,通过传入不同的命令行参数,可以使它有了新的行为:安装Windows服务、运行Windows服务、停止/重启Windows服务或者卸载Windows服务。进入跨平台的.NET Core时代之后,Topshelf自然有了支持.NET Core的版本,使用方法与之前的类似,具体在此不表了,因为接下来我们根本不打算使用它! 现在我想要的是:不要引入任何组件,不要对现在控制台程序进行任何修改(ASP.NET Core程序也是控制台程序),开发调试时候不要进行任何复杂的参数配置,一切照旧,仅仅是在部署阶段,把程序当做Windows服务去运行。——你嘚讲吼不吼? 要达到这个目标,就要借助一个神器了,此神器为NSSM,Non-Sucking Service Manager,名字有点拗口,翻译成中文就是:不嗝屁服务管理器。 NSSM的官网是:https://nssm.cc/,十分简陋,但程序功能可是非常强大和全面的,下面我来一步步演示它如何使用。 1,先构建一个简单的服务程序 构建一个简单的服务程序,程序功能描述:程序没有图形界面,仅仅是定时记录一些日志(5秒钟写一下日志),在用户按下<Ctrl>+<C>的时候,程序退出。功能明确,Okay,let's get down to work. 1. 创建一个.NET Core Application,叫MyService 2. Nuget引入Quartz和NLog.Extensions.Logging,一个用来做定时任务,另一个用来log 3. 另外,程序使用了依赖注入,还需要用Nuget引入Microsoft.Extensions.DependencyInjection 4. 给项目增加NLog.Config配置文件,内容是 <?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" throwExceptions="false" internalLogLevel="Off"> <variable name="theLayout" value="${date:format=HH\:mm\:ss.fff} [${level}][${logger}] ${callsite:className=False:fileName=True:methodName=False} ${message} ${onexception:${newline}}${exception:format=Message,ShortType,StackTrace:innerFormat=Message,ShortType,StackTrace:separator=\r\n:innerExceptionSeparator=\r\n---Inner---\r\n:maxInnerExceptionLevel=5}"/> <targets> <target name="asyncFile" xsi:type="AsyncWrapper"> <target name="logfile" xsi:type="File" fileName="${basedir}/log/${shortdate}.log" layout="${theLayout}" encoding="UTF-8" /> </target> <target name="debugger" xsi:type="Debugger" layout="${theLayout}" /> <target name="console" xsi:type="Console" layout="${theLayout}" /> <target name="void" xsi:type="Null" formatMessage="false" /> </targets> <rules> <logger name="Quartz.*" minlevel="Trace" maxlevel="Info" writeTo="void" final="true" /> <logger name="*" minlevel="Debug" writeTo="asyncFile" /> <logger name="*" minlevel="Trace" writeTo="debugger"/> <logger name="*" minlevel="Trace" writeTo="console"/> </rules> </nlog> 还要注意的是这个文件必须复制到生成目录去以便程序运行时候能够加载到。 5. 增加MyServiceJobFactory.cs using Quartz; using Quartz.Spi; using System; namespace MyService { class MyServiceJobFactory : IJobFactory { protected readonly IServiceProvider _container; public MyServiceJobFactory(IServiceProvider container) { _container = container; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { return _container.GetService(bundle.JobDetail.JobType) as IJob; } public void ReturnJob(IJob job) { } } } 6. 增加PeriodLoggingJob.cs using Microsoft.Extensions.Logging; using Quartz; using System; using System.Threading.Tasks; namespace MyService { class PeriodLoggingJob : IJob { private readonly ILogger<PeriodLoggingJob> _logger; public PeriodLoggingJob(ILogger<PeriodLoggingJob> logger, IServiceProvider serviceProvider) { _logger = logger; } private void DoLoggingJob() { _logger.LogInformation("logging..."); } public Task Execute(IJobExecutionContext context) { try { DoLoggingJob(); } catch (Exception ex) { //必须妥善处理好定时任务中发生的异常 _logger.LogError(ex, "执行定时任务发生意外错误"); } returnTask.CompletedTask; } } } 7. Program.cs的完整内容如下 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; using Quartz; using Quartz.Impl; using Quartz.Spi; using System; using System.Collections.Specialized; using System.IO; using System.Threading; namespace MyService { class Program { //注册各种服务 static void RegisterServices(IServiceCollection services) { //日志相关 services.AddSingleton<ILoggerFactory, LoggerFactory>(); services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)); //定时任务相关 services.AddSingleton<IJobFactory, MyServiceJobFactory>(); services.AddSingleton<PeriodLoggingJob>(); } static void Main(string[] args) { //注册退出事件处理(响应<Ctrl>+<C>) ManualResetEvent exitEvent = new ManualResetEvent(false); Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs e) { e.Cancel = true; exitEvent.Set(); }; //处理其它程序关闭事件(如kill),使得程序可以优雅地关闭 AppDomain.CurrentDomain.ProcessExit += (sender, e) => { exitEvent.Set(); }; //容器生成 ServiceCollection services = new ServiceCollection(); RegisterServices(services); using (ServiceProvider container = services.BuildServiceProvider()) { //日志初始化 var loggerFactory = container.GetRequiredService<ILoggerFactory>(); loggerFactory.AddNLog(new NLogProviderOptions { CaptureMessageTemplates = true, CaptureMessageProperties = true }); string nlogConfigFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NLog.config"); NLog.LogManager.LoadConfiguration(nlogConfigFile); //记录启动日志 ILogger<Program> logger = container.GetService<ILogger<Program>>(); logger.LogInformation("MyService启动."); //定时任务配置 NameValueCollection props = new NameValueCollection { { "quartz.serializer.type", "binary" } }; StdSchedulerFactory schedulerFactory = new StdSchedulerFactory(props); IScheduler scheduler = schedulerFactory.GetScheduler().Result; scheduler.JobFactory = container.GetService<IJobFactory>(); //每天1:00执行APP状态更新任务 ITrigger periodLoggingJobTrigger = TriggerBuilder.Create().WithIdentity("PeriodLoggingJobTrigger") .StartNow().WithSimpleSchedule(x=>x.WithIntervalInSeconds(5).RepeatForever()).Build(); IJobDetail checkPasswordOutOfDateJob = JobBuilder.Create<PeriodLoggingJob>().WithIdentity("PeriodLoggingJob").Build(); scheduler.ScheduleJob(checkPasswordOutOfDateJob, periodLoggingJobTrigger); //开启定时服务 scheduler.Start(); //----------------------------------------↑↑↑ 程序开始 ↑↑↑---------------------------------------- exitEvent.WaitOne(); //----------------------------------------↓↓↓ 程序结束 ↓↓↓---------------------------------------- //定时任务结束 scheduler.Shutdown(); //记录结束日志 logger.LogInformation("MyService停止."); } } } } 这就是整个服务程序的完整内容,本来我可以提供一个更简单的程序,这里啰里啰嗦写了这么一大堆,目的还是让初学者更加清楚.NET Core的程序结构和运行方式。其中内容包括:NLog的使用、Quartz的使用、容器及依赖注入的入门例子、如何处理程序关闭事件等,也许你想问“为什么要引入Quartz,搞这么复杂,弄个Timer不行吗?”当然行,但Quartz更强大,而且更适合给大家演示容器与依赖注入的使用。 8. 试运行程序 运行这个程序,输出几条日志信息后,以<Ctrl>+<C>来结束程序的运行,这样会在程序目录下产生log目录及日志文件,文件的内容大致如下: 19:03:37.117 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:55) MyService启动. 19:03:37.637 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:42.536 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:47.535 [Info][MyService.PeriodLoggingJob] (d:\work\MyService\MyService\PeriodLoggingJob.cs:15) logging... 19:03:49.293 [Info][MyService.Program] (d:\work\MyService\MyService\Program.cs:80) MyService停止. 9. 发布程序 选择publish,在publish的目标目录下产生一堆文件,将这些文件复制到D:\Service\MyService目录下,一会儿我们要用到这个目录。 2,NSSM配置 首先要获取NSSM程序,当然是要到官网下载,版本选择最新版,尽管它声称是pre-release版,但功能杠杠的,没有任何影响,而正式版(非pre-release)则是2014年的了,太旧了。下载下来后找到对应的exe文件,叫nssm.exe。(注意有32位版和64位版的分别) 它是个绿色软件,不需要安装,仅此一个exe文件,把这个文件复制到C:\Windows\System32目录下,之后经常要用。 在Windows命令行中直接敲nssm,会出现它的帮助提示。 1. 安装服务 >nssm install MyService 出现配置界面(注意,需要管理员权限) 配置选项比较多,这是我的配置,供参考: 点“Install service”即将服务安装好了。我们打开Windows服务来查看所安装的服务: 服务已经安装完毕,一切准备就绪。 2. 启动服务 >nssm start MyService 其它一些操作其实不用我说大家也应该知道了: nssm status MyService 查看服务状态 nssm stop MyService 停止服务 nssm restart MyService 重启服务 nssm edit MyService 重新配置服务的参数 nssm remove MyService 删除服务 其余的请自行参考nssm的使用手册。注意事项:需要用管理员身份来执行上面这些命令,否则会出现访问拒绝的错误。 3,分享一些想法 2018年快过去了,回顾这一年来,我觉得我在公司所做的最大且重要的一件事情就是推动了.NET Core的应用,将能迁移的.NET Framework的程序都迁移至.NET Core了,为什么要这么干?最最主要的原因当然是要跨平台,原先ASP.NET开发的网站,只能运行于Windows平台,它们得依赖于IIS!Windows(作为服务器)本身就是一个非常复杂的系统,有着各种令人眼花缭乱的配置,加上IIS,就更加令人感到困惑,我同意IIS是功能强大的服务器程序,但它真的过于复杂,设计不合理,很难用,让我等菜鸟频频掉到它的坑里爬不出来。IIS并不是一个能够自由选择版本的软件,它的版本通常认为与Windows操作系统绑定,微软官方并不建议安装与Windows操作系统原生版本不一致的IIS,所以现在甚至还有公司继续在用IIS6,而各个版本的IIS的行为却不尽相同,默认IIS并不带安装ASP.NET组件,所以在Windows系统和IIS刚部署好的时候,想直接运行ASP.NET网站居然还不行,要自己去安装ASP.NET的支持,完成后还需要使用一条额外的命令来注册ASP.NET组件,另外还可能遇到稀奇古怪的问题,大多数问题可以通过安装若干个补丁解决(如ASP.NET MVC的路由不起作用导致网站无法访问的问题),而有时则不会那么顺利,你得仔细看看这些补丁是否符合当前操作系统及IIS版本,甚至操作系统的语言版本也会影响你所要安装的补丁。IIS与ASP.NET程序之间的关系也是令人很懵逼,我想让我的ASP.NET程序自始至终运行着就是做不到,尽管应用程序池里似乎有这个选项,我在StackOverflow上针对相关问题进行过讨论,有不少人顶我,但也有人说不行(我猜跟IIS版本还有关系),ASP.NET程序空闲一段时间后便被IIS踢掉——即便你的主机不差内存,你无法肯定IIS一运行你的程序就跟着跑起来,也无法肯定你的程序什么时候在运行,什么时候被踢掉,这是个类似薛定谔的猫的问题,你的ASP.NET程序就通常处于这么一种“叠加态”,你得看一看才知道确切它是否在运行,这一看,才使得程序从“叠加态”坍缩为“生态”或“死态”,且从“死态”转入“生态”还需要耗费好些时间,表现为第一次打开页面时候的长时间卡顿,跟客户演示系统,有时候会很尴尬。我曾经为了让程序不被IIS踢掉,还手工写了一个KeepAlive的小程序,定时去get我的网站的首页,实在奇葩。微软对此的解释是:IIS并不是为long-term程序设计的,你想在IIS里做一个准时的定时服务,那是相当不妥,根本不是为这种事情设计的,所以不好用不能怪我。我承认这当然是一种设计,但ASP.NET网站除了提供网页之外,跑一些后台服务也应该是很正常的吧?没办法,于是我将服务和网站分开,中间用总线沟通,听起来很cool?——其实这是一段悲伤的往事,不过说来话长,以后有机会再提了。.NET Core出现了,ASP.NET Core也和它一起到来,2.0版开始就是一个很完善的版本,我想是时候上了,这是工作量很大的差事,但为了将来更好的发展,我们必须经历这个艰难的爬坡,所幸的是现在一切都已转入正轨,我预想的目的达到了。.NET Core的一大特点就是程序都可以独立运行,包括ASP.NET Core程序,不再依赖于IIS,我可以根据业务的需要,将系统划分为多个模块,方便开发分工和测试,这些模块甚至不需要部署在同一台主机上,极大提高了灵活性。一般来说,我还是推荐将程序部署至Linux环境,理由依旧是Linux作为服务器操作系统的使用体验远远好于Windows,Windows实在太过复杂了!但也有例外,如果遇到缺乏Linux支持技术的客户的情况,那就把程序部署到他们的Windows主机上吧,无所谓,反正.NET Core是跨平台的。不知这是不是我2018年的最后一篇博客,如果是,上面这段文字就算是我对今年自己的主要工作总结吧。
原文:.NET Core跨平台部署 .NET Core跨平台部署 1. Windows-IIS 大家对于在IIS上部署.NET站点已经驾轻就熟了,部署.NET Core也没有什么本质区别,但是这其中仍然有一些细节是不同的,下面记录了一些我在部署时遇到的问题 1.1 安装.NET Core Windows Server Hosting 要在IIS上运行ASP.NET Core,必须安装.NET Core Windows Server Hosting 安装完成后最好重启IIS 如果没有安装该组件就直接打开部署的网站会出现 500.19 相关的配置数据无效 1.2 配置应用程序池 Core的IIS站点应用程序池的.NET CLR版本要选择 无托管代码 1.3 使用发布文件 我最开始测试的时候,仍然使用Web根目录作为网站的物理路径,但是网站无法访问,报HTTP403错误——Web 服务器被配置为不列出此目录的内容,也是就是这个文件夹下没有可以访问的文件,在查阅网上的资料后发现其他人都是使用了发布文件夹作为物理路径,生成发布版本设置相应路径后.NET Core的示例站点即可正常访问 发布文件夹结构 成功访问 2 Linux 微软官方给出了不同系统的部署方法Tutorial Guide,由于Linux有不同的版本,所以这里选择CentOS作为示例,有以下几个步骤 2.1 添加.NET产品依赖 在安装.NET之前,你需要注册微软的Key,注册产品仓库,并且安装需要的依赖,在每台机器上只需要做一次。 直接执行以下命令: sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm 2.2 安装.NET SDK 更新可供安装的产品,然后安装.NET SDK 输入以下命令: sudo yum update sudo yum install dotnet-sdk-2.2 中间有两次手动确认,然后等待安装完成即可 2.3 创建你的应用 通过输入命令就可以创建一个官方的示例.NET Core程序 dotnet new console -o myApp cd myApp 第一条命令新建应用,第二条进入应用文件夹 通过 ls 命令我们可以看到该文件夹下只有两个文件,obj是文件夹 默认的主文件Program.cs的内容如下: using System; namespace myApp { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } } 2.4 运行应用 dotnet run 2.5 创建web应用 使用mkdir命令新建一个文件夹mvc,然后进入目录 创建网站 dotnet new mvc 然后发布这个网站程序 dotnet restore dotnet publish -c release 默认的发布目录是在/bin/release/netcoreapp2.x/publish/里,可以新建一个目录拷贝进去 scp -r /root/mvc/bin/release/netcoreapp2.2/publish/* /root/www/firstapp 2.6 从外网访问web应用 完成发布后,已经可以通过执行dotnet命令来启动网站了,但是只能在内网访问,显然这不是我们想要的,要想从外网访问,我们需要反向代理服务器,这里选择Nginx 使用yum命令远程安装 sudo yum install epel-release yum install nginx 启动 systemctl start nginx #启用Nginx systemctl enable nginx #设置开机启动 这时候已经可以直接通过服务器的IP地址的80端口访问Nginx的测试页了,需要注意的是如果使用阿里云服务器,需要在安全组配置中开放80端口才能够访问 接下来根据需要进行一些端口的配置,dotnet默认的访问端口为5000,但是我测试的时候好像是在linux上被占用了,所以对 Program.cs 进行修改,使其可以通过其他端口访问,这里使用8080 public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseUrls("http://*:8080") .UseStartup<Startup>(); } 改完之后需要重新生成发布,开始我使用的是微软官方的示例程序,这里为了对比端口,我在自己Windows系统下新建了一个2.1的示例程序,使用VS2017进行程序修改,发布后通过xftp再上传到Linux服务器上 然后去修改Nginx的配置,默认的路径应该是/etc/nginx/nginx.conf,在server节点下的location节点加一句 proxy_pass http://localhost:8080; 就可以 server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { proxy_pass http://localhost:8080; } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } } 修改完成后测试并重启Nginx服务 sudo nginx -t #测试配置 sudo nginx -s reload #重新加载配置 配置完成之后,启动网站访问服务器IP地址的8080端口即可 但是启动网站这里存在一个问题,如果像上面那样没有使用cd命令进入网站目录启动,样式和脚本等文件的路径就会出现错误,导致页面显示不正常所以要在网站目录启动 基本的网站部署就到这里,下一次讲讲用Docker如何进行.NET Core的部署与开发
原文:[UWP] 用 AudioGraph 来增强 UWP 的音频处理能力——AudioFrameInputNode 上一篇心得记录中提到了 AudioGraph, 描述了一下 什么是 AudioGraph 以及其中涉及到的各种类型的 节点(Node)。 这一篇就其中比较有意思的 AudioFrameInputNode 来详细展开一下。 借用 AudioFrameInputNode, 实现简单的音频左右声道互换 什么是 AudioFrameInputNode? 在微软的文档中这么介绍 An audio frame input node allows you to push audio data that you generate in your own code into the audio graph. This enables scenarios like creating a custom software synthesizer. 按照我个人的理解,AudioFrameInputNode 可以让我们自由的访问音频数据,音频数据是 PCM 格式,我们可以对音频数据做一些魔改,具体怎么魔改,就需要一些音频处理的算法知识了。 如何使用 AudioFrameInputNode? 1.创建 AudioFrameInputNode AudioEncodingProperties nodeEncodingProperties = audioGraph.EncodingProperties; nodeEncodingProperties.ChannelCount = 2; nodeEncodingProperties.Subtype = "float"; nodeEncodingProperties.SampleRate = 44100; nodeEncodingProperties.BitsPerSample = 32; AudioFrameInputNode frameInputNode = audioGraph.CreateFrameInputNode(nodeEncodingProperties); frameInputNode.QuantumStarted += FrameInputNode_QuantumStarted; 所有的音频输入节点,都必须通过 AudioGragh 的实例方法来创建,AudioFrameInputNode 也不例外,在创建时,需要传入一个 AudioEncodingProperties,来描述 AudioFrameInputNode 需要处理的音频的一些属性。 在创建完成一个 AudioFrameInputNode 的对象实例后,需要订阅其 QuantumStarted 事件,这个事件会在 AudioGraph 开始处理音频数据时调用,在该事件方法内部,可以完成对音频数据的添加和修改。 2.访问 AudioFrame AudioFrameInputNode 是基于 AudioFrame, 需要对其数据进行读取和写入。 所以在事件的订阅方法 FrameInputNode_QuantumStarted 内部,需要对 AudioFrame 填充 PCM 音频数据。 首先需要创建一个 AudioFrame 对象,在构造函数中,需要传入缓冲区的大小。 在这个示例中,每一个 采样点(Sample) 都是 Float 类型,采用立体声,也就是双通道,所以计算缓冲区大小的代码如下: var bufferSize = args.RequiredSamples * sizeof(float) * 2; AudioFrame audioFrame = new AudioFrame((uint)bufferSize); 在 AudioFrame 内部是一个 AudioBuffer,它代表存储 PCM 数据的缓冲区,所以接下来需要获取对该缓冲区的访问权,需要如下方法: AudioBuffer audioBuffer = audioFrame.LockBuffer(AudioBufferAccessMode.Write); IMemoryBufferReference bufferReference = audioBuffer.CreateReference(); 通过 AudioBuffer 的实例方法 CreateReference,得到 IMemoryBufferReference 的对象,它实际上是一个 COM 接口,通过如下方法强制转换,可以获取 native 的缓冲区指针和缓冲区长度: ((IMemoryBufferByteAccess)bufferReference).GetBuffer(out byte* dataInBytes, out uint capacityInBytes); 其中 IMemoryBufferByteAccess 接口定义如下: [ComImport] [Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] unsafe interface IMemoryBufferByteAccess { void GetBuffer(out byte* buffer, out uint capacity); } 注意,因为用到了指针,所以需要在工程配置文件中 允许unsafe code 选项打开, 并且在该方法签名中指明 unsafe 关键字。 至此,就得到了音频数据缓冲区的指针,但是此时整个缓冲区都是空的,需要填充 PCM 音频数据。 此处便是 AudioFrame 的便利之处,因为我们可以任意填充我们想要的音频数据,无论是处理过的还是没有处理过的。而获取 PCM 原始音频数据的途径很多,可以代码生成,也可以从文件读取,对于我这种对音频处理技术几乎白痴的人,我选择从一个 PCM 文件导入。 此处可以借用 Adobe Audition 等工具转换生成 PCM。 3.PCM 音频数据填充 打开一个 PCM 格式的文件流 fileStream, 其中 PCM 采样率是44100,32位浮点型,立体声。这些格式很重要,需要和初始化 AudioFrameInputNode 对象实例时设定的一样,才能保证数据填充过程正确。 在构造 AudioFrame 时传入了代表缓冲区长度的值 bufferSize,所以此处需要从文件流 fileStream 读取对应长度的数据到内存中, var managedBuffer = new byte[capacityInBytes]; var lastLength = fileStream.Length - fileStream.Position; int readLength = (int)(lastLength < capacityInBytes ? lastLength : capacityInBytes); if (readLength <= 0) { fileStream.Close(); fileStream = null; return; } fileStream.Read(managedBuffer, 0, readLength); 为了稍微体现一下 AudioFrameInputNode 的价值,这儿对要填充的数据做一项最简单的处理,即交换左右声道的内容。 在 PCM 中,每一个 Sample 是四个字节,具体排布是: 左声道,右声道,左声道,右声道,左声道,右声道,左声道,右声道........ 所以交换声道就很简单了,代码如下: for (int i = 0; i < readLength; i+=8) { dataInBytes[i+4] = managedBuffer[i+0]; dataInBytes[i+5] = managedBuffer[i+1]; dataInBytes[i+6] = managedBuffer[i+2]; dataInBytes[i+7] = managedBuffer[i+3]; dataInBytes[i+0] = managedBuffer[i+4]; dataInBytes[i+1] = managedBuffer[i+5]; dataInBytes[i+2] = managedBuffer[i+6]; dataInBytes[i+3] = managedBuffer[i+7]; } 因为 dataInBytes 是缓冲区的指针,所以对缓冲区赋值就是填充缓冲区的过程。在填充完后,需要释放 audioBuffer 和 bufferReference 对象,避免内存泄漏。 踩到的坑 大小端问题 借用百度百科内容: 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。 二进制内容在内存里面存储,是存在大小端问题的,对于PCM格式,也存在大小端问题,所以如果对数据想进一步处理,大小端的问题一定要注意。 在C#中调用 native 内容时,我的机器上实测时小端模式。 也可以通过如下 unsafe 代码来判断: int temp = 0x01; int* pTempInt = &temp; byte* pTempByte = (byte*)pTempInt; if(0x01== *pTempByte) { //小端 } else { //大端 } float 在内存中如何排布? 对于 int 类型,将其转换为二进制后,求补码,即是它在内存中的实际值,但是对于浮点型,就有一套自己的计算方法了,可以参考如下博客(大学计算机课本里的内容,忘得差不多了) float & double 内存布局 附件 Github AudioFrameInputNode Demo 附上我测试用的 PCM 数据,44100,32位 浮点型,小端模式 听说最近杭州下雪了,这歌现在很火! 许嵩-断桥残雪 片段 PCM 下图是该 PCM 的原始波形图, 所以听的时候听到的顺序应该是:先右声道,再立体声,最后左声道,和波形图里相反。 记得耳机别戴反!
参考:https://www.jianshu.com/p/50a2e13cd5cf 安装MySQL Utilities 下载地址:https://dev.mysql.com/downloads/utilities/ 下载完后直接安装即可 如果运行时需要python就下载 https://dev.mysql.com/downloads/connector/python/ 本机还需要安装与需要恢复数据的一样版本的Mysql 使用mysqlfrm命令读取frm的表结构 把需要进行数据恢复的frm文件放到一个目录里 mysqlfrm --diagnostic ./frm文件目录/ 这样就可以获得数据表的结构了。 创建新的数据库 把第二步获得的数据表结构执行,(利用旧的脚本)创建表。 对已创建的表进行表空间卸载 ALTER TABLE 表名 DISCARD TABLESPACE; 每个表都进行一次空间卸载 停掉MYSQL服务 把原始数据文件里的ibd文件拷到新的数据库文件夹里这里要注意把拷过来的ibd文件的所有者为mysql chwon mysql:mysql 数据库文件夹/*启动MYSQL服务 对数据表进行空间装载 ALTER TABLE 表名 IMPORT TABLESPACE; 每个表都进行一次空间装载
原文:C# 实现生成带二维码的专属微信公众号推广海报 很多微信公众号中需要生成推广海报的功能,粉丝获得专属海报后可以分享到朋友圈或发给朋友,为公众号代言邀请好友即可获取奖励的。海报自带渠道二维码,粉丝长按二维码即可关注微信公众号,从而达到吸粉的目的。 效果如下: 代码实现: 1.获取临时二维码ticket /// <summary> /// 获取临时二维码ticket /// </summary> /// <param name="scene_str">场景值ID openid做场景值ID</param> /// <returns></returns> public static string CreateTempQRCode(string scene_str,string access_token) { var result = HttpUtility.SendPostHttpRequest($"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={access_token}", "application/json", "{\"expire_seconds\": 2592000, \"action_name\": \"QR_STR_SCENE\", \"action_info\": {\"scene\": {\"scene_str\": \"" + scene_str + "\"}}}"); JObject jobect = (JObject)JsonConvert.DeserializeObject(result); string ticket = (string)jobect["ticket"]; if (string.IsNullOrEmpty(ticket)) { LogHelper.WriteLog(typeof(WeixinHelper), "获取临时二维码ticket失败" + result); return null; } return ticket; } 使用openid作为场景值的好处是通过扫A推广的二维码关注用户的场景值便是A的openid。 2. 生成带二维码的专属推广图片 /// <summary> /// 生成带二维码的专属推广图片 /// </summary> /// <param name="user"></param> /// <returns></returns> public string Draw(WxUser user) { //背景图片 string path = Server.MapPath("/Content/images/tg.jpg"); System.Drawing.Image imgSrc = System.Drawing.Image.FromFile(path); //处理二维码图片大小 240*240px System.Drawing.Image qrCodeImage = ReduceImage("https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+user.ticket, 240, 240); //处理头像图片大小 100*100px Image titleImage = ReduceImage(user.headimgurl, 100, 100); using (Graphics g = Graphics.FromImage(imgSrc)) { //画专属推广二维码 g.DrawImage(qrCodeImage, new Rectangle(imgSrc.Width - qrCodeImage.Width - 200, imgSrc.Height - qrCodeImage.Height - 200, qrCodeImage.Width, qrCodeImage.Height), 0, 0, qrCodeImage.Width, qrCodeImage.Height, GraphicsUnit.Pixel); //画头像 g.DrawImage(titleImage, 8, 8, titleImage.Width, titleImage.Height); Font font = new Font("宋体", 30, FontStyle.Bold); g.DrawString(user.nickname, font, new SolidBrush(Color.Red), 110, 10); } string newpath = Server.MapPath(@"/Content/images/newtg_" + Guid.NewGuid().ToString() + ".jpg"); imgSrc.Save(newpath, System.Drawing.Imaging.ImageFormat.Jpeg); return newpath; } /// <summary> /// 缩小/放大图片 /// </summary> /// <param name="url">图片网络地址</param> /// <param name="toWidth">缩小/放大宽度</param> /// <param name="toHeight">缩小/放大高度</param> /// <returns></returns> public Image ReduceImage(string url, int toWidth, int toHeight) { WebRequest request = WebRequest.Create(url); WebResponse response = request.GetResponse(); Stream responseStream = response.GetResponseStream(); Image originalImage = Image.FromStream(responseStream); if (toWidth <= 0 && toHeight <= 0) { return originalImage; } else if (toWidth > 0 && toHeight > 0) { if (originalImage.Width < toWidth && originalImage.Height < toHeight) return originalImage; } else if (toWidth <= 0 && toHeight > 0) { if (originalImage.Height < toHeight) return originalImage; toWidth = originalImage.Width * toHeight / originalImage.Height; } else if (toHeight <= 0 && toWidth > 0) { if (originalImage.Width < toWidth) return originalImage; toHeight = originalImage.Height * toWidth / originalImage.Width; } Image toBitmap = new Bitmap(toWidth, toHeight); using (Graphics g = Graphics.FromImage(toBitmap)) { g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; g.Clear(Color.Transparent); g.DrawImage(originalImage, new Rectangle(0, 0, toWidth, toHeight), new Rectangle(0, 0, originalImage.Width, originalImage.Height), GraphicsUnit.Pixel); originalImage.Dispose(); return toBitmap; } } 3.将图片上传微信服务器,并发送给用户 string imagePath = Draw(user); string result = HttpUtility.UploadFile($"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={access_token}&type=image", imagePath); JObject jObject = (JObject)JsonConvert.DeserializeObject(result); string media_id = (string)jObject["media_id"]; if (!string.IsNullOrEmpty(media_id)) { string resxml = "<xml><ToUserName><![CDATA[" + xmlMsg.FromUserName + "]]></ToUserName><FromUserName><![CDATA[" + xmlMsg.ToUserName + "]]></FromUserName><CreateTime>" + nowtime + "</CreateTime><MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[" + media_id + "]]></MediaId></Image></xml>"; return resxml; } LogHelper.WriteLog(typeof(WechatController), "上传专属推广图片素材失败" + result); 详细请查看 http://blog.yshizi.cn/50.html
原文:业务重点-实现一个简单的手机号码验证 前言 本文纯干货,直接拿走使用,不用付费。在业务开发中,手机号码验证是我们常常需要面对的问题,目前市场上各种各样的手机号码验证方式,比如正则表达式等等,本文结合实际业务场景,在业务级别对手机号码进行严格验证;同时增加可配置方式,方便业务扩展,代码非常简单,扩展非常灵活。 1. 目前手机号段有哪些 1.1 目前国内的手机号段主要集中在三大运营商手上,还有一些内部号段和虚拟号段 "中国电信": "133,153,189,180,181,177,173,199,174,141", "中国移动": "139,138,137,136,135,134,159,158,157,150,151,152,147,188,187,182,183,184,178,198", "中国联通": "130,131,132,146,156,155,166,186,185,145,175,176", "虛拟运营商": "170,171", "内部号码": "123" 2. 建立一个测试项目 Ron.PhoneTest 2.1 将上面的号段加入配置文件 appsettings.json 中 { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "phone-segment": { "中国电信": "133,153,189,180,181,177,173,199,174,141", "中国移动": "139,138,137,136,135,134,159,158,157,150,151,152,147,188,187,182,183,184,178,198", "中国联通": "130,131,132,146,156,155,166,186,185,145,175,176", "虛拟运营商": "170,171", "内部号码": "123" } } 3. 建立一个检查类,负责初始化号段库和校验的工作 public class PhoneValidator { private static readonly Regex checktor = new Regex(@"^1\d{10}$"); public IDictionary segment = null; public PhoneValidator(IDictionary segment) { this.segment = segment; } public bool IsPhone(ref string tel) { if (string.IsNullOrEmpty(tel)) { return false; } tel = tel.Replace("+86-", "").Replace("+86", "").Replace("86-", "").Replace("-", ""); if (!checktor.IsMatch(tel)) { return false; } string s = tel.Substring(0, 3); if (segment.Count > 0 && !segment.Contains(s)) { return false; } return true; } } 4. 通过 Startup.cs 实现读取配置和注入,以便系统使用 public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); CreatePhoneValidator(services); } private void CreatePhoneValidator(IServiceCollection services) { Hashtable segment = new Hashtable(); var coll = Configuration.GetSection("phone-segment").GetChildren(); foreach (var prefix in coll) { if (string.IsNullOrEmpty(prefix.Value)) continue; foreach (var s in prefix.Value.Split(',')) segment[s] = s; } var pv = new PhoneValidator(segment); services.AddSingleton<PhoneValidator>(pv); } 以上代码通过读取配置文件节点 phone-segment 并初始化 PhoneValidator 类,最后注入到 IServiceCollection 中,完成了初始化的工作 5. 在控制器中使用 PhoneValidator 进行验证 5.1 示例代码 [Route("api/home")] [ApiController] public class HomeController : ControllerBase { PhoneValidator validator = null; public HomeController(PhoneValidator pv) { validator = pv; } [HttpGet("login")] public IActionResult Login(string phone) { bool accept = validator.IsPhone(ref phone); return new JsonResult(new { phone, accept }); } } 5.2 运行项目,在浏览器中输入地址 http://localhost:33868/api/home/login?phone=86-13800138000 5.3 输出结果 结语 通过上面的示例,可以实现对各种各样手机号码的控制,由于号段写在配置文件中,我们可以在业务扩展到时候去动态的增加号段,还可以针对各个地区去扩展 PhoneValidator 类,以实现切合业务的验证需求,从此,手机号码验证不再需要一刀切。 示例代码下载 https://files.cnblogs.com/files/viter/Ron.PhoneTest.zip
原文:.NET redis cluster 一、下载Windows版本Redis 下载链接:https://github.com/MSOpenTech/redis/releases(根据系统选择对应版本) 二、修改默认的配置文件 如上图两个配置文件,redis.windows.conf(应用程序配置文件);redis.windows-service.conf(Redis windows 服务使用的配置文件)。 主要配置: 1. bind #IP 2.port #端口 3.loglevel #日志级别 4.logfile #日志保存位置 5.dir #数据保存地址 6.cluster-enabled yes #启用集群 7.cluster-config-file #nodes.conf ( redis记录文件,自动生成) 8.cluster-node-timeout #失效时间(毫秒) 注意:以上配置节点行头不要留有空格,否则会报错。 三、准备集群配置文件 将修改好的配置文件复制如下图 四、准备集群环境 安装rubay(由于 Redis 的集群使用 ruby脚本编写,所以系统需要有 Ruby 环境) 下载启动脚本:redis-trib.rb 五、编写批量启动Redis脚本(可选) @echo offcd 安装目录start redis-server.exe ./redis.windows.conf 如: 另存为bat文件,方便启动集群实例。 六、执行集群命令 1.先利用上面编写.bat文件启动Redis实例。 2.用cmd进入Redis安装目录 3.执行集群命令 ruby redis-trib.rb create --replicas 1 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 出现以上说明准备的环境是没有问题的。输入yes(表示同意上面的集群配置) 最后出现OK,说明集群成功。 七、集群测试 1.输入cluster info查看集群信息 2.ASP.NET MVC测试 至此集群配置成功。
原文:【.NET Core项目实战-统一认证平台】第九章 授权篇-使用Dapper持久化IdentityServer4 【.NET Core项目实战-统一认证平台】开篇及目录索引 上篇文章介绍了IdentityServer4的源码分析的内容,让我们知道了IdentityServer4的一些运行原理,这篇将介绍如何使用dapper来持久化Identityserver4,让我们对IdentityServer4理解更透彻,并优化下数据请求,减少不必要的开销。 .netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。 一、数据如何实现持久化 在进行数据持久化之前,我们要了解Ids4是如何实现持久化的呢?Ids4默认是使用内存实现的IClientStore、IResourceStore、IPersistedGrantStore三个接口,对应的分别是InMemoryClientStore、InMemoryResourcesStore、InMemoryPersistedGrantStore三个方法,这显然达不到我们持久化的需求,因为都是从内存里提取配置信息,所以我们要做到Ids4配置信息持久化,就需要实现这三个接口,作为优秀的身份认证框架,肯定已经帮我们想到了这点啦,有个EFCore的持久化实现,GitHub地址https://github.com/IdentityServer/IdentityServer4.EntityFramework,是不是万事大吉了呢?拿来直接使用吧,使用肯定是没有问题的,但是我们要分析下实现的方式和数据库结构,便于后续使用dapper来持久化和扩展成任意数据库存储。 下面以IClientStore接口接口为例,讲解下如何实现数据持久化的。他的方法就是通过clientId获取Client记录,乍一看很简单,不管是用内存或数据库都可以很简单实现。 Task<Client> FindClientByIdAsync(string clientId); 要看这个接口实际用途,就可以直接查看这个接口被注入到哪些方法中,最简单的方式就是Ctrl+F ,通过查找会发现,Client实体里有很多关联记录也会被用到,因此我们在提取Client信息时需要提取他对应的关联实体,那如果是数据库持久化,那应该怎么提取呢?这里可以参考IdentityServer4.EntityFramework项目,我们执行下客户端授权如下图所示,您会发现能够正确返回结果,但是这里执行了哪些SQL查询呢? 从EFCore实现中可以看出来,就一个简单的客户端查询语句,尽然执行了10次数据库查询操作(可以使用SQL Server Profiler查看详细的SQL语句),这也是为什么使用IdentityServer4获取授权信息时奇慢无比的原因。 public Task<Client> FindClientByIdAsync(string clientId) { var client = _context.Clients .Include(x => x.AllowedGrantTypes) .Include(x => x.RedirectUris) .Include(x => x.PostLogoutRedirectUris) .Include(x => x.AllowedScopes) .Include(x => x.ClientSecrets) .Include(x => x.Claims) .Include(x => x.IdentityProviderRestrictions) .Include(x => x.AllowedCorsOrigins) .Include(x => x.Properties) .FirstOrDefault(x => x.ClientId == clientId); var model = client?.ToModel(); _logger.LogDebug("{clientId} found in database: {clientIdFound}", clientId, model != null); return Task.FromResult(model); } 这肯定不是实际生产环境中想要的结果,我们希望是尽量一次连接查询到想要的结果。其他2个方法类似,就不一一介绍了,我们需要使用dapper来持久化存储,减少对服务器查询的开销。 特别需要注意的是,在使用refresh_token时,有个有效期的问题,所以需要通过可配置的方式设置定期清除过期的授权信息,实现方式可以通过数据库作业、定时器、后台任务等,使用dapper持久化时也需要实现此方法。 二、使用Dapper持久化 下面就开始搭建Dapper的持久化存储,首先建一个IdentityServer4.Dapper类库项目,来实现自定义的扩展功能,还记得前几篇开发中间件的思路吗?这里再按照设计思路回顾下,首先我们考虑需要注入什么来解决Dapper的使用,通过分析得知需要一个连接字符串和使用哪个数据库,以及配置定时删除过期授权的策略。 新建IdentityServerDapperBuilderExtensions类,实现我们注入的扩展,代码如下。 using IdentityServer4.Dapper.Options; using System; using IdentityServer4.Stores; namespace Microsoft.Extensions.DependencyInjection { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 使用Dapper扩展 /// </summary> public static class IdentityServerDapperBuilderExtensions { /// <summary> /// 配置Dapper接口和实现(默认使用SqlServer) /// </summary> /// <param name="builder">The builder.</param> /// <param name="storeOptionsAction">存储配置信息</param> /// <returns></returns> public static IIdentityServerBuilder AddDapperStore( this IIdentityServerBuilder builder, Action<DapperStoreOptions> storeOptionsAction = null) { var options = new DapperStoreOptions(); builder.Services.AddSingleton(options); storeOptionsAction?.Invoke(options); builder.Services.AddTransient<IClientStore, SqlServerClientStore>(); builder.Services.AddTransient<IResourceStore, SqlServerResourceStore>(); builder.Services.AddTransient<IPersistedGrantStore, SqlServerPersistedGrantStore>(); return builder; } /// <summary> /// 使用Mysql存储 /// </summary> /// <param name="builder"></param> /// <returns></returns> public static IIdentityServerBuilder UseMySql(this IIdentityServerBuilder builder) { builder.Services.AddTransient<IClientStore, MySqlClientStore>(); builder.Services.AddTransient<IResourceStore, MySqlResourceStore>(); builder.Services.AddTransient<IPersistedGrantStore, MySqlPersistedGrantStore>(); return builder; } } } 整体框架基本确认了,现在就需要解决这里用到的几个配置信息和实现。 DapperStoreOptions需要接收那些参数? 如何使用dapper实现存储的三个接口信息? 首先我们定义下配置文件,用来接收数据库的连接字符串和配置清理的参数并设置默认值。 namespace IdentityServer4.Dapper.Options { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 配置存储信息 /// </summary> public class DapperStoreOptions { /// <summary> /// 是否启用自定清理Token /// </summary> public bool EnableTokenCleanup { get; set; } = false; /// <summary> /// 清理token周期(单位秒),默认1小时 /// </summary> public int TokenCleanupInterval { get; set; } = 3600; /// <summary> /// 连接字符串 /// </summary> public string DbConnectionStrings { get; set; } } } 如上图所示,这里定义了最基本的配置信息,来满足我们的需求。 下面开始来实现客户端存储,SqlServerClientStore类代码如下。 using Dapper; using IdentityServer4.Dapper.Mappers; using IdentityServer4.Dapper.Options; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System.Data.SqlClient; using System.Threading.Tasks; namespace IdentityServer4.Dapper.Stores.SqlServer { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 实现提取客户端存储信息 /// </summary> public class SqlServerClientStore: IClientStore { private readonly ILogger<SqlServerClientStore> _logger; private readonly DapperStoreOptions _configurationStoreOptions; public SqlServerClientStore(ILogger<SqlServerClientStore> logger, DapperStoreOptions configurationStoreOptions) { _logger = logger; _configurationStoreOptions = configurationStoreOptions; } /// <summary> /// 根据客户端ID 获取客户端信息内容 /// </summary> /// <param name="clientId"></param> /// <returns></returns> public async Task<Client> FindClientByIdAsync(string clientId) { var cModel = new Client(); var _client = new Entities.Client(); using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { //由于后续未用到,暂不实现 ClientPostLogoutRedirectUris ClientClaims ClientIdPRestrictions ClientCorsOrigins ClientProperties,有需要的自行添加。 string sql = @"select * from Clients where ClientId=@client and Enabled=1; select t2.* from Clients t1 inner join ClientGrantTypes t2 on t1.Id=t2.ClientId where t1.ClientId=@client and Enabled=1; select t2.* from Clients t1 inner join ClientRedirectUris t2 on t1.Id=t2.ClientId where t1.ClientId=@client and Enabled=1; select t2.* from Clients t1 inner join ClientScopes t2 on t1.Id=t2.ClientId where t1.ClientId=@client and Enabled=1; select t2.* from Clients t1 inner join ClientSecrets t2 on t1.Id=t2.ClientId where t1.ClientId=@client and Enabled=1; "; var multi = await connection.QueryMultipleAsync(sql, new { client = clientId }); var client = multi.Read<Entities.Client>(); var ClientGrantTypes = multi.Read<Entities.ClientGrantType>(); var ClientRedirectUris = multi.Read<Entities.ClientRedirectUri>(); var ClientScopes = multi.Read<Entities.ClientScope>(); var ClientSecrets = multi.Read<Entities.ClientSecret>(); if (client != null && client.AsList().Count > 0) {//提取信息 _client = client.AsList()[0]; _client.AllowedGrantTypes = ClientGrantTypes.AsList(); _client.RedirectUris = ClientRedirectUris.AsList(); _client.AllowedScopes = ClientScopes.AsList(); _client.ClientSecrets = ClientSecrets.AsList(); cModel = _client.ToModel(); } } _logger.LogDebug("{clientId} found in database: {clientIdFound}", clientId, _client != null); return cModel; } } } 这里面涉及到几个知识点,第一dapper的高级使用,一次性提取多个数据集,然后逐一赋值,需要注意的是sql查询顺序和赋值顺序需要完全一致。第二是AutoMapper的实体映射,最后封装的一句代码就是_client.ToModel();即可完成,这与这块使用还不是很清楚,可学习相关知识后再看,详细的映射代码如下。 using System.Collections.Generic; using System.Security.Claims; using AutoMapper; namespace IdentityServer4.Dapper.Mappers { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 客户端实体映射 /// </summary> /// <seealso cref="AutoMapper.Profile" /> public class ClientMapperProfile : Profile { public ClientMapperProfile() { CreateMap<Entities.ClientProperty, KeyValuePair<string, string>>() .ReverseMap(); CreateMap<Entities.Client, IdentityServer4.Models.Client>() .ForMember(dest => dest.ProtocolType, opt => opt.Condition(srs => srs != null)) .ReverseMap(); CreateMap<Entities.ClientCorsOrigin, string>() .ConstructUsing(src => src.Origin) .ReverseMap() .ForMember(dest => dest.Origin, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientIdPRestriction, string>() .ConstructUsing(src => src.Provider) .ReverseMap() .ForMember(dest => dest.Provider, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientClaim, Claim>(MemberList.None) .ConstructUsing(src => new Claim(src.Type, src.Value)) .ReverseMap(); CreateMap<Entities.ClientScope, string>() .ConstructUsing(src => src.Scope) .ReverseMap() .ForMember(dest => dest.Scope, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientPostLogoutRedirectUri, string>() .ConstructUsing(src => src.PostLogoutRedirectUri) .ReverseMap() .ForMember(dest => dest.PostLogoutRedirectUri, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientRedirectUri, string>() .ConstructUsing(src => src.RedirectUri) .ReverseMap() .ForMember(dest => dest.RedirectUri, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientGrantType, string>() .ConstructUsing(src => src.GrantType) .ReverseMap() .ForMember(dest => dest.GrantType, opt => opt.MapFrom(src => src)); CreateMap<Entities.ClientSecret, IdentityServer4.Models.Secret>(MemberList.Destination) .ForMember(dest => dest.Type, opt => opt.Condition(srs => srs != null)) .ReverseMap(); } } } using AutoMapper; namespace IdentityServer4.Dapper.Mappers { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 客户端信息映射 /// </summary> public static class ClientMappers { static ClientMappers() { Mapper = new MapperConfiguration(cfg => cfg.AddProfile<ClientMapperProfile>()) .CreateMapper(); } internal static IMapper Mapper { get; } public static Models.Client ToModel(this Entities.Client entity) { return Mapper.Map<Models.Client>(entity); } public static Entities.Client ToEntity(this Models.Client model) { return Mapper.Map<Entities.Client>(model); } } } 这样就完成了从数据库里提取客户端信息及相关关联表记录,只需要一次连接即可完成,奈斯,达到我们的要求。接着继续实现其他2个接口,下面直接列出2个类的实现代码。 SqlServerResourceStore.cs using Dapper; using IdentityServer4.Dapper.Mappers; using IdentityServer4.Dapper.Options; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data.SqlClient; using System.Threading.Tasks; using System.Linq; namespace IdentityServer4.Dapper.Stores.SqlServer { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 重写资源存储方法 /// </summary> public class SqlServerResourceStore : IResourceStore { private readonly ILogger<SqlServerResourceStore> _logger; private readonly DapperStoreOptions _configurationStoreOptions; public SqlServerResourceStore(ILogger<SqlServerResourceStore> logger, DapperStoreOptions configurationStoreOptions) { _logger = logger; _configurationStoreOptions = configurationStoreOptions; } /// <summary> /// 根据api名称获取相关信息 /// </summary> /// <param name="name"></param> /// <returns></returns> public async Task<ApiResource> FindApiResourceAsync(string name) { var model = new ApiResource(); using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = @"select * from ApiResources where Name=@Name and Enabled=1; select * from ApiResources t1 inner join ApiScopes t2 on t1.Id=t2.ApiResourceId where t1.Name=@name and Enabled=1; "; var multi = await connection.QueryMultipleAsync(sql, new { name }); var ApiResources = multi.Read<Entities.ApiResource>(); var ApiScopes = multi.Read<Entities.ApiScope>(); if (ApiResources != null && ApiResources.AsList()?.Count > 0) { var apiresource = ApiResources.AsList()[0]; apiresource.Scopes = ApiScopes.AsList(); if (apiresource != null) { _logger.LogDebug("Found {api} API resource in database", name); } else { _logger.LogDebug("Did not find {api} API resource in database", name); } model = apiresource.ToModel(); } } return model; } /// <summary> /// 根据作用域信息获取接口资源 /// </summary> /// <param name="scopeNames"></param> /// <returns></returns> public async Task<IEnumerable<ApiResource>> FindApiResourcesByScopeAsync(IEnumerable<string> scopeNames) { var apiResourceData = new List<ApiResource>(); string _scopes = ""; foreach (var scope in scopeNames) { _scopes += "'" + scope + "',"; } if (_scopes == "") { return null; } else { _scopes = _scopes.Substring(0, _scopes.Length - 1); } string sql = "select distinct t1.* from ApiResources t1 inner join ApiScopes t2 on t1.Id=t2.ApiResourceId where t2.Name in(" + _scopes + ") and Enabled=1;"; using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { var apir = (await connection.QueryAsync<Entities.ApiResource>(sql))?.AsList(); if (apir != null && apir.Count > 0) { foreach (var apimodel in apir) { sql = "select * from ApiScopes where ApiResourceId=@id"; var scopedata = (await connection.QueryAsync<Entities.ApiScope>(sql, new { id = apimodel.Id }))?.AsList(); apimodel.Scopes = scopedata; apiResourceData.Add(apimodel.ToModel()); } _logger.LogDebug("Found {scopes} API scopes in database", apiResourceData.SelectMany(x => x.Scopes).Select(x => x.Name)); } } return apiResourceData; } /// <summary> /// 根据scope获取身份资源 /// </summary> /// <param name="scopeNames"></param> /// <returns></returns> public async Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeAsync(IEnumerable<string> scopeNames) { var apiResourceData = new List<IdentityResource>(); string _scopes = ""; foreach (var scope in scopeNames) { _scopes += "'" + scope + "',"; } if (_scopes == "") { return null; } else { _scopes = _scopes.Substring(0, _scopes.Length - 1); } using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { //暂不实现 IdentityClaims string sql = "select * from IdentityResources where Enabled=1 and Name in(" + _scopes + ")"; var data = (await connection.QueryAsync<Entities.IdentityResource>(sql))?.AsList(); if (data != null && data.Count > 0) { foreach (var model in data) { apiResourceData.Add(model.ToModel()); } } } return apiResourceData; } /// <summary> /// 获取所有资源实现 /// </summary> /// <returns></returns> public async Task<Resources> GetAllResourcesAsync() { var apiResourceData = new List<ApiResource>(); var identityResourceData = new List<IdentityResource>(); using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "select * from IdentityResources where Enabled=1"; var data = (await connection.QueryAsync<Entities.IdentityResource>(sql))?.AsList(); if (data != null && data.Count > 0) { foreach (var m in data) { identityResourceData.Add(m.ToModel()); } } //获取apiresource sql = "select * from ApiResources where Enabled=1"; var apidata = (await connection.QueryAsync<Entities.ApiResource>(sql))?.AsList(); if (apidata != null && apidata.Count > 0) { foreach (var m in apidata) { sql = "select * from ApiScopes where ApiResourceId=@id"; var scopedata = (await connection.QueryAsync<Entities.ApiScope>(sql, new { id = m.Id }))?.AsList(); m.Scopes = scopedata; apiResourceData.Add(m.ToModel()); } } } var model = new Resources(identityResourceData, apiResourceData); return model; } } } SqlServerPersistedGrantStore.cs using Dapper; using IdentityServer4.Dapper.Mappers; using IdentityServer4.Dapper.Options; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; using System.Threading.Tasks; namespace IdentityServer4.Dapper.Stores.SqlServer { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 重写授权信息存储 /// </summary> public class SqlServerPersistedGrantStore : IPersistedGrantStore { private readonly ILogger<SqlServerPersistedGrantStore> _logger; private readonly DapperStoreOptions _configurationStoreOptions; public SqlServerPersistedGrantStore(ILogger<SqlServerPersistedGrantStore> logger, DapperStoreOptions configurationStoreOptions) { _logger = logger; _configurationStoreOptions = configurationStoreOptions; } /// <summary> /// 根据用户标识获取所有的授权信息 /// </summary> /// <param name="subjectId">用户标识</param> /// <returns></returns> public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "select * from PersistedGrants where SubjectId=@subjectId"; var data = (await connection.QueryAsync<Entities.PersistedGrant>(sql, new { subjectId }))?.AsList(); var model = data.Select(x => x.ToModel()); _logger.LogDebug("{persistedGrantCount} persisted grants found for {subjectId}", data.Count, subjectId); return model; } } /// <summary> /// 根据key获取授权信息 /// </summary> /// <param name="key">认证信息</param> /// <returns></returns> public async Task<PersistedGrant> GetAsync(string key) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "select * from PersistedGrants where [Key]=@key"; var result = await connection.QueryFirstOrDefaultAsync<Entities.PersistedGrant>(sql, new { key }); var model = result.ToModel(); _logger.LogDebug("{persistedGrantKey} found in database: {persistedGrantKeyFound}", key, model != null); return model; } } /// <summary> /// 根据用户标识和客户端ID移除所有的授权信息 /// </summary> /// <param name="subjectId">用户标识</param> /// <param name="clientId">客户端ID</param> /// <returns></returns> public async Task RemoveAllAsync(string subjectId, string clientId) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "delete from PersistedGrants where ClientId=@clientId and SubjectId=@subjectId"; await connection.ExecuteAsync(sql, new { subjectId, clientId }); _logger.LogDebug("remove {subjectId} {clientId} from database success", subjectId, clientId); } } /// <summary> /// 移除指定的标识、客户端、类型等授权信息 /// </summary> /// <param name="subjectId">标识</param> /// <param name="clientId">客户端ID</param> /// <param name="type">授权类型</param> /// <returns></returns> public async Task RemoveAllAsync(string subjectId, string clientId, string type) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "delete from PersistedGrants where ClientId=@clientId and SubjectId=@subjectId and Type=@type"; await connection.ExecuteAsync(sql, new { subjectId, clientId }); _logger.LogDebug("remove {subjectId} {clientId} {type} from database success", subjectId, clientId, type); } } /// <summary> /// 移除指定KEY的授权信息 /// </summary> /// <param name="key"></param> /// <returns></returns> public async Task RemoveAsync(string key) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "delete from PersistedGrants where [Key]=@key"; await connection.ExecuteAsync(sql, new { key }); _logger.LogDebug("remove {key} from database success", key); } } /// <summary> /// 存储授权信息 /// </summary> /// <param name="grant">实体</param> /// <returns></returns> public async Task StoreAsync(PersistedGrant grant) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { //移除防止重复 await RemoveAsync(grant.Key); string sql = "insert into PersistedGrants([Key],ClientId,CreationTime,Data,Expiration,SubjectId,Type) values(@Key,@ClientId,@CreationTime,@Data,@Expiration,@SubjectId,@Type)"; await connection.ExecuteAsync(sql, grant); } } } } 使用dapper提取存储数据已经全部实现完,接下来我们需要实现定时清理过期的授权信息。 首先定义一个清理过期数据接口IPersistedGrants,定义如下所示。 using System; using System.Threading.Tasks; namespace IdentityServer4.Dapper.Interfaces { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 过期授权清理接口 /// </summary> public interface IPersistedGrants { /// <summary> /// 移除指定时间的过期信息 /// </summary> /// <param name="dt">过期时间</param> /// <returns></returns> Task RemoveExpireToken(DateTime dt); } } 现在我们来实现下此接口,详细代码如下。 using Dapper; using IdentityServer4.Dapper.Interfaces; using IdentityServer4.Dapper.Options; using Microsoft.Extensions.Logging; using System; using System.Data.SqlClient; using System.Threading.Tasks; namespace IdentityServer4.Dapper.Stores.SqlServer { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 实现授权信息自定义管理 /// </summary> public class SqlServerPersistedGrants : IPersistedGrants { private readonly ILogger<SqlServerPersistedGrants> _logger; private readonly DapperStoreOptions _configurationStoreOptions; public SqlServerPersistedGrants(ILogger<SqlServerPersistedGrants> logger, DapperStoreOptions configurationStoreOptions) { _logger = logger; _configurationStoreOptions = configurationStoreOptions; } /// <summary> /// 移除指定的时间过期授权信息 /// </summary> /// <param name="dt">Utc时间</param> /// <returns></returns> public async Task RemoveExpireToken(DateTime dt) { using (var connection = new SqlConnection(_configurationStoreOptions.DbConnectionStrings)) { string sql = "delete from PersistedGrants where Expiration>@dt"; await connection.ExecuteAsync(sql, new { dt }); } } } } 有个清理的接口和实现,我们需要注入下实现builder.Services.AddTransient<IPersistedGrants, SqlServerPersistedGrants>();,接下来就是开启后端服务来清理过期记录。 using IdentityServer4.Dapper.Interfaces; using IdentityServer4.Dapper.Options; using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; namespace IdentityServer4.Dapper.HostedServices { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 清理过期Token方法 /// </summary> public class TokenCleanup { private readonly ILogger<TokenCleanup> _logger; private readonly DapperStoreOptions _options; private readonly IPersistedGrants _persistedGrants; private CancellationTokenSource _source; public TimeSpan CleanupInterval => TimeSpan.FromSeconds(_options.TokenCleanupInterval); public TokenCleanup(IPersistedGrants persistedGrants, ILogger<TokenCleanup> logger, DapperStoreOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); if (_options.TokenCleanupInterval < 1) throw new ArgumentException("Token cleanup interval must be at least 1 second"); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _persistedGrants = persistedGrants; } public void Start() { Start(CancellationToken.None); } public void Start(CancellationToken cancellationToken) { if (_source != null) throw new InvalidOperationException("Already started. Call Stop first."); _logger.LogDebug("Starting token cleanup"); _source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); Task.Factory.StartNew(() => StartInternal(_source.Token)); } public void Stop() { if (_source == null) throw new InvalidOperationException("Not started. Call Start first."); _logger.LogDebug("Stopping token cleanup"); _source.Cancel(); _source = null; } private async Task StartInternal(CancellationToken cancellationToken) { while (true) { if (cancellationToken.IsCancellationRequested) { _logger.LogDebug("CancellationRequested. Exiting."); break; } try { await Task.Delay(CleanupInterval, cancellationToken); } catch (TaskCanceledException) { _logger.LogDebug("TaskCanceledException. Exiting."); break; } catch (Exception ex) { _logger.LogError("Task.Delay exception: {0}. Exiting.", ex.Message); break; } if (cancellationToken.IsCancellationRequested) { _logger.LogDebug("CancellationRequested. Exiting."); break; } ClearTokens(); } } public void ClearTokens() { try { _logger.LogTrace("Querying for tokens to clear"); //提取满足条件的信息进行删除 _persistedGrants.RemoveExpireToken(DateTime.UtcNow); } catch (Exception ex) { _logger.LogError("Exception clearing tokens: {exception}", ex.Message); } } } } using IdentityServer4.Dapper.Options; using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Tasks; namespace IdentityServer4.Dapper.HostedServices { /// <summary> /// 金焰的世界 /// 2018-12-03 /// 授权后端清理服务 /// </summary> public class TokenCleanupHost : IHostedService { private readonly TokenCleanup _tokenCleanup; private readonly DapperStoreOptions _options; public TokenCleanupHost(TokenCleanup tokenCleanup, DapperStoreOptions options) { _tokenCleanup = tokenCleanup; _options = options; } public Task StartAsync(CancellationToken cancellationToken) { if (_options.EnableTokenCleanup) { _tokenCleanup.Start(cancellationToken); } return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { if (_options.EnableTokenCleanup) { _tokenCleanup.Stop(); } return Task.CompletedTask; } } } 是不是实现一个定时任务很简单呢?功能完成别忘了注入实现,现在我们使用dapper持久化的功能基本完成了。 builder.Services.AddSingleton<TokenCleanup>(); builder.Services.AddSingleton<IHostedService, TokenCleanupHost>(); 三、测试功能应用 在前面客户端授权中,我们增加dapper扩展的实现,来测试功能是否能正常使用,且使用SQL Server Profiler来监控下调用的过程。可以从之前文章中的源码TestIds4项目中,实现持久化的存储,改造注入代码如下。 services.AddIdentityServer() .AddDeveloperSigningCredential() //.AddInMemoryApiResources(Config.GetApiResources()) //.AddInMemoryClients(Config.GetClients()); .AddDapperStore(option=> { option.DbConnectionStrings = "Server=192.168.1.114;Database=mpc_identity;User ID=sa;Password=bl123456;"; }); 好了,现在可以配合网关来测试下客户端登录了,打开PostMan,启用本地的项目,然后访问之前配置的客户端授权地址,并开启SqlServer监控,查看运行代码。 访问能够得到我们预期的结果且查询全部是dapper写的Sql语句。且定期清理任务也启动成功,会根据配置的参数来执行清理过期授权信息。 四、使用Mysql存储并测试 这里Mysql重写就不一一列出来了,语句跟sqlserver几乎是完全一样,然后调用.UseMySql()即可完成mysql切换,我花了不到2分钟就完成了Mysql的所有语句和切换功能,是不是简单呢?接着测试Mysql应用,代码如下。 services.AddIdentityServer() .AddDeveloperSigningCredential() //.AddInMemoryApiResources(Config.GetApiResources()) //.AddInMemoryClients(Config.GetClients()); .AddDapperStore(option=> { option.DbConnectionStrings = "Server=*******;Database=mpc_identity;User ID=root;Password=*******;"; }).UseMySql(); 可以返回正确的结果数据,扩展Mysql实现已经完成,如果想用其他数据库实现,直接按照我写的方法扩展下即可。 五、总结及预告 本篇我介绍了如何使用Dapper来持久化Ids4信息,并介绍了实现过程,然后实现了SqlServer和Mysql两种方式,也介绍了使用过程中遇到的技术问题,其实在实现过程中我发现的一个缓存和如何让授权信息立即过期等问题,这块大家可以一起先思考下如何实现,后续文章中我会介绍具体的实现方式,然后把缓存迁移到Redis里。 下一篇开始就正式介绍Ids4的几种授权方式和具体的应用,以及如何在我们客户端进行集成,如果在学习过程中遇到不懂或未理解的问题,欢迎大家加入QQ群聊637326624与作者联系吧。
原文:SQLServer中查询的数字列前面补0返回指定长度的字符串 SQLServer中查询的数字列前面补0返回指定长度的字符串: 如: 角本如下: /****** Script for SelectTopNRows command from SSMS ******/ SELECT TOP 1000 [ID] ,[SN] ,[Name] FROM [EduDB].[dbo].[TestTab] select Right('0123456',SN) from TestTab; select RIGHT(REPLICATE('0',5)+CAST(SN AS varchar(10)),5) AS 'SN' from TestTab; select RIGHT('00000000'+CAST(SN as varchar(10)),5) as 'sn' from TestTab 效果如下:
原文:sql-----STR 函数 sql-----STR 函数 STR 函数由数字数据转换来的字符数据。 语法 STR ( float_expression [ , length [ , decimal ] ] ) 参数 float_expression是带小数点的近似数字 (float) 数据类型的表达式。不要在 STR 函数中将函数或子查询用作 float_expression。 length是总长度,包括小数点、符号、数字或空格。默认值为 10。 decimal是小数点右边的位数。 返回类型char 注释 如果为 STR 提供 length 和 decimal 参数值,则这些值应该是正数。在默认情况下或者小数参数为 0 时,数字四舍五入为整数。指定长度应该大于或等于小数点前面的数字加上数字符号(若有)的长度。短的 float_expression 在指定长度内右对齐,长的 float_expression 则截断为指定的小数位数。 例如,STR(12,10) 输出的结果是 12,在结果集内右对齐。而 STR(1223, 2) 则将结果集截断为 **。可以嵌套字符串函数。
原文:SQL常用函数之五 str() 使用str函数 :STR 函数由数字数据转换来的字符数据。 语法 STR ( float_expression [ , length [ , decimal ] ] ) 参数 float_expression 是带小数点的近似数字 (float) 数据类型的表达式。不要在 STR 函数中将函数或子查询用作 float_expression。 length 是总长度,包括小数点、符号、数字或空格。默认值为 10。 decimal 是小数点右边的位数。 返回类型 char 注释 如果为 STR 提供 length 和 decimal 参数值,则这些值应该是正数。在默认情况下或者小数参数为 0 时,数字四舍五入为整数。指定长度应该大于或等于小数点前面的数字加上数字符号(若有)的长度。短的 float_expression 在指定长度内右对齐,长的 float_expression 则截断为指定的小数位数。例如,STR(12,10) 输出的结果是 12,在结果集内右对齐。而 STR(1223, 2) 则将结果集截断为 **。可以嵌套字符串函数。 select str(123.46,8,1) 结果为 ###123.5 其中#代表空格 该结果总长度为8 ,取一位小数,小数为四舍五入,同时该SQL语句如果取3个小数的话补零操作。 说明 若要转换为 Unicode 数据,请在 CONVERT 或 CAST 转换函数内使用 STR。
原文:mysql下sql语句 update 字段=字段+字符串 mysql下sql语句令某字段值等于原值加上一个字符串 update 表明 SET 字段= 'feifei' || 字段; (postgreSQL 用 || 来连贯字符串) MySQL连贯字符串不能利用加号(+),而利用concat。 比方在aa表的name字段前加字符'x',利用: update aa set name=concat('x',name)
原文:【UWP】使用 Rx 改善 AutoSuggestBox 在 UWP 中,有一个控件叫 AutoSuggestBox,它的主要成分是一个 TextBox 和 ComboBox。使用它,我们可以做一些根据用户输入来显示相关建议输入的功能,例如百度首页搜索框那种效果: 在看这篇文章之前,我建议先看看老周写的这一篇:https://www.cnblogs.com/tcjiaan/p/4967031.html ,先对 AutoSuggestBox 有一个大体的印象,不然下面干什么都不知道了。 接下来开始我们的实验,先准备好百度的接口(这个可以用浏览器的开发者工具抓出来): public class BaiduService { static BaiduService() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } public async Task<IReadOnlyList<string>> GetSuggestionsAsync(string query) { using (var client = new HttpClient()) { var url = $"http://www.baidu.com/su?wd={HttpUtility.UrlEncode(query)}"; var str = await client.GetStringAsync(url); str = str.Substring(str.IndexOf('{')); str = str.Substring(0, str.LastIndexOf('}') + 1); var jObject = JObject.Parse(str); return jObject["s"].ToObject<string[]>(); } } } 需要引用一下 Newtonsoft.Json 这个包。 静态构造函数里我注册了一下本机的 Encoding,不然会报错(百度这厮用的是 gbk,而不是常见的 utf-8)。 然后开始编写 Demo 页面 XAML <Grid> <Grid Margin="20"> <StackPanel Orientation="Vertical"> <AutoSuggestBox x:Name="AutoSuggestBox" TextChanged="AutoSuggestBox_TextChanged" /> </StackPanel> </Grid> </Grid> 这里随便写了下,反正就是弄了个 AutoSuggestBox,订阅了一下它的 TextChanged 事件。 cs代码: private async void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { switch (args.Reason) { case AutoSuggestionBoxTextChangeReason.ProgrammaticChange: case AutoSuggestionBoxTextChangeReason.SuggestionChosen: sender.ItemsSource = null; return; } // User input var query = sender.Text; Debug.WriteLine("get suggestion: " + query); var suggestions = await _baiduService.GetSuggestionsAsync(query); sender.ItemsSource = suggestions; } 触发的事件参数中有个 Reason 属性,表面该次事件触发的原因。 在这里我如果是程序代码修改或者用户选择了建议项的话,那么就清除建议项列表。否则就去问百度要一下建议(顺便输出一下,说明触发了)。 然后就把我们的 Demo 程序跑起来吧。 看上去工作得还是蛮正常的嘛。 但是,在这里我要告诉你,这样写,是有一些坑的! 1、 全选,复制,再粘贴,我们的文字内容是没有变化才对的,然而也触发了一次请求。 2、 如果我的内容为空,那么就不应该请求才对的。 3、 在上面的图中,我 UWP 这三个字母的输入速度应该是比较快的,那么 U 那一次就不应该去请求才对。应该以停止输入一段时间后,才去进行请求。AutoSuggestBox 控件应该是做了(不然在 UW 时也应该会触发才对),但目测时间非常短(可能就 0.1 秒),而且也没有相关的属性能够控制这个时长。 4、 因为这个请求是一个异步的网络请求,所以说不好的话,后发起的请求有可能先返回。按上面的代码逻辑来说,这样输入和建议项就对不上了。 按传统思路,第 1 点我们可以在请求前加个判断,如果跟上一次相同就不请求。第 2 点加个空字符串判断即可。第 3 点就麻烦了,真要实现我们得加个计时之类的方法来做。第 4 点也是很麻烦,我目前想到的是发起请求时给个 token 之类,接收到的时候再对比是否是最新的 token。 但说实话,这么一整套下来,不麻烦么?而且代码量不是一点两点。 在这里,我要安利各位,只要你使用 Rx,解决这点小问题完全不在话下。 Rx 的全称是 Reactive Extensions,是一种针对异步编程的编程模型。Rx 不仅仅在 .Net 下有实现,在 JavaScript、Java 等等平台都有相关的实现。 概念说完了,继续实验。 引用 Rx 的 nuget 包,System.Reactive。 在页面的构造函数先编写如下的代码: var changed = Observable.FromEventPattern<TypedEventHandler<AutoSuggestBox, AutoSuggestBoxTextChangedEventArgs>, AutoSuggestBox, AutoSuggestBoxTextChangedEventArgs>( handler => AutoSuggestBox.TextChanged += handler, handler => AutoSuggestBox.TextChanged -= handler); 这段代码以 AutoSuggestBox 的 TextChanged 事件创建一个可监听的数据源 changed 对象。 接下来,我们处理第 1 点,需要忽略掉相同的文本内容。 var input = changed .DistinctUntilChanged(temp => temp.Sender.Text); DistinctUntilChanged 这个扩展方法是 Rx 提供的,如果数据源内容不变,则不会触发。 然后我们处理第 3 点,只有停止输入一段时间后,我们再去发起请求。 var input = changed .DistinctUntilChanged(temp => temp.Sender.Text) .Throttle(TimeSpan.FromSeconds(1)); 这个也很简单,Rx 提供了 Throttle 方法,传入需要的时间就可以了,这里我设定成停止输入 1 秒后才触发。 然后接下来我们要区分两种情况,一个是用户输入的,另一个是非用户输入的。 var notUserInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason != AutoSuggestionBoxTextChangeReason.UserInput); var userInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason == AutoSuggestionBoxTextChangeReason.UserInput) .Where(temp => !string.IsNullOrEmpty(temp.Sender.Text)); 在用户输入的时候,输入后文本框非空我们才触发(第 2 点)。 这里注意到还有 ObserveOnDispatcher 这个方法的调用,这个调用就是说,接下来我的操作需要在当前线程上进行。Rx 默认是会在另一个线程上的,在 Where 方法中我们引用到了 AutoSuggestBox 控件,所以需要调用到该方法。 接下来我们处理一下 userInput,有了输入,我们自然需要输出,输出就是建议项: var userInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason == AutoSuggestionBoxTextChangeReason.UserInput) .Where(temp => !string.IsNullOrEmpty(temp.Sender.Text)) .Select(temp => _baiduService.GetSuggestionsAsync(temp.Sender.Text)); 调用百度接口,返回 Task<IReadOnlyList<string>>。同时,我们对 notUserInput 也处理一下,返回 null,但类型也是 Task<IReadOnlyList<string>>。 var notUserInput = input .ObserveOnDispatcher() .Where(temp => temp.EventArgs.Reason != AutoSuggestionBoxTextChangeReason.UserInput) .Select(temp => Task.FromResult<IReadOnlyList<string>>(null)); 现在,我们把这两个重新合成为一个,因为我们数据源触发的条件是 TextChanged,而不是因为上面这一大堆东西才进行触发。 var merge = Observable .Merge(notUserInput, userInput); 最后,我们可以监听这个数据源了,调用 Subscribe 方法(当然还要再 ObserveOnDispatcher 一次): merge .ObserveOnDispatcher() .Subscribe(suggestions => { AutoSuggestBox.ItemsSource = suggestions; }); 这样更新上去我们的 AutoSuggestBox 就行了。 慢着,我们的第 4 点还没处理呢。这个只需要稍微修改一下就可以了(Rx 真方便)。 var merge = Observable .Merge(notUserInput, userInput) .Switch(); Switch 方法会将输出的顺序按照输入的顺序来排序,这样之后,我们的第 4 点就能解决掉了。 最终下来,我们解决这么一系列问题只是写了这么点的代码,如果按传统的写法嘛,那不知道写到什么时候去了。Rx 万岁! 虽然 Rx 学习起来难度曲线非常大,但是在解决某些场景,Rx 是非常的有效的。(顺带一提,Angular 就集成了 RxJS,可见 Rx 存在其优势) 参考资料: DevCamp 2010 Keynote - Rx: Curing your asynchronous programming blues
原文:WPF TextBox 正则验证 大于等于0 小于等于1 的两位小数 正则:^(0\.\d+|[1-9][0-9]|1)$ TextBox绑定正则验证 <TextBox x:Name="txb" MaxLength="6" Margin="1 0 0 0" Width="40" > <TextBox.Text> <Binding Path="Opacity" ValidatesOnExceptions="True" ValidatesOnDataErrors="True" StringFormat="F2" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" > <Binding.ValidationRules> <shared1:InventoryValidationRule InventoryPattern="^(0\.\d+|[1-9][0-9]|1)$"/> </Binding.ValidationRules> </Binding> </TextBox.Text></TextBox> 用到的InventoryValidationRule类: public class InventoryValidationRule : ValidationRule { #region Properties public string InventoryPattern { get; set; } #endregion Properties #region Methods public override ValidationResult Validate( object value, CultureInfo cultureInfo) { if (InventoryPattern == null) return ValidationResult.ValidResult; if (!(value is string)) return new ValidationResult(false, "Inventory should be a comma separated list of model numbers as a string"); string[] pieces = value.ToString().Split(','); Regex m_RegEx = new Regex(InventoryPattern); foreach (string item in pieces) { Match match = m_RegEx.Match(item); if (match == null || match == Match.Empty) return new ValidationResult( false, "Invalid input format"); } return ValidationResult.ValidResult; } #endregion Methods }
原文:webapi的返回类型,webapi返回图片 1.0 首先是返回常用的系统类型,当然这些返回方式不常用到。如:int,string,list,array等。这些类型直接返回即可。 1 public List<string> Get() 2 { 3 List<string> list = new List<string>() { "11","22","33"}; 4 return list; 5 } 1.1 用不同的浏览器测试发现,返回的类型竟然是不一样的。如用ie,edge返回的是json,而用chrome,firefox返回的是xml类型。后来才知道原来WebApi的返回值类型是根据客户端的请求报文头的类型而确定的。IE在发生http请求时请求头accpet节点相比Firefox和Chrome缺少"application/xml"类型,由于WebAPI返回数据为xml或json格式,IE没有发送可接受xml和json类型,所以默认为json格式数据,而Firefox和chrome则发送了可接受xml类型。请参考:http://www.cnblogs.com/lzrabbit/archive/2013/03/19/2948522.html 2.0 返回json类型数据。这也是最常用的方式。 public HttpResponseMessage Get() { var jsonStr = "{\"code\":0,\"data\":\"abc\"}"; var result = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(jsonStr, Encoding.UTF8, "text/json") }; return result; } 3.0 返回流类型数据,如:图片类型。 public HttpResponseMessage Get() { var imgPath = System.Web.Hosting.HostingEnvironment.MapPath("~/111.jpg"); //从图片中读取byte var imgByte = File.ReadAllBytes(imgPath); //从图片中读取流 var imgStream = new MemoryStream(File.ReadAllBytes(imgPath)); var resp = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(imgStream) //或者 //Content = new ByteArrayContent(imgByte) }; resp.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpg"); return resp; }
原文:SQL SERVER-时间戳(timestamp)与时间格式(datetime)互相转换 SQL里面有个DATEADD的函数。时间戳就是一个从1970-01-01 08:00:00到时间的相隔的秒数。所以只要把这个时间戳加上1970-01-01 08:00:00这个时间就可以得到你想要的时间了select DATEADD(second,1268738429 + 8 * 60 * 60,'1970-01-01 00:00:00')注解:北京时间与GMT时间关系 1.GMT是中央时区,北京在东8区,相差8个小时 2.所以北京时间 = GMT时间 + 八小时例如: SELECT DATEADD(S,1160701488,'1970-01-01 08:00:00') --时间戳转换成普通时间 SELECT DATEDIFF(S,'1970-01-01 08:00:00', GETDATE()) --普通时间转换成时间戳
原文:【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析 【.NET Core项目实战-统一认证平台】开篇及目录索引 上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于IdentityServer4(后面简称Ids4)的认证相关知识,在具体介绍ids4实现我们统一认证的相关功能前,我们首先需要分析下Ids4源码,便于我们彻底掌握认证的原理以及后续的扩展需求。 .netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。 一、Ids4文档及源码 文档地址 http://docs.identityserver.io/en/latest/ Github源码地址 https://github.com/IdentityServer/IdentityServer4 二、源码整体分析 【工欲善其事,必先利其器,器欲尽其能,必先得其法】 在我们使用Ids4前我们需要了解它的运行原理和实现方式,这样实际生产环境中才能安心使用,即使遇到问题也可以很快解决,如需要对认证进行扩展,也可自行编码实现。 源码分析第一步就是要找到Ids4的中间件是如何运行的,所以需要定位到中间价应用位置app.UseIdentityServer();,查看到详细的代码如下。 /// <summary> /// Adds IdentityServer to the pipeline. /// </summary> /// <param name="app">The application.</param> /// <returns></returns> public static IApplicationBuilder UseIdentityServer(this IApplicationBuilder app) { //1、验证配置信息 app.Validate(); //2、应用BaseUrl中间件 app.UseMiddleware<BaseUrlMiddleware>(); //3、应用跨域访问配置 app.ConfigureCors(); //4、启用系统认证功能 app.UseAuthentication(); //5、应用ids4中间件 app.UseMiddleware<IdentityServerMiddleware>(); return app; } 通过上面的源码,我们知道整体流程分为这5步实现。接着我们分析下每一步都做了哪些操作呢? 1、app.Validate()为我们做了哪些工作? 校验IPersistedGrantStore、IClientStore、IResourceStore是否已经注入? 验证IdentityServerOptions配置信息是否都配置完整 输出调试相关信息提醒 internal static void Validate(this IApplicationBuilder app) { var loggerFactory = app.ApplicationServices.GetService(typeof(ILoggerFactory)) as ILoggerFactory; if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); var logger = loggerFactory.CreateLogger("IdentityServer4.Startup"); var scopeFactory = app.ApplicationServices.GetService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var serviceProvider = scope.ServiceProvider; TestService(serviceProvider, typeof(IPersistedGrantStore), logger, "No storage mechanism for grants specified. Use the 'AddInMemoryPersistedGrants' extension method to register a development version."); TestService(serviceProvider, typeof(IClientStore), logger, "No storage mechanism for clients specified. Use the 'AddInMemoryClients' extension method to register a development version."); TestService(serviceProvider, typeof(IResourceStore), logger, "No storage mechanism for resources specified. Use the 'AddInMemoryIdentityResources' or 'AddInMemoryApiResources' extension method to register a development version."); var persistedGrants = serviceProvider.GetService(typeof(IPersistedGrantStore)); if (persistedGrants.GetType().FullName == typeof(InMemoryPersistedGrantStore).FullName) { logger.LogInformation("You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch to a different store implementation."); } var options = serviceProvider.GetRequiredService<IdentityServerOptions>(); ValidateOptions(options, logger); ValidateAsync(serviceProvider, logger).GetAwaiter().GetResult(); } } private static async Task ValidateAsync(IServiceProvider services, ILogger logger) { var options = services.GetRequiredService<IdentityServerOptions>(); var schemes = services.GetRequiredService<IAuthenticationSchemeProvider>(); if (await schemes.GetDefaultAuthenticateSchemeAsync() == null && options.Authentication.CookieAuthenticationScheme == null) { logger.LogWarning("No authentication scheme has been set. Setting either a default authentication scheme or a CookieAuthenticationScheme on IdentityServerOptions is required."); } else { if (options.Authentication.CookieAuthenticationScheme != null) { logger.LogInformation("Using explicitly configured scheme {scheme} for IdentityServer", options.Authentication.CookieAuthenticationScheme); } logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for authentication", (await schemes.GetDefaultAuthenticateSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for sign-in", (await schemes.GetDefaultSignInSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for sign-out", (await schemes.GetDefaultSignOutSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for challenge", (await schemes.GetDefaultChallengeSchemeAsync())?.Name); logger.LogDebug("Using {scheme} as default ASP.NET Core scheme for forbid", (await schemes.GetDefaultForbidSchemeAsync())?.Name); } } private static void ValidateOptions(IdentityServerOptions options, ILogger logger) { if (options.IssuerUri.IsPresent()) logger.LogDebug("Custom IssuerUri set to