开源项目葫芦藤:IdentityServer4的实现及其运用

前言

本篇文章主要是讲解葫芦藤项目中对IdentityServer的实践使用,为了使您对本篇文章中所讲述的内容有深刻的认识,并且在阅读时避免感到乏味,文中的内容不会涉及太多的基础理论知识,而更多的是采用动手实践的方式进行讲解,所以在阅读此篇文章前假定您已经掌握了OAuth2.0的基础知识,如您事先并未了解OAuth2.0,请参阅一下阮一峰老师的文章《理解OAuth2.0》(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html), ASP.NET Core 认证与授权,可以看看博客 雨夜朦胧(https://www.cnblogs.com/RainingNight),另外IdentityServer的相关文章也可以参考博客 晓晨Master(https://www.cnblogs.com/stulzq/)。

葫芦藤前端地址:https://account.suuyuu.cn (验证码获取后,输入123456即可)

葫芦藤后端地址:https://account-web.suuyuu.cn

葫芦藤源码地址:https://github.com/fuluteam/fulusso (帮忙点个小星星哦)

团队博文地址:https://www.cnblogs.com/fulu

签名证书(Signing Credential)

IdentityServer支持X.509证书(包括原始文件和对Windows证书存储库的引用)、RSA密钥和EC密钥,用于令牌签名和验证。每个密钥都可以配置一个(兼容的)签名算法,如RS256、RS384、RS512、PS256、PS384、PS512、ES256、ES384或ES512。

通常情况下,我们使用的是针对开发场景创建的临时证书 AddDeveloperSigningCredential,
生产环境怎么办呢?IdentityServer还提供了AddSigningCredential用来装载证书文件,
为此我们需要准备一个X.509证书,下面是在控制台项目中用于生成证书的代码,完整代码请参考项目:https://github.com/fuluteam/ICH.BouncyCastle

//颁发者DNvar issuer = new X509Name(new ArrayList{X509Name.C,X509Name.O,X509Name.OU,X509Name.L,X509Name.ST},new Hashtable{[X509Name.C] = "CN",[X509Name.O] = "Fulu Newwork",[X509Name.OU] = "Fulu RSA CA 2020",[X509Name.L] = "Wuhan",[X509Name.ST] = "Hubei"});//使用者DNvar subject = new X509Name(new ArrayList{X509Name.C,X509Name.O,X509Name.CN}, new Hashtable {[X509Name.C] = "CN",[X509Name.O] = "ICH",[X509Name.CN] = "*.fulu.com"});//生成证书文件CertificateGenerator.GenerateCertificate(newCertificateGenerator.GenerateCertificateOptions { Path = "mypfx.pfx",Issuer = issuer, Subject = subject });

执行代码后,在项目编译输出目录中,会看到一个mypfx.pfx的文件,此时我们的证书就创建成功啦。

接着怎么使用呢,看下面代码:

var certificate2 = new X509Certificate2("mypfx.pfx", "password", X509KeyStorageFlags.Exportable);identityServerBuilder.AddSigningCredential(certificate2);

大家可能会问,葫芦藤中怎么不是这么写的呢,其实葫芦藤项目中是将证书文件的流数据转成了二进制字符串,这样就可以写在配置文件中了:

using (var fs = new FileStream(options.Path, FileMode.Open)){var bytes = new byte[fs.Length];fs.Read(bytes, 0, bytes.Length);var pfxHexString = Hex.ToHexString(bytes);}

然后在这么使用:

identityServerBuilder.AddSigningCredential(new X509Certificate2(Hex.Decode(appSettings.X509RawCertData), appSettings.X509CertPwd));

客户端存储(Client Store)

在葫芦藤项目中,我们创建了一个ClientStore类,继承自接口IClientStore,实现其方法代码如下:

public class ClientStore : IClientStore{private readonly IClientCacheStrategy _clientInCacheRepository;public ClientStore(IClientCacheStrategy clientInCacheRepository){_clientInCacheRepository = clientInCacheRepository;}public async Task<Client> FindClientByIdAsync(string clientId){var clientEntity = await _clientInCacheRepository.GetClientByIdAsync(clientId.ToInt32());if (clientEntity == null){return null;}return new Client{ClientId = clientId,AllowedScopes = new[] { "api", "get_user_info" },ClientSecrets = new[] { new Secret(clientEntity.ClientSecret.Sha256()) },AllowedGrantTypes = new[]{GrantType.AuthorizationCode,    //授权码模式GrantType.ClientCredentials,    //客户端模式GrantType.ResourceOwnerPassword,    //密码模式CustomGrantType.External,   //自定义模式——三方(移动端)模式CustomGrantType.Sms //自定义——短信模式},AllowOfflineAccess = false,RedirectUris = string.IsNullOrWhiteSpace(clientEntity.RedirectUri) ? null : clientEntity.RedirectUri.Split(';'),RequireConsent = false,AccessTokenType = AccessTokenType.Jwt,AccessTokenLifetime = 7200,ClientClaimsPrefix = "",Claims = new[] { new Claim(JwtClaimTypes.Role, "Client") }};}}

通过代码可以看到,通过clientId从缓存中读取Client的相关信息构建并返回,这里我们为所有的Client简单的设置了统一的AllowedGrantTypes,这是一种偷懒的做法,应当按需授予GrantType,例如通常情况下我们只应默认给应用分配AuthorizationCode或者ClientCredentials,ResourceOwnerPassword需要谨慎授予(需要用户对Client高度信任)。

资源存储(Resource Store)

由于历史原因,在葫芦藤中,我们并没有通过IdentityServer对api资源进行访问保护(后续会提供我们的实现方式),我们为所有Client设置了相同的Scope。

持久化授权存储(Persisted Grant Store)

葫芦藤中,我们使用了Redis来持久化数据,

通过EntityFramework Core持久化配置和操作数据,请参考
https://www.cnblogs.com/stulzq/p/8120518.html
https://github.com/IdentityServer/IdentityServer4.EntityFramework

IPersistedGrantStore接口中定义了如下6个方法:

/// <summary>Interface for persisting any type of grant.</summary>public interface IPersistedGrantStore{/// <summary>Stores the grant.</summary>/// <param name="grant">The grant.</param>/// <returns></returns>Task StoreAsync(PersistedGrant grant);/// <summary>Gets the grant.</summary>/// <param name="key">The key.</param>/// <returns></returns>Task<PersistedGrant> GetAsync(string key);/// <summary>Gets all grants for a given subject id.</summary>/// <param name="subjectId">The subject identifier.</param>/// <returns></returns>Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId);/// <summary>Removes the grant by key.</summary>/// <param name="key">The key.</param>/// <returns></returns>Task RemoveAsync(string key);/// <summary>/// Removes all grants for a given subject id and client id combination./// </summary>/// <param name="subjectId">The subject identifier.</param>/// <param name="clientId">The client identifier.</param>/// <returns></returns>Task RemoveAllAsync(string subjectId, string clientId);/// <summary>/// Removes all grants of a give type for a given subject id and client id combination./// </summary>/// <param name="subjectId">The subject identifier.</param>/// <param name="clientId">The client identifier.</param>/// <param name="type">The type.</param>/// <returns></returns>Task RemoveAllAsync(string subjectId, string clientId, string type);}

PersistedGrant的结构如下:

/// <summary>A model for a persisted grant</summary>public class PersistedGrant{/// <summary>Gets or sets the key.</summary>/// <value>The key.</value>public string Key { get; set; }/// <summary>Gets the type.</summary>/// <value>The type.</value>public string Type { get; set; }/// <summary>Gets the subject identifier.</summary>/// <value>The subject identifier.</value>public string SubjectId { get; set; }/// <summary>Gets the client identifier.</summary>/// <value>The client identifier.</value>public string ClientId { get; set; }/// <summary>Gets or sets the creation time.</summary>/// <value>The creation time.</value>public DateTime CreationTime { get; set; }/// <summary>Gets or sets the expiration.</summary>/// <value>The expiration.</value>public DateTime? Expiration { get; set; }/// <summary>Gets or sets the data.</summary>/// <value>The data.</value>public string Data { get; set; }}

可以看出主要是针对PersistedGrant对象的操作,通过观察GetAsync和RemoveAsync方法的入参均为key,我们在StoreAsync中将PersistedGrant中的Key作为缓存key,将PersistedGrant对象以hash的方式存入缓存中,并设置过期时间(注意将UTC时间转换为本地时间)

public async Task StoreAsync(PersistedGrant grant){//var expiresIn = grant.Expiration - DateTimeOffset.UtcNow;var db = await _redisCache.GetDatabaseAsync();var trans = db.CreateTransaction();var expiry = grant.Expiration.Value.ToLocalTime();db.HashSetAsync(grant.Key, GetHashEntries(grant));  //GetHashEntries是将对象PersistedGrant转换为HashEntry数组db.KeyExpireAsync(grant.Key, expiry);await trans.ExecuteAsync();}

同时,把GetAsync和RemoveAsync的代码填上:

public async Task<PersistedGrant> GetAsync(string key){var db = await _redisCache.GetDatabaseAsync();var items = await db.HashGetAllAsync(key);return GetPersistedGrant(items);    //将HashEntry数组转换为PersistedGrant对象}public async Task RemoveAsync(string key){var db = await _redisCache.GetDatabaseAsync();await db.KeyDeleteAsync(key);}

接着,GetAllAsync方法,通过subjectId查询PersistedGrant集合,1对n,因此,我们在StoreAsync中补上这一层关系,以subjectId为缓存key,grant.Key为缓存值存入list集合中;GetAllAsync方法中,通过subjectId取出grant.Key的集合,最终得到PersistedGrant集合。

public async Task StoreAsync(PersistedGrant grant){//var expiresIn = grant.Expiration - DateTimeOffset.UtcNow;var db = await _redisCache.GetDatabaseAsync();var trans = db.CreateTransaction();var expiry = grant.Expiration.Value.ToLocalTime();db.HashSetAsync(grant.Key, GetHashEntries(grant));  //GetHashEntries是将对象PersistedGrant转换为HashEntry数组db.KeyExpireAsync(grant.Key, expiry);db.ListLeftPushAsync(grant.SubjectId, grant.Key);db.KeyExpireAsync(grant.SubjectId, expiry);await trans.ExecuteAsync();}public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId){if (string.IsNullOrWhiteSpace(subjectId))return new List<PersistedGrant>();var db = await _redisCache.GetDatabaseAsync();var keys = await db.ListRangeAsync(subjectId);var list = new List<PersistedGrant>();foreach (string key in keys){var items = await db.HashGetAllAsync(key);list.Add(GetPersistedGrant(items));}return list;}

类似的,StoreAsync方法中我们只需StoreAsync方法中根据RemoveAllAsync方法参数组装缓存key,grant.Key为缓存值写入缓存,对应的RemoveAllAsync中根据参数组装的key查询出grant.Key集合,删除缓存即可。

public async Task StoreAsync(PersistedGrant grant){var db = await _redisCache.GetDatabaseAsync();var trans = db.CreateTransaction();var expiry = grant.Expiration.Value.ToLocalTime();db.HashSetAsync(grant.Key, GetHashEntries(grant));db.KeyExpireAsync(grant.Key, expiry);if (!string.IsNullOrEmpty(grant.SubjectId)){db.ListLeftPushAsync(grant.SubjectId, grant.Key);db.KeyExpireAsync(grant.SubjectId, expiry);var key1 = $"{grant.SubjectId}:{grant.ClientId}";db.ListLeftPushAsync(key1, grant.Key);db.KeyExpireAsync(key1, expiry);var key2 = $"{grant.SubjectId}:{grant.ClientId}:{grant.Type}";db.ListLeftPushAsync(key2, grant.Key);db.KeyExpireAsync(key2, expiry);}await trans.ExecuteAsync();}public async Task RemoveAllAsync(string subjectId, string clientId){if (string.IsNullOrEmpty(subjectId) || string.IsNullOrEmpty(clientId))return;var db = await _redisCache.GetDatabaseAsync();var key = $"{subjectId}:{clientId}";var keys = await db.ListRangeAsync(key);if (!keys.Any()) return;var trans = db.CreateTransaction();db.KeyDeleteAsync(keys.ToRedisKeys());db.KeyDeleteAsync(key);await trans.ExecuteAsync();}public async Task RemoveAllAsync(string subjectId, string clientId, string type){if (string.IsNullOrEmpty(subjectId) || string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(type)) return;var db = await _redisCache.GetDatabaseAsync();var key = $"{subjectId}:{clientId}:{type}";var keys = await db.ListRangeAsync(key);if (!keys.Any()) return;var trans = db.CreateTransaction();db.KeyDeleteAsync(keys.ToRedisKeys());db.KeyDeleteAsync(key);await trans.ExecuteAsync();}

至此,持久化的代码填写完毕;启动并调试项目,可以看到PersistedGrant对象如下:

资源拥有者验证器(Resource Owner Validator)

如果要使用OAuth 2.0 密码模式(Resource Owner Password Credentials Grant),则需要实现并注册IResourceOwnerPasswordValidator接口:

public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context){var result = await _userService.LoginByPasswordAsync(context.UserName, context.Password);if (result.Code == 0){var claims = await _userService.SaveSuccessLoginInfo(context.Request.ClientId.ToInt32(), result.Data.Id,_contextAccessor.HttpContext.GetIp(), UserLoginModel.Password);context.Result = new GrantValidationResult(result.Data.Id, OidcConstants.AuthenticationMethods.Password, claims);}else{context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, result.Message);}}

重定向地址验证器(Redirect Uri Validator)

用于验证重定向(授权码模式)和注销后重定向Uri的校验,葫芦藤项目中重定向地址验证只验证域名(不验证完整的requestedUri地址),且未进行注销重定向Uri的校验。

public class RedirectUriValidator : IRedirectUriValidator{public Task<bool> IsRedirectUriValidAsync(string requestedUri, Client client){if (client.RedirectUris == null || !client.RedirectUris.Any()){return Task.FromResult(false);}var uri = new Uri(requestedUri);return Task.FromResult(client.RedirectUris.Any(x => x.Contains(uri.Host)));}public Task<bool> IsPostLogoutRedirectUriValidAsync(string requestedUri, Client client){return Task.FromResult(true);}}

扩展授权验证器(Extension Grant Validator)

在IdentityServer4中,通过实现IExtensionGrantValidator接口,可以实现自定义授权。在葫芦藤项目中,我们有两个场景需要用到自定义授权:

  • 通过第三方(QQ、微信)的用户标识(OpenId)进行登录(颁发用户令牌)

  • 通过短信验证码进行登录(颁发用户令牌)

在IdentityServer4中实现短信验证码授权模式,我们创建了一个SmsGrantValidator类,继承自IExtensionGrantValidator接口,然后给属性GrantType取一个名字,此处名称为“sms”,实现ValidateAsync方法,方法内进行入参校验,然后验证短信验证码,验证通过后取出用户信息,下面代码中,当用户不存在时也可以自动注册。代码如下:

public class SmsGrantValidator : IExtensionGrantValidator{private readonly IHttpContextAccessor _contextAccessor;private readonly IValidationComponent _validationComponent;private readonly IUserService _userService;public SmsGrantValidator(IHttpContextAccessor contextAccessor, IValidationComponent validationComponent, IUserService userService){_contextAccessor = contextAccessor;_validationComponent = validationComponent;_userService = userService;GrantType = CustomGrantType.Sms;}public async Task ValidateAsync(ExtensionGrantValidationContext context){var phone = context.Request.Raw.Get("phone");var code = context.Request.Raw.Get("code");if (string.IsNullOrEmpty(phone) || Regex.IsMatch(phone, RegExp.PhoneNumber) == false){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "phone is not valid");return;}if (string.IsNullOrEmpty(code)){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "code is not valid");return;}try{var validSms = await _validationComponent.ValidSmsAsync(phone, code);if (!validSms.Data){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, validSms.Message);return;}var userEntity = await _userService.GetUserByPhoneAsync(phone);if (userEntity == null){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "用户不存在或未注册");return;}if (userEntity.Enabled == false){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "您的账号已被禁止登录");return;}await _userService.SaveSuccessLoginInfo(context.Request.ClientId.ToInt32(), userEntity.Id, _contextAccessor.HttpContext.GetIp(),UserLoginModel.SmsCode);}catch (Exception ex){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, ex.Message);}}public string GrantType { get; }}

OAuth2.0的实践运用场景

基于角色的授权(role-based authorization)

基于角色的授权检查是声明性的,开发人员将其嵌入到代码中、控制器或控制器内的操作,指定当前用户必须是其成员的角色才能访问请求的资源,文档参考《ASP.NET Core 中的基于角色的授权》。

葫芦藤中定义了两种角色Claim(声明),客户端和用户,使用客户端授权模式(client credentials)颁发的令牌,ClaimRole为Client,使用授权码模式(authorization code)、密码模式(resource owner password credentials)、自定义授权模式(短信、第三方)颁发的用户令牌,ClaimRole为User

public static class ClaimRoles{/// <summary>/// 客户端/// </summary>public const string Client = "Client";/// <summary>/// 用户/// </summary>public const string User = "User";}

在ClientStore中增加返回Client的Claims,JwtClaimTypes.Role为ClaimRoles.Client,下面是客户端令牌,可以看到 “role”:”Client”

{"alg":"RS256","kid":"99AA0C1236097972F29789562761D38AAE301918","typ":"JWT","x5t":"maoMEjYJeXLyl4lWJ2HTiq4wGRg"}{"nbf":1608522625,"exp":1608529825,"iss":"http://localhost:80","aud":"api","client_id":"10000001","role":"Client","scope":["api","get_user_info"]}

在用户登录成功后返回的Claims中增加JwtClaimTypes.Role为ClaimRoles.User,下面是用户令牌,可以看到 “role”:”User”

{"alg":"RS256","kid":"99AA0C1236097972F29789562761D38AAE301918","typ":"JWT","x5t":"maoMEjYJeXLyl4lWJ2HTiq4wGRg"}{"nbf":1608522576,"exp":1608529776,"iss":"http://localhost:80","aud":"api","client_id":"10000001","sub":"df09efff-0074-4dca-91c3-e38180c5e4ac","auth_time":1608522576,"idp":"local","id":"df09efff-0074-4dca-91c3-e38180c5e4ac","open_id":"07E8E30B56D256EF8C440019AB6AAA89","name":"1051dfd1-73e5-4e6f-9326-3423bc9b71a3","nickname":"laowang","phone_number":"18627131390","email":"","role":"User","login_ip":"0.0.0.1","login_address":"保留地址","last_login_ip":"0.0.0.1","last_login_address":"保留地址","scope":["api","get_user_info"],"amr":["pwd","mfa"]}

在项目Fulu.Passport.API的Startup文件中,添加对组件Fulu.Service.Authorize的服务注入

services.AddServiceAuthorize(o =>...代码省略...);
services.AddAuthentication(x =>...代码省略...).AddJwtBearer(o =>{...代码省略...o.TokenValidationParameters = new TokenValidationParameters{NameClaimType = JwtClaimTypes.Name,RoleClaimType = ClaimTypes.Role,    //注意,这里不能使用JwtClaimTypes.Role...代码省略...}}

接着,只需在Controller或Action上指定属性即可

[Route("api/[controller]/[action]")][ApiController][Authorize(Roles = ClaimRoles.Client)]public class ClientController : ControllerBase{...省略部分代码.../// <summary>/// 获取应用列表/// </summary>/// <returns></returns>[HttpGet][ProducesResponseType(typeof(ActionObjectResult<List<ClientEntity>, Statistic>), 200)]public async Task<IActionResult> GetClients(){var clients = await _clientRepository.TableNoTracking.Where(c => c.Enabled).ToListAsync();return ObjectResponse.Ok(clients);}...省略部分代码...

客户端授权模式(client credentials)

通过客户端授权模式颁发的令牌,可以实现对服务资源进行保护。步骤如下:

(A)客户端10000001向葫后进行身份认证,并要求一个访问令牌。(B)葫后验证客户端身份后,向客户端10000001提供访问令牌。

A步骤中,客户端10000001发出的HTTP请求,包含以下参数:

  • grant_type:表示授权类型,此处的值固定为”clientcredentials”,必选项。

  • client_id:表示客户端的ID,必选项。

  • client_secret:表示客户端密钥,必选项。

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1Host: www.xxx.comContent-Type: application/x-www-form-urlencodedgrant_type=client_credentials&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w

B步骤中,葫芦藤向客户端10000001发放令牌,下面是一个例子。

HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8Cache-Control: no-store, no-cache, max-age=0Pragma: no-cache{"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk5QUEwQzEyMzYwOTc5NzJGMjk3ODk1NjI3NjFEMzhBQUUzMDE5MTgiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJtYW9NRWpZSmVYTHlsNGxXSjJIVGlxNHdHUmcifQ.eyJuYmYiOjE2MDc0MTQ2MjUsImV4cCI6MTYwNzQyMTgyNSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwicm9sZSI6IkNsaWVudCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkNsaWVudCIsInNjb3BlIjpbImFwaSIsImdldF91c2VyX2luZm8iXX0.ilu1qMxDiXVxsqU6aO-xuyYaLvvj2mxONjYkXtpMs46K7O3_Qc5VsY0ZZaYPoLROAqPulxsWWpxjEiQd10OdRh4IziGAcpYfAfoD80CZxrcuWrWloB5aWncv_PMZcjzKw7Vt3G3g-WkJl4amTta498hZJ3B-N-ReLhl-3ICSMFU8PU_ZVtEB-2lRx93rVyPIaQu_DWmpyW4Bdf2ocYm4RPQAEsvBToEFObbWPG6paLWIjrSN2aQPvsRWziorvlIhyFV5L6oyFIGIrZxdLJTOsvRQaevpV1sbv9pD_Z9PZDbSQiQDbWQv0MfrYB0Npc6VQlIMkL2GPNlQ8NgwyGT1sQ","expires_in": 7200,"token_type": "Bearer","scope": "api get_user_info"}

授权码模式(authorization code)

葫芦藤项目通过授权码模式(authorization code)实现了单点登录,通过授权码模式拿到用户令牌。目前葫芦藤只有一个应用(葫芦藤安全中心),这里为了不把概念搞混淆,我们假定百度(客户端10000002,redirect_uri 为 http://www.baidu.com)接入了咱们的授权体系,当然,百度的前端肯定没有写如何构造请求步骤的逻辑代码,因此,我们下面通过人工模拟请求步骤。

名词定义

  • 葫芦藤的client_id是10000001,百度的client_id是10000002

  • 葫芦藤前端服务,简称“葫前”(https://account.suuyuu.cn)

  • 葫芦藤后端服务,简称“葫后”(https://account-web.suuyuu.cn)

  • 百度前端服务,简称“百前”(https://www.baidu.com)

  • 百度后端服务,简称“百后”(假定地址为 https://api.baidu.com)

(A)用户访问“百前”,“百前”将用户导向“葫后”。
(B)“葫后”检查用户是否需要登录(是否携带了有效的登录Cookie),如需登录跳转到“葫前”。
(C)用户登录后,“葫后”将用户导向百度事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)“百前”收到授权码,附上早先的"重定向URI",向“百后”申请令牌,“百后”拿到授权码之后携带密钥client_secret向“葫后”申请令牌。
(E)“葫后”核对了授权码和重定向URI,确认无误后,向“百后”颁发访问令牌(access token)。
(F)“百后”将令牌返回给“百前”。

A步骤中,构造的请求地址包含以下参数:

  • response_type:表示授权类型,必选项,此处的值固定为”code”

  • client_id:表示客户端的ID,必选项

  • redirect_uri:表示重定向URI,可选项

  • scope:表示申请的权限范围,可选项

  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值

步骤A中开发人员需向前端人员提供client_id,即上面的client_id,下面是一个例子。

构造如下地址,复制到浏览器地址栏中并回车,如果跳转到登录页,请进行登录。

https://account-web.suuyuu.cn/connect/authorize?client_id=10000002&redirect_uri=https%3A%2F%2Fwww.baidu.com&response_type=code&scope=api&state=STATE

登录后会重定向redirect_uri到如下地址:

https://www.baidu.com/?code=1MlxrvXuD7TfH-s4dLzcw9ymO0SKDbf5xAlh3ZEHlMo&scope=api&state=STATE

D步骤中,我们通过临时授权码向“葫后”索取令牌,包含以下参数:

  • grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code”。

  • code:表示上一步获得的授权码,必选项。

  • redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。

  • client_id:表示应用ID,必选项。

  • client_secret:表示应用密钥,必选项。

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1Host: account-web.suuyuu.cnContent-Type: application/x-www-form-urlencodedgrant_type=authorization_code&code=1MlxrvXuD7TfH-s4dLzcw9ymO0SKDbf5xAlh3ZEHlMo&redirect_uri=https%3A%2F%2Fwww.baidu.com&client_id=10000002&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w
{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwQzQ3OUY1QUIyQTFERjM2QzE0MkNEQjQ3NjQ1QkEwMzQ1MTg1NUEiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJjTVI1OWFzcUhmTnNGQ3piUjJSYm9EUlJoVm8ifQ.eyJuYmYiOjE2MDc0MjY0MjcsImV4cCI6MTYwNzQzMzYyNywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwiYXV0aF90aW1lIjoxNjA3NDI2MTk2LCJpZHAiOiJsb2NhbCIsImlkIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwib3Blbl9pZCI6IjA3RThFMzBCNTZEMjU2RUY4QzQ0MDAxOUFCNkFBQTg5IiwibmFtZSI6IjEwNTFkZmQxLTczZTUtNGU2Zi05MzI2LTM0MjNiYzliNzFhMyIsIm5pY2tuYW1lIjoibGFvd2FuZyIsInBob25lX251bWJlciI6IjE4NjI3MTMxMzkwIiwiZW1haWwiOiIiLCJyb2xlIjoiVXNlciIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVzZXIiLCJsb2dpbl9pcCI6IjExMy41Ny4xMTguNTEiLCJsb2dpbl9hZGRyZXNzIjoi5rmW5YyX55yB5q2m5rGJ5biCIiwibGFzdF9sb2dpbl9pcCI6IjExMy41Ny4xMTguNTEiLCJsYXN0X2xvZ2luX2FkZHJlc3MiOiLmuZbljJfnnIHmrabmsYnluIIiLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbIm1mYSJdfQ.ElnHr5Niknq7kzGL8iv1TH0F6NQ21yPrswzSTIZuvetUxztYgQpD-RfgBW2HL6b_rRyQxFjE23gU4lBIEayM8k3M9_sUzZq8E_dFT8LwpsU76-CxepxHft4hn1YG0a5C6QRyjFQoSFVUZXIp663Es7vwRQ6PgsfkHZKXxAqXL-obHj_QLbv6OeciTIRGwYrL9-1_SDQ4esFR2n8LkGGOug55j9QuQEKMCufQLJ-nB3y7A2-0mnNoiuF2BBYSPLamcvMcLe8LbhCITLrHkcUSc6tsSdnEeisS6BMIoiyRq-LR2jJwDD30swTPFd85v6kUBJ3ZnWjeCqsluGGKHrwDLA","expires_in":7200,"token_type":"Bearer","scope":"api"}

密码模式(resource owner password credentials)

密码模式主要用于给可信应用颁发用户令牌,此类应用有个性化的登录页(不依赖单点登录,葫芦藤的登录页面),如app、小程序、h5等。

  • grant_type:表示授权类型,此处的值固定为”password”,必选项。

  • client_id:表示客户端的ID,必选项。

  • client_secret:表示客户端密钥,必选项。

  • username:用户名,必选项。

  • password:密码,必选项。(基于密码原文的rsa加密串)

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1Host: account-web.suuyuu.cnContent-Type: application/x-www-form-urlencodedgrant_type=password&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w&username=18627131390&password=0200f6389afbcbc624811785c9fbbf5c1b6d7b53b1315a1a43021c0733323fab7625bb9e6594cd30758fa700798421bc189dc223bf696d2438530ffab337809b96bb47ee38f3416bf4b57222050d5f4ad66ee052598ea62ff5ec6f991729956cb692f6f48b758564a46aeff86208581cad9063d3ccd71b551fa4b4b4b983fc1a
{"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwQzQ3OUY1QUIyQTFERjM2QzE0MkNEQjQ3NjQ1QkEwMzQ1MTg1NUEiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJjTVI1OWFzcUhmTnNGQ3piUjJSYm9EUlJoVm8ifQ.eyJuYmYiOjE2MDc1MTE2NTEsImV4cCI6MTYwNzUxODg1MSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwiYXV0aF90aW1lIjoxNjA3NTExNjUxLCJpZHAiOiJsb2NhbCIsImlkIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwib3Blbl9pZCI6IjA3RThFMzBCNTZEMjU2RUY4QzQ0MDAxOUFCNkFBQTg5IiwibmFtZSI6IjEwNTFkZmQxLTczZTUtNGU2Zi05MzI2LTM0MjNiYzliNzFhMyIsIm5pY2tuYW1lIjoibGFvd2FuZyIsInBob25lX251bWJlciI6IjE4NjI3MTMxMzkwIiwiZW1haWwiOiIiLCJyb2xlIjoiVXNlciIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVzZXIiLCJsb2dpbl9pcCI6IjExMy41Ny4xMTguNjEiLCJsb2dpbl9hZGRyZXNzIjoi5rmW5YyX55yB5q2m5rGJ5biCIiwibGFzdF9sb2dpbl9pcCI6IjExMy41Ny4xMTguNjEiLCJsYXN0X2xvZ2luX2FkZHJlc3MiOiLmuZbljJfnnIHmrabmsYnluIIiLCJzY29wZSI6WyJhcGkiLCJnZXRfdXNlcl9pbmZvIl0sImFtciI6WyJwd2QiLCJtZmEiXX0.d3qvhX6KSdm5EgWpUzbjJX2bB1OiUo-285nZ1qsGKpqTQJUH1VHQoJogB0NI-uVYdgIV-y3CMBhFY_fDYQJto43zDf0gDvYxa2eWnX5MWL7Augigi59Icp0YvNDCGd2iT5ztAWpxk1Jww815TtCFtFFGiQfQC75bKLrTW9QvdXr8t4VHcFKGmz92m8g3WL-0eWqAyvk0YuSBvxOd8P8zoocEiiOgVKTSylphSIQxuC8B4MFNf2DoFWDQjNZmDCs7PLh7sniMmLdfilo7T7gAlq9qjUrmQmav4wbDMT8WZqa01WY-LsWq6mZUnbCytgSu7Xrr90b6LAEGn-hxdQ5VHg","expires_in": 7200,"token_type": "Bearer","scope": "api get_user_info"}

自定义授权模式(短信、第三方)(extension grant)

客户端通过用户手机号短信验证码或第三方用户(QQ、WeChat)的用户唯一标识(OpenId)向认证服务器索要用户令牌。

以短信验证码方式为例,我们定义的流程如下:

用户向客户端提供自己的手机号和短信验证码。客户端使用这些信息,向认证服务器索要授权。步骤如下:

(A)用户向客户端提供手机号和短信验证码。

(B)客户端将手机号和短信码发给认证服务器,向后者请求令牌。

(C)认证服务器确认无误后,向客户端提供用户令牌。

B步骤中,客户端发出的HTTP请求,包含以下参数:

  • grant_type:表示授权类型,此处的值固定为”sms”,必选项。

  • client_id:表示客户端的ID,必选项。

  • client_secret:表示客户端的密钥,必选项。

  • phone:表示手机号,必选项。

  • code:表示短信验证码,必选项。

下面是一个请求示例。

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1Host: account-web.suuyuu.cnContent-Type: application/x-www-form-urlencodedgrant_type=sms&phone=18627131390&code=123456&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w
{"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk5QUEwQzEyMzYwOTc5NzJGMjk3ODk1NjI3NjFEMzhBQUUzMDE5MTgiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJtYW9NRWpZSmVYTHlsNGxXSjJIVGlxNHdHUmcifQ.eyJuYmYiOjE2MDczOTU4NTIsImV4cCI6MTYwNzQwMzA1MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiMTg2MjcxMzEzOTAiLCJhdXRoX3RpbWUiOjE2MDczOTU4NTIsImlkcCI6ImxvY2FsIiwiaWQiOiJkZjA5ZWZmZi0wMDc0LTRkY2EtOTFjMy1lMzgxODBjNWU0YWMiLCJvcGVuX2lkIjoiMDdFOEUzMEI1NkQyNTZFRjhDNDQwMDE5QUI2QUFBODkiLCJuYW1lIjoiMTA1MWRmZDEtNzNlNS00ZTZmLTkzMjYtMzQyM2JjOWI3MWEzIiwibmlja25hbWUiOiJsYW93YW5nIiwicGhvbmVfbnVtYmVyIjoiMTg2MjcxMzEzOTAiLCJlbWFpbCI6IiIsInJvbGUiOiJVc2VyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiVXNlciIsImxvZ2luX2lwIjoiMC4wLjAuMSIsImxvZ2luX2FkZHJlc3MiOiLkv53nlZnlnLDlnYAiLCJsYXN0X2xvZ2luX2lwIjoiMC4wLjAuMSIsImxhc3RfbG9naW5fYWRkcmVzcyI6IuS_neeVmeWcsOWdgCIsInNjb3BlIjpbImFwaSIsImdldF91c2VyX2luZm8iXSwiYW1yIjpbInBhc3N3b3JkIiwibWZhIl19.ZQklMJMXObc3vL-gMOWnWIS56ck5_XbDfXjw9Vm6BeYjG4dyz05JTN_YHgU-EIJoM04nmFyjNgGYtqL-28-3MQeHfWhvQf_5dyY1w-DBBCKo1EMEm_ujKTDB1QQTN1XmVTgW7bBkEiv4NK5v3uYqh_s7pv8Csusm4oWZThWPlKLtxWVDtawFzvz4Un-2WATytsLNfluutiLVnpN7INhkdglansTTOCUOdCOLBEEbDzTuLyCnhm00xYtg5GrMAkDohqXLKYD2jSFzIyYTA_oryTFXcJpkGYwIRqRX7bXvAlMR5yE_CTtNWpSnaLJ2GtFv_QFe-YItCtSO-bBd6XQBRA","expires_in": 7200,"token_type": "Bearer","scope": "api get_user_info"}

第三方授权登录的编写与使用

在葫芦藤项目中我们提供了钉钉、微信的OAuth组件,并实现了功能,演示地址在 https://account.suuyuu.cn,下面我们以微信为例简单介绍下如何编写组件及使用。

首先咱们阅读一下网站应用微信登录开发指南,了解一下接入流程。要使用微信登录,先得在微信·开放平台注册成为开发者,并进行资质认证。

微信开放平台帐号的开发者资质认证提供更安全、更严格的真实性认证、也能够更好的保护企业及用户的合法权益开发者资质认证通过后,微信开放平台帐号下的应用,将获得微信登录、智能接口、第三方平台开发等高级能力审核费用:中国大陆地区:300元,非中国大陆地区:99美元

然后在管理中心创建网站应用

对照微信开发指南将需要用到的地址定义到WeChatDefaults.cs中

public static class WeChatDefaults{public const string AuthenticationScheme = "wechat";public static readonly string DisplayName = "wechat";//第一步:请求CODEpublic static readonly string AuthorizationEndpoint = "https://open.weixin.qq.com/connect/qrconnect";//第二步:通过code获取access_tokenpublic static readonly string TokenEndpoint = "https://api.weixin.qq.com/sns/oauth2/access_token";//第三步:获取用户个人信息public static readonly string UserInformationEndpoint = "https://api.weixin.qq.com/sns/userinfo";}

此处唯一要注意的地方,ClaimActions集合的参数来自微信返回的字段

public class WeChatOptions : OAuthOptions{/// <summary>/// Initializes a new <see cref="WeChatOptions"/>./// </summary>public WeChatOptions(){CallbackPath = new PathString("/signin-wechat");AuthorizationEndpoint = WeChatDefaults.AuthorizationEndpoint;TokenEndpoint = WeChatDefaults.TokenEndpoint;UserInformationEndpoint = WeChatDefaults.UserInformationEndpoint;Scope.Add("snsapi_login");ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "openid");ClaimActions.MapJsonKey(ClaimTypes.Name, "nickname");}/// <summary>/// access_type. Set to 'offline' to request a refresh token./// </summary>public string AccessType { get; set; }}
public static class WeChatExtensions{public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder)=> builder.AddWeChat(WeChatDefaults.AuthenticationScheme, _ => { });public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, Action<WeChatOptions> configureOptions)=> builder.AddWeChat(WeChatDefaults.AuthenticationScheme, configureOptions);public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, string authenticationScheme, Action<WeChatOptions> configureOptions)=> builder.AddWeChat(authenticationScheme, WeChatDefaults.DisplayName, configureOptions);public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WeChatOptions> configureOptions)=> builder.AddOAuth<WeChatOptions, WeChatHandler>(authenticationScheme, displayName, configureOptions);}

新增一个类WeChatHandler,继承自OAuthHandler

BuildChallengeUrl(构造客户端申请认证的URI)

protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri){var state = Options.StateDataFormat.Protect(properties);var baseUri = $"{Request.Scheme}{Uri.SchemeDelimiter}{Request.Host}{Request.PathBase}";var currentUri = $"{baseUri}{Request.Path}{Request.QueryString}";if (string.IsNullOrEmpty(properties.RedirectUri)){properties.RedirectUri = currentUri;}var queryStrings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase){{"response_type", "code"},{"appid", Uri.EscapeDataString(Options.ClientId)},{"redirect_uri", redirectUri},{"state", Uri.EscapeDataString(state)}};var scope = string.Join(",", Options.Scope);queryStrings.Add("scope", Uri.EscapeDataString(scope));var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings);return authorizationEndpoint;}

HandleRemoteAuthenticateAsync(向认证服务器申请令牌获取用户信息并创建票据)

protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync(){var state = Request.Query["state"];var properties = Options.StateDataFormat.Unprotect(state);if (properties == null)return HandleRequestResult.Fail("The oauth state was missing or invalid.");if (!ValidateCorrelationId(properties))return HandleRequestResult.Fail("Correlation failed.", properties);var code = Request.Query["code"];if (StringValues.IsNullOrEmpty(code))return HandleRequestResult.Fail("Code was not found.", properties);var redirectUri = !string.IsNullOrEmpty(Options.CallbackPath) ?Options.CallbackPath.Value : BuildRedirectUri(Options.CallbackPath);var context = new OAuthCodeExchangeContext(properties, code, redirectUri);var tokens = await ExchangeCodeAsync(context);if (tokens.Error != null)return HandleRequestResult.Fail(tokens.Error, properties);if (string.IsNullOrEmpty(tokens.AccessToken))return HandleRequestResult.Fail("Failed to retrieve access token.", properties);var identity = new ClaimsIdentity(ClaimsIssuer);if (Options.SaveTokens){var authenticationTokenList = new List<AuthenticationToken>{new AuthenticationToken{Name = "access_token",Value = tokens.AccessToken}};if (!string.IsNullOrEmpty(tokens.RefreshToken)){authenticationTokenList.Add(new AuthenticationToken{Name = "refresh_token",Value = tokens.RefreshToken});}if (!string.IsNullOrEmpty(tokens.TokenType)){authenticationTokenList.Add(new AuthenticationToken{Name = "token_type",Value = tokens.TokenType});}if (!string.IsNullOrEmpty(tokens.ExpiresIn) && int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)){var dateTimeOffset = Clock.UtcNow + TimeSpan.FromSeconds(result);authenticationTokenList.Add(new AuthenticationToken(){Name = "expires_at",Value = dateTimeOffset.ToString("o", CultureInfo.InvariantCulture)});}properties.StoreTokens(authenticationTokenList);}var ticket = await CreateTicketAsync(identity, properties, tokens);return ticket == null ? HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties) : HandleRequestResult.Success(ticket);}

此步骤中包含两个子步骤

ExchangeCodeAsync(交换授权码Code)

protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context){var tokenRequestParameters = new List<KeyValuePair<string, string>>{new KeyValuePair<string, string>("appid", Options.ClientId),new KeyValuePair<string, string>("secret", Options.ClientSecret),new KeyValuePair<string, string>("code", context.Code),new KeyValuePair<string, string>("grant_type", "authorization_code"),};var urlEncodedContent = new FormUrlEncodedContent(tokenRequestParameters);var response =await Backchannel.PostAsync(Options.TokenEndpoint, urlEncodedContent, Context.RequestAborted);return response.IsSuccessStatusCode ? OAuthTokenResponse.Success(JsonDocument.Parse(await response.Content.ReadAsStringAsync())) : OAuthTokenResponse.Failed(new Exception("OAuth token failure"));}

CreateTicketAsync(创建票据)

protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity,AuthenticationProperties properties,OAuthTokenResponse tokens){var openId = tokens.Response.RootElement.GetString("openid");var parameters = new Dictionary<string, string>{{  "openid", openId},{  "access_token", tokens.AccessToken }};var userInfoEndpoint = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, parameters);var response = await Backchannel.GetAsync(userInfoEndpoint, Context.RequestAborted);if (!response.IsSuccessStatusCode){throw new HttpRequestException($"An error occurred when retrieving WeChat user information ({response.StatusCode}). Please check if the authentication information is correct.");}using (var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync())){var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme,Options, Backchannel, tokens, payload.RootElement);context.RunClaimActions();await Events.CreatingTicket(context);context.Properties.ExpiresUtc = DateTimeOffset.Now.AddMinutes(15);return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);}}

组件写好了,怎么使用呢?在Fulu.Passport.Web项目的Startup.cs文件中添加代码如下:

public void ConfigureServices(IServiceCollection services){......省略部分代码......services.AddAuthentication().AddWeChat(o =>{o.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;o.ClientId = Configuration["ExternalWeChat:AppId"];o.ClientSecret = Configuration["ExternalWeChat:Secret"];})}

接着,在UserController.cs中添加如下代码:

/// <summary>/// 外部账号登录/// </summary>/// <param name="model"></param>/// <returns></returns>[HttpGet, AllowAnonymous]public IActionResult ExternalLogin([FromQuery] ExternalLoginModel model){var authenticationProperties = new AuthenticationProperties(){RedirectUri = Url.Action(nameof(ExternalLoginCallback)),Items ={{ "returnUrl", model.ReturnUrl },{ "scheme", model.Provider },}};return Challenge(authenticationProperties, model.Provider);}/// <summary>/// 外部登录回调/// </summary>/// <returns></returns>[HttpGet][AllowAnonymous]public async Task<IActionResult> ExternalLoginCallback(){//获取idsrv.external Cookie 对象var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);var returnUrl = result.Properties.Items["returnUrl"];if (result.Succeeded == false){return await RedirectErrorResult("error", "External authentication error", returnUrl);}......省略部分代码......//删除 idsrv.external Cookieawait HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);//写入 .AspNetCore.Cookiesawait SignIn(userEntity, UserLoginModel.External);return Redirect(returnUrl);}

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

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

相关文章

java io 读取多个对象_Java IO系列(五):读写对象ObjectOutputStream和ObjectInputStream详解...

有必要多看几遍的关于字符和字节&#xff0c;例如文本文件&#xff0c;XML这些都是用字符流来读取和写入。而如RAR&#xff0c;EXE文件&#xff0c;图片等非文本&#xff0c;则用字节流来读取和写入。读写对象&#xff0c;传输对象在Java中很常使用&#xff0c;在javaBean中就经…

Beetlex之websocket/tls服务压测工具

为了方便压力测试ws服务&#xff0c;Beetlex同样提供相关工具来对ws/wss服务的性能进行测试测试。安装可以访问https://github.com/beetlex-io/TCPBenchmarks 下载最新版本工作&#xff0c;工具可以运行在linux和windows系统&#xff0c;安装完成后通过浏览器访问相关服务进行操…

大学班里80%都去培训班,为什么我没去

背景大四刚开始&#xff0c;各大培训机构就开始到各大高校开始宣传&#xff0c;我们学校也不例外&#xff1b;当时信息与计算科学这专业在我们学校并不景气&#xff0c;有好几次听说&#xff1a;如果我们那届的就职率还不行的话&#xff0c;那很大可能将此专业拿掉&#xff1b;…

如何在 C# 中使用 Buffer

缓冲区 是内存中的一组字节序列&#xff0c;缓冲 是用来处理落在内存中的数据&#xff0c;.NET 缓冲 指的是处理 非托管内存 中的数据&#xff0c;用 byte[] 来表示。当你想把数据写入到内存或者你想处理非托管内存中的数据&#xff0c;可以使用 .NET 提供的 System.Buffer类&a…

.NET SDK-Style 项目(Core、Standard、.NET5)中的版本号

.NET SDK-Style 项目&#xff08;Core、Standard、.NET5&#xff09;中的版本号独立观察员 2020 年 12 月 24 日之前 .NET Framework 时&#xff0c;项目版本号等信息是存储在 AssemblyInfo.cs 文件中&#xff0c;通过程序集特性进行设置&#xff1a;.NET Core 之后&#xff0c…

[Stardust]星尘分布式全链路监控

随着业务的发展&#xff0c;微服务系统会变得越来越大&#xff0c;各个服务之间的调用关系也会日趋复杂。一个WebApi请求&#xff0c;后方可能经历多个微服务以及数据库和MQ操作&#xff0c;在这个调用过程中&#xff0c;可能因为某一个服务节点出现延迟或者失败&#xff0c;而…

如何在 C# 中使用 const,readonly,static

平时在开发时经常会用到 const,readonly,static 关键字&#xff0c;可以肯定这些关键词是完全不同的概念&#xff0c;但有时候他们在用法上很相似以至于在场景中不知道选择哪一个&#xff0c;这篇文章我们就来讨论 C# 中的 const&#xff0c;static 和 readonly 关键词&#xf…

2020,你收获了什么?又失去了什么?

这是头哥侃码的第228篇原创今天是圣诞节&#xff0c;既魔幻又真实的2020年&#xff0c;还有6天就要结束了。每年的年末&#xff0c;我都习惯在下班途中&#xff0c;把头靠在地铁车厢两侧的扶手上&#xff0c;闭上眼睛开始思考自己过去一年的收获与得失&#xff0c;并把这些思绪…

2019龙少php泛站群,龙少php泛站群|PHP版站群 全自动泛解析站群程序 赠送教程

首先将准备建站的域名设置泛解析如baidu.com泛解析为 添加A记录为 *.baidu.com然后在iis里添加空头主机不能设置 汉字目录keywords.txt 关键字&#xff0c;一行一个domain.txt 域名&#xff0c;一行一个&#xff0c;*代表随机muban.html 模板文件&#xff0c;可以修改缓存在dat…

轻量级消息队列RedisQueue

消息队列&#xff08;Message Queue&#xff09;是分布式系统必不可少的中间件&#xff0c;大部分消息队列产品&#xff08;如RocketMQ/RabbitMQ/Kafka等&#xff09;要求团队有比较强的技术实力&#xff0c;不适用于中小团队&#xff0c;并且对.NET技术的支持力度不够。而Redi…

生态和能力是国内自研操作系统发展的关键

“缺芯少魂”一直是我国信息产业短板&#xff0c;如果无法实现国产化替代&#xff0c;信息安全和产业安全就犹如沙滩上盖房子&#xff0c;上层再坚固&#xff0c;地基不稳&#xff0c;一遇到风吹草动就有可能全部垮掉。近年来&#xff0c;国内自研操作系统厂商动作频频&#xf…

matlab群延时函数,群延迟函数(group delay function)群延迟滤波器 | 学步园

最近看了许多介绍Group delay function的论文&#xff0c;文章中大篇幅提到Group delay&#xff0c;group delay of digital filters,对这个方面的知识好像还挺有用的&#xff0c;所以想把它记录下来。然后总结下计算Group delay function的步骤。假设有N个样本的脉冲响应为h(n…

怎样使用C# 获取WIFI的连接状态?

怎样使用C# 获取WIFI的连接状态&#xff1f;行文导航思路问题得到解决代码展示断开与连接WIFI状态效果在OrangePI Linux Arm32上的测试效果C# 获取WIFI的连接状态本文是在知道WIFI网络设备名称的情况下&#xff0c;获取该设备的连接状态&#xff0c;同样也是可以判断是否已连接…

如何在 ASP.NET Core 中使用 URL Rewriting 中间件

URL rewriting 是根据预先配置好的一组规则去修改 request url&#xff0c;值得注意的是&#xff1a;URL Rewriting 的重写功能和 url 重定向 是两个概念&#xff0c;本篇我们就来讨论下如何在 ASP.NET Core 中对 url 进行 rewriting。安装 URL Rewriting 中间件 要想使用 URL …

睡眠分期matlab代码,非接触式睡眠分期方法与流程

本发明属于雷达监测技术领域&#xff0c;特别是一种非接触式睡眠分期方法。背景技术&#xff1a;传统的呼吸睡眠监护系统主要依靠贴附于人体的接触式传感器、电极进行测量&#xff0c;从而实时获得人体的生命参数信号&#xff0c;这些方法都需要直接或间接地接触人体&#xff0…

叮咚!你有一份来自明源云的圣诞邀约

请查收&#xff0c;来自明源云的圣诞邀约&#xff5e;

java先进先出 循环队列,JavaScript队列、优先队列与循环队列

队列是一种遵从先进先出(FIFO)原则的有序集合队列在尾部添加新元素&#xff0c;从顶部移除元素队列的理解队列在我们生活中最常见的场景就是排队了队列这个名字也已经很通俗易懂了和栈很像&#xff0c;这不过队列是先入先出的数据结构队列的前面是队头队列的后面是队尾出队从队…

Abp小试牛刀之 图片上传

图片上传是很常见的功能&#xff0c;里面有些固定的操作也可以沉淀下来。本文记录使用Abp vNext做图片上传的姿势。目标上传图片----->预览图片----->确定保存支持集群部署实现思路&#xff1a;1. 上传图片要使用WebAPI特定媒体类型&#xff1a;multipart/form-data;2. 因…

.Net Conf 2020 之回顾

Intro上周 .NET Conf 在苏州成功举办了第二届活动&#xff0c;一年一度的 .NET 盛会又来了&#xff0c;今年大会依然有许多从外地过来参加的开发者们&#xff0c;也有很多讲师也是从外地赶过来为我们分享。虽然今年是疫情的一年&#xff0c;并没有影响 .NET Conf 参会者们的热情…

如何使用 C# 在异步代码中处理异常

异常处理是一种处理运行时错误的技术&#xff0c;而 异步编程 允许我们在处理资源密集型的业务逻辑时不需要在 Main 方法或者在 执行线程 中被阻塞&#xff0c;值得注意的是&#xff0c;异步方法和同步方法的异常处理机制是不一样的&#xff0c;本篇我们就来讨论下如何在异步方…