使用 DataAnnotations(数据注解)实现模型的通用数据校验
参数校验的意义在实际项目开发中,无论任何方式、任何规模的开发模式,项目中都离不开对接入数据模型参数的合法性校验,目前普片的开发模式基本是前后端分离,当用户在前端页面中输入一些表单数据时,点击提交按钮,触发请求目标服务器的一系列后续操作,在这中间的执行过程中(标准做法推荐)无论是前端代码部分,还是服务端代码部分都应该有针对用户输入数据的合法性校验,典型做法如下:前端部分:当用户在页面输入表单数据时,前端监听页面表单事件触发相应的数据合法性校验规则,当数据非法时,合理的提示用户数据错误,只有当所有表单数据都校验通过后,才继续提交数据给目标后端对应的接口;后端部分:当前端数据合法校验通过后,向目标服务器提交表单数据时,服务端接收到相应的提交数据,在入口源头出就应该触发相关的合法性校验规则,当数据都校验通过后,继续执行后续的相关业务逻辑处理,反之则响应相关非法数据的提示信息;特别说明:在实际的项目中,无论前端部分还是服务端部分,参数的校验都是很有必要性的。无效的参数,可能会导致应用程序的异常和一些不可预知的错误行为。常用参数的校验这里例举一些项目中比较常用的参数模型校验项,如下所示:Name:姓名校验,比如需要是纯汉字的姓名;Password:密码强度验证,比如要求用户输入必须包含大小写字母、数字和特殊符号的强密码;QQ号:QQ 号码验证,是否是有效合法的 QQ 号码;China Postal Code:中国邮政编码;IP Address:IPV4 或者 IPV6 地址验证;Phone:手机号码或者座机号码合法性验证;ID Card:身份证号码验证,比如:15 位和 18 位数身份证号码;Email Address:邮箱地址的合法性校验;String:字符串验证,比如字段是否不为 null、长度是否超限;URL:验证属性是否具有 URL 格式;Number:数值型参数校验,数值范围校验,比如非负数,非负整数,正整数等;File:文件路径及扩展名校验;对于参数校验,常见的方式有正则匹配校验,通过对目标参数编写合法的正则表达式,实现对参数合法性的校验。.NET 中内置 DataAnnotations 提供的特性校验上面我们介绍了一些常用的参数验证项,接下来我们来了解下在 .NET 中内置提供的 DataAnnotations 数据注解,该类提供了一些常用的验证参数特性。官方解释:提供用于为 ASP.NET MVC 和 ASP.NET 数据控件定义元数据的特性类。该类位于 System.ComponentModel.DataAnnotations 命名空间。关于 DataAnnotations 中的特性介绍让我们可以通过这些特性对 API 请求中的参数进行验证,常用的特性一般有:[ValidateNever]: 指示应从验证中排除属性或参数。[CreditCard]:验证属性是否具有信用卡格式。[Compare]:验证模型中的两个属性是否匹配。[EmailAddress]:验证属性是否具有电子邮件格式。[Phone]:验证属性是否具有电话号码格式。[Range]:验证属性值是否位于指定范围内。[RegularExpression]:验证属性值是否与指定的正则表达式匹配。[Required]:验证字段是否不为 null。[StringLength]:验证字符串属性值是否不超过指定的长度限制。[Url]:验证属性是否具有 URL 格式。其中 RegularExpression 特性,基于正则表达式可以扩展实现很多常用的验证类型,下面的( 基于 DataAnnotations 的通用模型校验封装 )环节举例说明;关于该类更多详细信息请查看,https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations?view=net-7.0基于 DataAnnotations 的通用模型校验封装此处主要是使用了 Validator.TryValidateObject() 方法:Validator.TryValidateObject(object instance, ValidationContext validationContext, ICollection<ValidationResult>? validationResults, bool validateAllProperties);Validator 类提供如下校验方法:基于 DataAnnotations 的特性校验助手实现步骤错误成员对象类 ErrorMembernamespace Jeff.Common.Validatetion;
/// <summary>
/// 错误成员对象
/// </summary>
public class ErrorMember
{
/// <summary>
/// 错误信息
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// 错误成员名称
/// </summary>
public string? ErrorMemberName { get; set; }
}验证结果类 ValidResultnamespace Jeff.Common.Validatetion;
/// <summary>
/// 验证结果类
/// </summary>
public class ValidResult
{
public ValidResult()
{
ErrorMembers = new List<ErrorMember>();
}
/// <summary>
/// 错误成员列表
/// </summary>
public List<ErrorMember> ErrorMembers { get; set; }
/// <summary>
/// 验证结果
/// </summary>
public bool IsVaild { get; set; }
}定义操作正则表达式的公共类 RegexHelper(基于 RegularExpression 特性扩展)using System;
using System.Net;
using System.Text.RegularExpressions;
namespace Jeff.Common.Validatetion;
/// <summary>
/// 操作正则表达式的公共类
/// Regex 用法参考:https://learn.microsoft.com/zh-cn/dotnet/api/system.text.regularexpressions.regex.-ctor?redirectedfrom=MSDN&view=net-7.0
/// </summary>
public class RegexHelper
{
#region 常用正则验证模式字符串
public enum ValidateType
{
Email, // 邮箱
TelePhoneNumber, // 固定电话(座机)
MobilePhoneNumber, // 移动电话
Age, // 年龄(1-120 之间有效)
Birthday, // 出生日期
Timespan, // 时间戳
IdentityCardNumber, // 身份证
IpV4, // IPv4 地址
IpV6, // IPV6 地址
Domain, // 域名
English, // 英文字母
Chinese, // 汉字
MacAddress, // MAC 地址
Url, // URL
}
private static readonly Dictionary<ValidateType, string> keyValuePairs = new Dictionary<ValidateType, string>
{
{ ValidateType.Email, _Email },
{ ValidateType.TelePhoneNumber,_TelephoneNumber },
{ ValidateType.MobilePhoneNumber,_MobilePhoneNumber },
{ ValidateType.Age,_Age },
{ ValidateType.Birthday,_Birthday },
{ ValidateType.Timespan,_Timespan },
{ ValidateType.IdentityCardNumber,_IdentityCardNumber },
{ ValidateType.IpV4,_IpV4 },
{ ValidateType.IpV6,_IpV6 },
{ ValidateType.Domain,_Domain },
{ ValidateType.English,_English },
{ ValidateType.Chinese,_Chinese },
{ ValidateType.MacAddress,_MacAddress },
{ ValidateType.Url,_Url },
};
public const string _Email = @"^(\w)+(\.\w)*@(\w)+((\.\w+)+)$"; // ^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$ , [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}
public const string _TelephoneNumber = @"(d+-)?(d{4}-?d{7}|d{3}-?d{8}|^d{7,8})(-d+)?"; //座机号码(中国大陆)
public const string _MobilePhoneNumber = @"^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$"; //移动电话
public const string _Age = @"^(?:[1-9][0-9]?|1[01][0-9]|120)$"; // 年龄 1-120 之间有效
public const string _Birthday = @"^((?:19[2-9]\d{1})|(?:20(?:(?:0[0-9])|(?:1[0-8]))))((?:0?[1-9])|(?:1[0-2]))((?:0?[1-9])|(?:[1-2][0-9])|30|31)$";
public const string _Timespan = @"^15|16|17\d{8,11}$"; // 目前时间戳是15开头,以后16、17等开头,长度 10 位是秒级时间戳的正则,13 位时间戳是到毫秒级的。
public const string _IdentityCardNumber = @"^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$|^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$";
public const string _IpV4 = @"^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$";
public const string _IpV6 = @"^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$";
public const string _Domain = @"^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?$";
public const string _English = @"^[A-Za-z]+$";
public const string _Chinese = @"^[\u4e00-\u9fa5]{0,}$";
public const string _MacAddress = @"^([0-9A-F]{2})(-[0-9A-F]{2}){5}$";
public const string _Url = @"^[a-zA-z]+://(\w+(-\w+)*)(\.(\w+(-\w+)*))*(\?\S*)?$";
#endregion
/// <summary>
/// 获取验证模式字符串
/// </summary>
/// <param name="validateType"></param>
/// <returns></returns>
public static (bool hasPattern, string pattern) GetValidatePattern(ValidateType validateType)
{
bool hasPattern = keyValuePairs.TryGetValue(validateType, out string? pattern);
return (hasPattern, pattern ?? string.Empty);
}
#region 验证输入字符串是否与模式字符串匹配
/// <summary>
/// 验证输入字符串是否与模式字符串匹配
/// </summary>
/// <param name="input">输入的字符串</param>
/// <param name="validateType">模式字符串类型</param>
/// <param name="matchTimeout">超时间隔</param>
/// <param name="options">筛选条件</param>
/// <returns></returns>
public static (bool isMatch, string info) IsMatch(string input, ValidateType validateType, TimeSpan matchTimeout, RegexOptions options = RegexOptions.None)
{
var (hasPattern, pattern) = GetValidatePattern(validateType);
if (hasPattern && !string.IsNullOrWhiteSpace(pattern))
{
bool isMatch = IsMatch(input, pattern, matchTimeout, options);
if (isMatch) return (true, "Format validation passed."); // 格式验证通过。
else return (false, "Format validation failed."); // 格式验证未通过。
}
return (false, "Unknown ValidatePattern."); // 未知验证模式
}
/// <summary>
/// 验证输入字符串是否与模式字符串匹配,匹配返回true
/// </summary>
/// <param name="input">输入字符串</param>
/// <param name="pattern">模式字符串</param>
/// <returns></returns>
public static bool IsMatch(string input, string pattern)
{
return IsMatch(input, pattern, TimeSpan.Zero, RegexOptions.IgnoreCase);
}
/// <summary>
/// 验证输入字符串是否与模式字符串匹配,匹配返回true
/// </summary>
/// <param name="input">输入的字符串</param>
/// <param name="pattern">模式字符串</param>
/// <param name="matchTimeout">超时间隔</param>
/// <param name="options">筛选条件</param>
/// <returns></returns>
public static bool IsMatch(string input, string pattern, TimeSpan matchTimeout, RegexOptions options = RegexOptions.None)
{
return Regex.IsMatch(input, pattern, options, matchTimeout);
}
#endregion
}定义验证结果统一模型格式类 ResponseInfo(此类通常也是通用的数据响应模型类)namespace Jeff.Common.Model;
public sealed class ResponseInfo<T> where T : class
{
/*
Microsoft.AspNetCore.Http.StatusCodes
System.Net.HttpStatusCode
*/
/// <summary>
/// 响应代码(自定义)
/// </summary>
public int Code { get; set; }
/// <summary>
/// 接口状态
/// </summary>
public bool Success { get; set; }
#region 此处可以考虑多语言国际化设计(语言提示代号对照表)
/// <summary>
/// 语言对照码,参考:https://blog.csdn.net/shenenhua/article/details/79150053
/// </summary>
public string Lang { get; set; } = "zh-cn";
/// <summary>
/// 提示信息
/// </summary>
public string Message { get; set; } = string.Empty;
#endregion
/// <summary>
/// 数据体
/// </summary>
public T? Data { get; set; }
}实现验证助手类 ValidatetionHelper,配合 System.ComponentModel.DataAnnotations 类使用// 数据注解,https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations?view=net-7.0
using System.ComponentModel.DataAnnotations;
using Jeff.Common.Model;
namespace Jeff.Common.Validatetion;
/// <summary>
/// 验证助手类
/// </summary>
public sealed class ValidatetionHelper
{
/// <summary>
/// DTO 模型校验
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static ValidResult IsValid(object value)
{
var result = new ValidResult();
try
{
var validationContext = new ValidationContext(value);
var results = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(value, validationContext, results, true);
result.IsVaild = isValid;
if (!isValid)
{
foreach (ValidationResult? item in results)
{
result.ErrorMembers.Add(new ErrorMember()
{
ErrorMessage = item.ErrorMessage,
ErrorMemberName = item.MemberNames.FirstOrDefault()
});
}
}
}
catch (ValidationException ex)
{
result.IsVaild = false;
result.ErrorMembers = new List<ErrorMember>
{
new ErrorMember()
{
ErrorMessage = ex.Message,
ErrorMemberName = "Internal error"
}
};
}
return result;
}
/// <summary>
/// DTO 模型校验统一响应信息
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="model"></param>
/// <returns></returns>
public static ResponseInfo<ValidResult> GetValidInfo<T>(T model) where T : class
{
var result = new ResponseInfo<ValidResult>();
var validResult = IsValid(model);
if (!validResult.IsVaild)
{
result.Code = 420;
result.Message = "DTO 模型参数值异常";
result.Success = false;
result.Data = validResult;
}
else
{
result.Code = 200;
result.Success = true;
result.Message = "DTO 模型参数值合法";
}
return result;
}
}如何使用 DataAnnotations 封装的特性校验助手?首先定义一个数据模型类(DTO),添加校验特性 ValidationAttributeusing System.ComponentModel.DataAnnotations;
using Jeff.Common.Validatetion;
namespace Jeff.Comm.Test;
public class Person
{
[Display(Name = "姓名"), Required(ErrorMessage = "{0}必须填写")]
public string Name { get; set; }
[Display(Name = "邮箱")]
[Required(ErrorMessage = "{0}必须填写")]
[RegularExpression(RegexHelper._Email, ErrorMessage = "RegularExpression: {0}格式非法")]
[EmailAddress(ErrorMessage = "EmailAddress: {0}格式非法")]
public string Email { get; set; }
[Display(Name = "Age年龄")]
[Required(ErrorMessage = "{0}必须填写")]
[Range(1, 120, ErrorMessage = "超出范围")]
[RegularExpression(RegexHelper._Age, ErrorMessage = "{0}超出合理范围")]
public int Age { get; set; }
[Display(Name = "Birthday出生日期")]
[Required(ErrorMessage = "{0}必须填写")]
[RegularExpression(RegexHelper._Timespan, ErrorMessage = "{0}超出合理范围")]
public TimeSpan Birthday { get; set; }
[Display(Name = "Address住址")]
[Required(ErrorMessage = "{0}必须填写")]
[StringLength(200, MinimumLength = 10, ErrorMessage = "{0}输入长度不正确")]
public string Address { get; set; }
[Display(Name = "Mobile手机号码")]
[Required(ErrorMessage = "{0}必须填写")]
[RegularExpression(RegexHelper._MobilePhoneNumber, ErrorMessage = "{0}格式非法")]
public string Mobile { get; set; }
[Display(Name = "Salary薪水")]
[Required(ErrorMessage = "{0}必须填写")]
[Range(typeof(decimal), "1000.00", "3000.99")]
public decimal Salary { get; set; }
[Display(Name = "MyUrl连接")]
[Required(ErrorMessage = "{0}必须填写")]
[Url(ErrorMessage = "Url:{0}格式非法")]
[RegularExpression(RegexHelper._Url, ErrorMessage = "RegularExpression:{0}格式非法")]
public string MyUrl { get; set; }
}控制台调用通用校验助手验证方法 ValidatetionHelper.IsValid() 或 ValidatetionHelper.GetValidInfo()// 通用模型数据验证测试
static void ValidatetionTest()
{
var p = new Person
{
Name = "",
Age = -10,
Email = "www.baidu.com",
MobilePhoneNumber = "12345",
Salary = 4000,
MyUrl = "aaa"
};
// 调用通用模型校验
var result = ValidatetionHelper.IsValid(p);
if (!result.IsVaild)
{
foreach (ErrorMember errorMember in result.ErrorMembers)
{
// 控制台打印字段验证信息
Console.WriteLine($"{errorMember.ErrorMemberName}:{errorMember.ErrorMessage}");
}
}
Console.WriteLine();
// 调用通用模型校验,返回统一数据格式
var validInfo = ValidatetionHelper.GetValidInfo(p);
var options = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 设置中文编码乱码
WriteIndented = false
};
string jsonStr = JsonSerializer.Serialize(validInfo, options);
Console.WriteLine($"校验结果返回统一数据格式:{jsonStr}");
}在控制台Program.Main 方法中调用 ValidatetionTest() 方法:internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
{
#region 数据注解(DataAnnotations)模型验证
ValidatetionTest();
#endregion
}
Console.ReadKey();
}启动控制台,输出如下信息:如何实现自定义的验证特性?当我们碰到这些参数需要验证的时候,而上面内置类提供的特性不能满足需求时,此时我们可以实现自定义的验证特性来满足校验需求,按照微软给出的编码规则,我们只需继承 ValidationAttribute 类,并重写 IsValid() 方法即可。自定义校验特性案例比如实现一个密码强度的验证,实现步骤如下:定义密码强度规则,只包含英文字母、数字和特殊字符的组合,并且组合长度至少 8 位数;/// <summary>
/// 只包含英文字母、数字和特殊字符的组合
/// </summary>
/// <returns></returns>
public static bool IsCombinationOfEnglishNumberSymbol(string input, int? minLength = null, int? maxLength = null)
{
var pattern = @"(?=.*\d)(?=.*[a-zA-Z])(?=.*[^a-zA-Z\d]).";
if (minLength is null && maxLength is null)
pattern = $@"^{pattern}+$";
else if (minLength is not null && maxLength is null)
pattern = $@"^{pattern}{{{minLength},}}$";
else if (minLength is null && maxLength is not null)
pattern = $@"^{pattern}{{1,{maxLength}}}$";
else
pattern = $@"^{pattern}{{{minLength},{maxLength}}}$";
return Regex.IsMatch(input, pattern);
}实现自定义特性 EnglishNumberSymbolCombinationAttribute,继承自 ValidationAttribute;using System.ComponentModel.DataAnnotations;
namespace Jeff.Common.Validatetion.CustomAttributes;
/// <summary>
/// 是否是英文字母、数字和特殊字符的组合
/// </summary>
public class EnglishNumberSymbolCombinationAttribute : ValidationAttribute
{
/// <summary>
/// 默认的错误提示信息
/// </summary>
private const string error = "无效的英文字母、数字和特殊字符的组合";
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is null) return new ValidationResult("参数值为 null");
//if (value is null)
//{
// throw new ArgumentNullException(nameof(attribute));
//}
// 验证参数逻辑 value 是需要验证的值,而 validationContext 中包含了验证相关的上下文信息,这里可自己封装一个验证格式的 FormatValidation 类
if (FormatValidation.IsCombinationOfEnglishNumberSymbol(value as string, 8))
//验证成功返回 success
return ValidationResult.Success;
//不成功 提示验证错误的信息
else return new ValidationResult(ErrorMessage ?? error);
}
}以上就实现了一个自定义规则的 自定义验证特性,使用方式很简单,可以把它附属在我们 请求的参数 上或者 DTO 里的属性,也可以是 Action 上的形参,如下所示:public class CreateDTO
{
[Required]
public string StoreName { get; init; }
[Required]
// 附属在 DTO 里的属性
[EnglishNumberSymbolCombination(ErrorMessage = "UserId 必须是英文字母、数字和特殊符号的组合")]
public string UserId { get; init; }
}
...
// 附属在 Action 上的形参
[HttpGet]
public async ValueTask<ActionResult> Delete([EnglishNumberSymbolCombination]string userId, string storeName)该自定义验证特性还可以结合 DataAnnotations 内置的 [Compare] 特性,可以实现账号注册的密码确认验证(输入密码和确认密码是否一致性)。关于更多自定义参数校验特性,感兴趣的小伙伴可参照上面案例的实现思路,自行扩展实现哟。总结对于模型参数的校验,在实际项目系统中是非常有必要性的(通常在数据源头提供验证),利用 .NET 内置的 DataAnnotations(数据注解)提供的特性校验,可以很方便的实现通用的模型校验助手,关于其他特性的用法,请自行参考微软官方文档,这里注意下RegularExpressionAttribute(指定 ASP.NET 动态数据中的数据字段值必须与指定的正则表达式匹配),该特性可以方便的接入正则匹配验证,当遇到复杂的参数校验时,可以快速方便的扩展自定义校验特性,从此告别传统编码中各种 if(xxx != yyyy) 判断的验证,让整体代码编写更佳简练干净。
IoT小程序在展示中央空调采集数据和实时运行状态上的应用
利用前端语言实现跨平台应用开发似乎是大势所趋,跨平台并不是一个新的概念,“一次编译、到处运行”是老牌服务端跨平台语言Java的一个基本特性。随着时代的发展,无论是后端开发语言还是前端开发语言,一切都在朝着减少工作量,降低工作成本的方向发展。 与后端开发语言不同,利用前端语言实现跨平台有先天的优势,比如后端语言Java跨平台需要将源代码编译为class字节码文件后,再放进 Java 虚拟机运行;而前端语言JavaScript是直接将源代码放进JavaScript解释器运行。这就使得以JavaScript为跨平台语言开发的应用,可移植性非常强大。 目前跨平台技术按照解决方案分类,主要分为 Web 跨平台、容器跨平台、小程序跨平台。这里,我们主要以小程序跨端为例,测试对比IoT小程序和其他小程序在开发和应用上的优缺点。说到小程序,大家肯定想到微信小程序,实际在各大互联网公司:支付宝、百度、头条等等都有自己的小程序,小程序跨平台和Web跨平台十分类似,都是基于前端语言实现,小程序跨平台的优势在于可以调用系统底层能力,例如:蓝牙、相机等,性能方面也优于Web跨平台。 IoT小程序和大多数小程序一样,它是一套跨平台应用显示框架,它利用JS语言低门槛和API标准化大幅度降低了IoT应用的研发难度,其官方框架介绍如下: IoT小程序在前端框架能力、应用框架能力、图形框架能力都进行了适配和优化。那么接下来,我们按照其官方步骤搭建开发环境,然后结合中央空调数据采集和状态显示的实际应用场景开发物联网小程序应用。一、IoT小程序开发环境搭建 IoT小程序开发环境搭建一共分为四步,对于前端开发来说,安装NodeJS、配置cnpm、安装VSCode都是轻车熟路,不需要细讲,唯一不同的是按照官方说明安装IoT小程序的模拟器和VSCode开发插件HaaS UI,前期开发环境准备完毕,运行Demo查看一下效果,然后就可以进行IoT小程序应用开发了。搭建开发环境,安装HaaS UI插件和运行新建项目,出现一下界面说明开发环境搭建成功,就可以进行IoT小程序开发了:二、开发展示中央空调采集数据和运行状态的IoT小程序应用应用场景 中央空调的维保单位会对中央空调进行定期维护保养,定期的维护保养可排出故障隐患,减少事故发生,降低运行费用,延长设备的使用寿命,同时保障正常的工作时序。除了定期的维护保养外,还需要实时监测中央空调的运行参数(温度、累计排污量、不锈钢_腐蚀率等)和运行状态,及时发现中央空调运行过程中某些参数低于或高于报警值的问题,以便及时定位诊断中央空调存在的问题,然后进行相应的维护保养操作。架构实现 中央空调的数据采集和展示是典型的物联网应用架构,在中央空调端部署采集终端,通过Modbus通信协议采集中央空调设备参数,然后再由采集终端通过MQTT消息发送的我们的云端服务器,云端服务器接收到MQTT消息后转发到消息队列Kafka中,由云服务器上的自定义服务应用订阅Kafka主题,再存储到我们时序数据库中。下图展示了物联网应用的整体架构和IoT小程序在物联网架构中的位置: IoT小程序框架作为跨平台应用显示框架,顾名思义,其在物联网应用中主要作为显示框架开发。在传统应用中,我们使用微信小程序实现采集数据和运行状态的展示。而IoT小程序支持部署在AliOS Things、Ubuntu、Linux、MacOS、Window等系统中,这就使得我们可以灵活的将IoT小程序部署到多种设备终端中运行。 下面将以阿里云ASP-80智显面板为例,把展示中央空调采集数据和运行状态的IoT小程序部署在阿里云ASP-80智显面板中。IoT小程序开发 我们将从IoT小程序提供的前端框架能力、应用框架能力、图形框架能力来规划相应的功能开发。 IoT小程序采用Vue.js(v2.6.12)开源框架,实现了W3C标准的标签和样式子集;定义了四个应用生命周期,分别是:onLaunch,onShow,onHide,onDestroy;定义了十四个前端基础组件,除了基础的CSS样式支持外,还提供了对Less的支持;Net网络请求通过框架内置的JSAPI实现。 为了快速熟悉IoT小程序框架的开发方式,我们将在VSCode中导入官方公版案例,并以公版案例为基础框架开发我们想要的功能。简单实现通过网络请求获取中央空调采集数据并展示:1、在VSCode编辑器中导入从IoT小程序官网下载的公版案例,下载地址。2、因为IoT小程序前端框架使用的是Vue.js框架,所以在新增页面时也是按照Vue.js框架的模式,将页面添加到pages目录。我们是空调项目的IoT小程序,所以这里在pages目录下新增air-conditioning目录用于存放空调IoT小程序相关前端代码。3、在app.json中配置新增的页面,修改pages项,增加"air-conditioning": "pages/air-conditioning/index.vue"。{
"pages": {
......
"air-conditioning": "pages/air-conditioning/index.vue",
......
},
"options": {
"style": {
"theme": "theme-dark"
}
}
}4、在air-conditioning目录下新增index.vue前端页面代码,用于展示空调的采集数据是否正常及历史曲线图。设计需要开发的界面如下,页面的元素有栅格布局、Tabs 标签页、Radio单选框、日期选择框、曲线图表等元素。5、首先是实现Tabs标签页,IoT小程序没有Tabs组件,只能自己设置多个Text组件自定义样式并添加click事件来实现。 <div class="tab-list">
<fl-icon name="back" class="nav-back" @click="onBack" />
<text
v-for="(item, index) in scenes"
:key="index"
:class="'tab-item' + (index === selectedIndex ? ' tab-item-selected' : '')"
@click="tabSelected(index)"
>{{ item }}</text
>
</div>
......
data() {
return {
scenes: ["设备概览", "实时数据", "数据统计", "状态统计"],
selectedIndex: 0
};
},
......6、添加采集数据显示列表,在其他小程序框架中,尤其是以Vue.js为基础框架的小程序框架,这里有成熟的组件,而IoT小程序也是需要自己来实现。<template>
<div class="scene-wrapper" v-if="current">
<div class="label-temperature-wrapper top-title">
<div class="label-temperature-wrapper left-text">
<text class="label-temperature">设备编码:</text>
<text class="label-temperature-unit">{{deviceNo}}</text>
</div>
<div class="label-temperature-wrapper right-text">
<text class="label-temperature">数据日期:</text>
<text class="label-temperature-unit">{{collectTime}}</text>
</div>
</div>
<div class="main-wrapper">
<div class="section">
<div class="demo-block icon-block">
<div class="icons-item" v-for="(value, key, index) in IconTypes" :key="index">
<div class="label-title-wrapper">
<text class="label-title left-text">{{paramName}}</text>
<text class="label-title-unit right-text" style="padding-right: 5px;">{{paramWarn}}</text>
</div>
<div class="label-zhibiao-wrapper">
<text class="label-zhibiao">当前值:</text>
<text class="label-zhibiao-unit">{{value}}</text>
</div>
<div class="label-zhibiao-wrapper" style="margin-bottom: 10px;">
<text class="label-zhibiao">目标值:</text>
<text class="label-zhibiao-unit">{{targetValue}}</text>
</div>
</div>
</div>
</div>
</div>
</div>
</template> 在开发过程中发现,IoT小程序对样式的支持不是很全面,本来想将组件放置在同一行,一般情况下,只需要使用标准CSS样式display: inline就可以实现,但这里没有效果只能通过Flexbox进行布局在同一行。在设置字体方面,本来想把采集数据显示的描述字段加粗,用于突出显示,但是使用CSS样式font-weight无效,无论是设置数值还是blod,没有一点加粗效果。7、界面实现之后,需要发送数据请求,来查询采集数据并显示在界面上。IoT小程序通过框架内置JSAPI的Net网络提供网络请求工具。目前从官方文档和代码中来看,官方框架只提供了http请求,没有提供物联网中常用的WebSocket和MQTT工具,估计需要自定义扩展系统JSAPI实现其他网络请求。 created() {
const http = $falcon.jsapi.http
http.request({
url: 'http://服务域名/device/iot/query/data/point',
data: {
'deviceNo': '97306000000000005',
'rangeType': 'mo',
'lastPoint': '1',
'beginDateTime': '2023-02-10+16:09:42',
'endDateTime': '2023-03-12+16:09:42'
},
header: {
'Accept': 'application/json;charset=UTF-8',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Type': 'application/json;charset=UTF-8',
'Authorization': '有效token'
}
}, (response) => {
console.log(response)
var obj = JSON.parse(response.result)
console.log(obj.success)
console.log(JSON.parse(obj.data))
});
}, 按照官方要求编写http请求,发现默认未开启https请求:Protocol "https" not supported or disabled in libcurl。切换为http请求,返回数据为乱码,设置Accept-Encoding和Accept为application/json;charset=UTF-8仍然无效,且返回数据为JSON字符串,需要自己手动使用JSON.parse()进行转换,对于习惯于应用成熟框架的人来说,十分不友好。想了解更多关于 $falcon.jsapi.http的相关配置和实现,但是官方文档只有寥寥几句,没有详细的说明如何使用和配置,以及http请求中遇到一些常见问题的解决方式。8、IoT小程序框架提供画布组件,原则上来讲可以实现常用的曲线图表功能,但是如果使用其基础能力从零开始开发一套图表系统,耗时又耗力,所以这里尝试引入常用的图表组件库ECharts,使用ECharts在IoT小程序上显示曲线图表。执行cnpm install echarts --save安装echarts组件cnpm install echarts --save新建echarts配置文件,按需引入// 加载echarts,注意引入文件的路径
import echarts from 'echarts/lib/echarts'
// 再引入你需要使用的图表类型,标题,提示信息等
import 'echarts/lib/chart/bar'
import 'echarts/lib/chart/pie'
import 'echarts/lib/component/legend'
import 'echarts/lib/component/title'
import 'echarts/lib/component/tooltip'
export default echarts新增echarts组件ChartDemo.vue<template>
<div ref="chartDemo" style="height:200px;" ></div>
</template>
<script>
import echarts from '@/utils/echarts-config.js'
const ChartDemo = {
name: 'ChartDemo',
data() {
return {
chart: null
}
},
watch: {
option: {
handler(newValue, oldValue) {
this.chart.setOption(newValue)
},
deep: true
}
},
mounted() {
this.chart = echarts.init(this.$refs.chartDemo)
},
methods: {
setOption(option) {
this.chart && this.chart.setOption(option)
},
throttle(func, wait, options) {
let time, context, args
let previous = 0
if (!options) options = {}
const later = function() {
previous = options.leading === false ? 0 : new Date().getTime()
time = null
func.apply(context, args)
if (!time) context = args = null
}
const throttled = function() {
const now = new Date().getTime()
if (!previous && options.leading === false) previous = now
const remaining = wait - (now - previous)
context = this
args = arguments
if (remaining <= 0 || remaining > wait) {
if (time) {
clearTimeout(time)
time = null
}
previous = now
func.apply(context, args)
if (!time) context = args = null
} else if (!time && options.trailing !== false) {
time = setTimeout(later, remaining)
}
}
return throttled
}
}
}
export default ChartDemo
</script>
在base-page.js中注册全局组件......
import ChartDemo from './components/ChartDemo.vue';
export class BasePage extends $falcon.Page {
constructor() {
super()
}
beforeVueInstantiate(Vue) {
......
Vue.component('ChartDemo', ChartDemo);
}
}新建空调采集数据展示页history-charts.vue,用于展示Echarts图表<template>
<div class="scene-wrapper" v-if="current">
<div class="brightness-wrap">
<ChartBlock ref="chart2"></ChartBlock>
</div>
</div>
</template>
<script>
let option2 = {
title: {
text: '中央空调状态图',
subtext: '运行状态占比',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
data: ['开机', '关机', '报警', '故障', '空闲']
},
series: [
{
name: '运行状态',
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
data: [
{ value: 335, name: '开机' },
{ value: 310, name: '关机' },
{ value: 234, name: '报警' },
{ value: 135, name: '故障' },
{ value: 1548, name: '空闲' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
export default {
props:{
current:{
type:Boolean,
default:false
}
},
data() {
return {
};
},
methods: {
},
mounted: function() {
this.$refs.chart2.setOption(option2)
}
};
</script>执行HaaS UI: Build-Debug ,显示打包成功执行HaaS UI: Simulator ,显示“当前HaaS UI: Simulator任务正在执行,请稍后再试” 本来想在模拟器上看一下Echarts显示效果,但是执行HaaS UI: Simulator时一直显示任务正在执行。然后以为是系统进程占用,但是重启、关闭进程等操作一系列操作下来,仍然显示此提示,最后将Echarts代码删除,恢复到没有Echarts的状态,又可以执行了。这里不清楚是否是IoT小程序不支持引入第三方图表组件,从官方文档中没有找到答案。后来又使用echarts的封装组件v-charts进行了尝试,结果依然不能展示。 如果不能使用第三方组件,那么只能使用IoT官方小程序提供的画布组件来自己实现图表功能,官方提供的画布曲线图示例。9、通过IoT小程序提供的组件分别实现显示中央空调采集数据的实时数据、数据统计、状态统计图表。-实现实时数据折线图<template>
<div class="scene-wrapper" v-show="current">
<div class="main-wrapper">
<div class="label-temperature-wrapper top-title">
<div class="label-temperature-wrapper left-text">
<text class="label-temperature">设备编码:</text>
<text class="label-temperature-unit">{{deviceNo}}</text>
</div>
<div class="label-temperature-wrapper right-text">
<text class="label-temperature">数据日期:</text>
<text class="label-temperature-unit">{{collectTime}}</text>
</div>
</div>
<canvas ref="c2" class="canvas" width="650" height="300"></canvas>
</div>
</div>
</template>
<script>
export default {
name: "canvas",
props: {},
data() {
return {
deviceNo: '97306000000000005',
collectTime: '2023-03-11 23:59:59'
};
},
mounted() {
this.c2();
},
methods: {
c2() {
let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c2) : this.$refs.c1.getContext("2d");
// Demo测试数据
let arr = [{key:'01:00',value:61.68},{key:'02:00',value:83.68},{key:'03:00',value:56.68},{key:'04:00',value:86.68},{key:'05:00',value:53.68},
{key:'06:00',value:41.68},{key:'07:00',value:33.68}];
this.drawStat(ctx, arr);
},
//该函数用来绘制折线图
drawStat(ctx, arr) {
//画布的款高
var cw = 700;
var ch = 300;
//内间距padding
var padding = 35;
//原点,bottomRight:X轴终点,topLeft:Y轴终点
var origin = {x:padding,y:ch-padding};
var bottomRight = {x:cw-padding,y:ch-padding};
var topLeft = {x:padding,y:padding};
ctx.strokeStyle='#FF9500';
ctx.fillStyle='#FF9500';
//绘制X轴
ctx.beginPath();
ctx.moveTo(origin.x,origin.y);
ctx.lineTo(bottomRight.x,bottomRight.y);
//绘制X轴箭头
ctx.lineTo(bottomRight.x-10,bottomRight.y-5);
ctx.moveTo(bottomRight.x,bottomRight.y);
ctx.lineTo(bottomRight.x-10,bottomRight.y+5);
//绘制Y轴
ctx.moveTo(origin.x,origin.y);
ctx.lineTo(topLeft.x,topLeft.y);
//绘制Y轴箭头
ctx.lineTo(topLeft.x-5,topLeft.y+10);
ctx.moveTo(topLeft.x,topLeft.y);
ctx.lineTo(topLeft.x+5,topLeft.y+10);
//设置字号
var color = '#FF9500';
ctx.fillStyle=color;
ctx.font = "13px scans-serif";//设置字体
//绘制X方向刻度
//计算刻度可使用的总宽度
var avgWidth = (cw - 2*padding - 50)/(arr.length-1);
for(var i=0;i<arr.length;i++){
//循环绘制所有刻度线
if(i > 0){
//移动刻度起点
ctx.moveTo(origin.x+i*avgWidth,origin.y);
//绘制到刻度终点
ctx.lineTo(origin.x+i*avgWidth,origin.y-10);
}
//X轴说明文字:1月,2月...
var txtWidth = 35;
ctx.fillText(
arr[i].key,
origin.x+i*avgWidth-txtWidth/2 + 10,
origin.y+20);
}
//绘制Y方向刻度
//最大刻度max
var max = 0;
for(var i=0;i<arr.length;i++){
if(arr[i].value>max){
max=arr[i].value;
}
}
console.log(max);
/*var max = Math.max.apply(this,arr);
console.log(max);*/
var avgValue=Math.floor(max/5);
var avgHeight = (ch-padding*2-50)/5;
for(var i=1;i<arr.length;i++){
//绘制Y轴刻度
ctx.moveTo(origin.x,origin.y-i*avgHeight);
ctx.lineTo(origin.x+10,origin.y-i*avgHeight);
//绘制Y轴文字
var txtWidth = 40;
ctx.fillText(avgValue*i,
origin.x-txtWidth-5,
origin.y-i*avgHeight+6);
}
//绘制折线
for(var i=0;i<arr.length;i++){
var posY = origin.y - Math.floor(arr[i].value/max*(ch-2*padding-50));
if(i==0){
ctx.moveTo(origin.x+i*avgWidth,posY);
}else{
ctx.lineTo(origin.x+i*avgWidth,posY);
}
//具体金额文字
ctx.fillText(arr[i].value,
origin.x+i*avgWidth,
posY
)
}
ctx.stroke();
//绘制折线上的小圆点
ctx.beginPath();
for(var i=0;i<arr.length;i++){
var posY = origin.y - Math.floor(arr[i].value/max*(ch-2*padding-50));
ctx.arc(origin.x+i*avgWidth,posY,4,0,Math.PI*2);//圆心,半径,画圆
ctx.closePath();
}
ctx.fill();
}
}
};
</script>-数据统计图表<template>
<div class="scene-wrapper" v-show="current">
<div class="main-wrapper">
<div class="label-temperature-wrapper top-title">
<div class="label-temperature-wrapper left-text">
<text class="label-temperature">设备编码:</text>
<text class="label-temperature-unit">{{deviceNo}}</text>
</div>
<div class="label-temperature-wrapper right-text">
<text class="label-temperature">数据日期:</text>
<text class="label-temperature-unit">{{collectTime}}</text>
</div>
</div>
<canvas ref="c1" class="canvas" width="650" height="300"></canvas>
</div>
</div>
</template>
<script>
export default {
name: "canvas",
props: {},
data() {
return {
deviceNo: '97306000000000005',
collectTime: '2023-03-13 20:23:36'
};
},
mounted() {
this.c1();
},
methods: {
c1() {
let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c1) : this.$refs.c1.getContext("2d");
this.draw(ctx);
},
draw(ctx){
var x0=30,//x轴0处坐标
y0=280,//y轴0处坐标
x1=700,//x轴顶处坐标
y1=30,//y轴顶处坐标
dis=30;
//先绘制X和Y轴
ctx.beginPath();
ctx.lineWidth=1;
ctx.strokeStyle='#FF9500';
ctx.fillStyle='#FF9500';
ctx.moveTo(x0,y1);//笔移动到Y轴的顶部
ctx.lineTo(x0,y0);//绘制Y轴
ctx.lineTo(x1,y0);//绘制X轴
ctx.stroke();
//绘制虚线和Y轴值
var yDis = y0-y1;
var n=1;
ctx.fillText(0,x0-20,y0);//x,y轴原点显示0
while(yDis>dis){
ctx.beginPath();
//每隔30划一个虚线
ctx.setLineDash([2,2]);//实线和空白的比例
ctx.moveTo(x1,y0-dis);
ctx.lineTo(x0,y0-dis);
ctx.fillText(dis,x0-20,y0-dis);
//每隔30划一个虚线
dis+=30;
ctx.stroke();
}
var xDis=30,//设定柱子之前的间距
width=40;//设定每个柱子的宽度
//绘制柱状和在顶部显示值
for(var i=0;i<12;i++){//假设有8个月
ctx.beginPath();
var color = '#' + Math.random().toString(16).substr(2, 6).toUpperCase();//随机颜色
ctx.fillStyle=color;
ctx.font = "13px scans-serif";//设置字体
var height = Math.round(Math.random()*220+20);//在一定范围内随机高度
var rectX=x0+(width+xDis)*i,//柱子的x位置
rectY=height;//柱子的y位置
ctx.color='#FF9500';
ctx.fillText((i+1)+'月份',rectX,y0+15);//绘制最下面的月份稳住
ctx.fillRect(rectX,y0, width, -height);//绘制一个柱状
ctx.fillText(rectY,rectX+10,280-rectY-5);//显示柱子的值
}
},
}
};
</script>-状态统计图表<template>
<div class="scene-wrapper" v-show="current">
<div class="main-wrapper">
<div class="label-temperature-wrapper top-title">
<div class="label-temperature-wrapper left-text">
<text class="label-temperature">设备编码:</text>
<text class="label-temperature-unit">{{deviceNo}}</text>
</div>
<div class="label-temperature-wrapper right-text">
<text class="label-temperature">数据日期:</text>
<text class="label-temperature-unit">{{collectTime}}</text>
</div>
</div>
<canvas ref="c3" class="canvas" width="600" height="300"></canvas>
</div>
</div>
</template>
<script>
export default {
name: "canvas",
props: {},
data() {
return {
deviceNo: '97306000000000005',
collectTime: '2023-03-13 20:29:36'
};
},
mounted() {
this.c3();
},
methods: {
c3() {
let ctx = typeof createCanvasContext === "function" ? createCanvasContext(this.$refs.c3) : this.$refs.c3.getContext("2d");
this.drawPie(ctx);
},
drawPie(pen){
// Demo测试数据
var deg = Math.PI / 180
var arr = [
{
name: "开机",
time: 8000,
color: '#7CFF00'
},
{
name: "关机",
time: 1580,
color: '#737F9C'
},
{
name: "空闲",
time: 5790,
color: '#0ECC9B'
},
{
name: "故障",
time: 4090,
color: '#893FCD'
},
{
name: "报警",
time: 2439,
color: '#EF4141'
},
];
//总价
pen.translate(30,-120);
arr.tatol = 0;
for (let i = 0; i < arr.length; i++) {
arr.tatol = arr.tatol + arr[i].time
}
var stardeg = 0
arr.forEach(el => {
pen.beginPath()
var r1 = 115
pen.fillStyle = el.color
pen.strokeStyle='#209AAD';
pen.font = "15px scans-serif";
//求出每个time的占比
var angle = (el.time / arr.tatol) * 360
//利用占比来画圆弧
pen.arc(300, 300, r1, stardeg * deg, (stardeg + angle) * deg)
//将圆弧与圆心相连接,形成扇形
pen.lineTo(300, 300)
var r2 = r1+10;
if(el.name === '关机' || el.name === '空闲')
{
r2 = r1+30
}
//给每个扇形添加数组的name
var y1 = 300 + Math.sin((stardeg + angle) * deg-angle*deg/2 ) *( r2)
var x1 = 300 + Math.cos((stardeg + angle) * deg-angle*deg/2 ) * (r2)
pen.fillText(`${el.name}`, x1, y1)
stardeg = stardeg + angle
pen.fill()
pen.stroke()
});
},
}
};
</script>三、将IoT小程序更新到ASP-80智显面板查看运行效果 将IoT小程序更新到ASP-80智显面板,在硬件设备上查看IoT应用运行效果。如果是使用PC端初次连接,那么需要安装相关驱动和配置,否则无法使用VSCode直接更新IoT小程序到ASP-80智显面板。1、如果使用Win10将IoT小程序包更新到ASP-80智显面板上,必须用到CH340串口驱动,第一次通过TypeC数据线连接设备,PC端设备管理器的端口处不显示端口,这时需要下载Windows版本的CH340串口驱动下载链接 。2、将下载的驱动文件CH341SER.ZIP解压并安装之后,再次查看PC端设备管理器端口就有了USB Serial CH340端口。3、使用SourceCRT连接ASP-80智显面板,按照官方文档说明,修改配置文件,连接好WiFi无线网,下一步通过VSCode直接更新IoT小程序到ASP-80智显面板上查看测试。4、所有准备工作就绪后,点击VSCode的上传按钮HaaS UI: Device,将应用打包并上传至ASP-80智显面板。在选择ip地址框的时候,输入我们上一步获取到的ip地址192.168.1.112,其他参数保持默认即可,上传成功后,VSCode控制台提示安装app成功。5、IoT小程序安装成功之后就可以在ASP-80智显面板上查看运行效果了。 综上所述,IoT小程序框架在跨系统平台(AliOS Things、Ubuntu、Linux、MacOS、Window等)方面提供了非常优秀的基础能力,应用的更新升级提供了多种方式,在实际业务开发过程中可以灵活选择。IoT小程序框架通过JSAPI提供了调用系统底层应用的能力,同时提供了自定义JSAPI扩展封装的方法,这样就足够业务开发通过自定义的方式满足特殊的业务需求。 虽然多家互联网公司都提供了小程序框架,但在128M 128M这样的低资源设备里输出,IoT小程序是比较领先的,它不需要另外下载APP作为小程序的容器,降低了资源的消耗,这一点是其他小程序框架所不能比拟的。 但是在前端框架方面,实用组件太少。其他小程序已发展多年,基于基础组件封装并开源的前端组件应用场景非常丰富,对于中小企业来说,习惯于使用成熟的开源组件,如果使用IoT小程序开发物联网应用可能需要耗费一定的人力物力。既然是基于Vue.js的框架,却没有提供引入其他优秀组件的文档说明和示例,不利于物联网应用的快速开发,希望官方能够完善文档,详细说明IoT小程序开发框架配置项,将来能够提供更多的实用组件。
我对组件化概念的理解
那什么是组件化呢?我们可以认为组件就是页面里的 UI 组件,一个页面可以由很多组件构成。例如一个后台管理系统页面,可能包含了 Header、Sidebar、Main 等各种组件。一个组件又包含了 template(html)、script、style 三部分,其中 script、style 可以由一个或多个模块组成。在传统的开发模式中,往往是把前端的网页代码和后端的程序代码混合在一起,借助某种模板技术(如 JSP、ASP、PHP)来在服务器端动态生成 HTML 页面。在这种开发模式下,网页的每次改动都需要前后端人员共同参与才能完成,网站前后端的开发人员需要很大的沟通成本、协调成本。企业招人的时候,也不得不招一些既懂前端又懂后端程序员,来减少前后端开发人员的冲突。前端为什么需要组件化开发呢组件化的目的就是为了让页面中的各个部分可以被复用来减少重复的代码。另一方面,也可以更好地使团队分工协作,让不同的人负责编写不同的组件。页面上的每个独立的、可视/可交互区域视为一个组件;每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护;可以参考京东商城案例,京东把页面个各个部分来规划处组件得益于技术的发展,目前三大框架在构建工具(例如 webpack、vite...)的配合下都可以很好的实现组件化。例如 Vue,使用 _.vue 文件就可以把 template、script、style 写在一起,一个 _.vue 文件就是一个组件。<template>
<div>
{{ msg }}
</div>
</template>
<script>
export default {
data() {
return {
msg: '组件化开发',
};
},
};
</script>
<style>
body {
color: #fff;
}
</style>组件化的理念:页面上独立的可视/可交互区域视为一个组件每个组件作为一个目录,所需资源就近维护,在统一工程目录开发维护组件之间可以只有组合替换组件的可复用性到这里,我们就对前端组件化开发有了一个比较清晰的认识。前端技术的发展,就是从网页制作变成了网页应用的开发,网页不再是一个只用来展示的页面,而是一个应用程序。组件化开发是为了提高代码的可复用性,以及方便团队分工协作开发。
web页面实现全背景视频功能方案:使用bideo.js来处理object-fit在ie浏览器下不兼容问题
object-fit 兼容问题【视频资源】:https://raw.githubusercontent.com/rishabhp/bideo.js/master/night.mp4先看下面例子:<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>全屏HTML5网页背景视频</title>
<style>
html,
body {
margin: 0;
padding: 0;
}
#container {
overflow: hidden;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
text-align: center;
}
#main {
display: inline-block;
position: relative;
top: 50%;
transform: translateY(-50%);
color: #fff;
}
#video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -1;
object-fit: fill;
}
</style>
</head>
<body>
<div id="container">
<!-- 内容 -->
<section id="main">
<div id="head">
<h1>Bideo.js</h1>
<p class="sub_head">A JS library that makes it super easy to add fullscreen background videos.</p>
</div>
</section>
<!-- 视频背景 -->
<video id="video" src="/night.mp4" autoplay loop muted></video>
</div>
</body>
</html>
使用 360 极速浏览器的极速模式效果如下:使用 360 极速浏览器的 ie 兼容模式效果如下:原因是我们在 css 代码里面使用了 object-fit: fill;,而 object-fit 在 ie 模式下是不兼容的。https://caniuse.com/?search=object-fitbideo.js为了解决上面 object-fit 在 ie 模式下不兼容的问题,这里我们使用 bideo.js,它能实现全屏HTML5网页背景视频。GitHub地址:https://github.com/rishabhp/bideo.jsFullscreen HTML5 Background Video for the Web./**
* Full Background Video
*
* More info on Audio/Video Media Events/Attributes/Methods
* - https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events
* - http://www.w3schools.com/tags/ref_av_dom.asp
*/
(function (global) {
// Define Bideo constructor on the global object
global.Bideo = function () {
// Plugin options
this.opt = null;
// The Video element
this.videoEl = null;
// Approximate Loading Rate
//
// The value will be a number like 0.8
// which means to load 4 seconds of the video
// it takes 5 seconds. If the number is super low
// like 0.2 (regular 3g connections) then you can
// decide whether to play the video or not.
// This behaviour will be controller with
// the `acceptableLoadingRate` option.
this.approxLoadingRate = null;
// Methods to which `this` will be bound
this._resize = null;
this._progress = null;
// Time at which video is initialized
this.startTime = null;
this.onLoadCalled = false;
// Initialize and setup the video in DOM`
this.init = function (opt) {
// If not set then set to an empty object
this.opt = opt = opt || {};
var self = this;
self._resize = self.resize.bind(this);
// Video element
self.videoEl = opt.videoEl;
// Meta data event
self.videoEl.addEventListener('loadedmetadata', self._resize, false);
// Fired when enough has been buffered to begin the video
// self.videoEl.readyState === 4 (HAVE_ENOUGH_DATA)
self.videoEl.addEventListener('canplay', function () {
// Play the video when enough has been buffered
if (!self.opt.isMobile) {
self.opt.onLoad && self.opt.onLoad();
if (self.opt.autoplay !== false) self.videoEl.play();
}
});
// If resizing is required (resize video as window/container resizes)
if (self.opt.resize) {
global.addEventListener('resize', self._resize, false);
}
// Start time of video initialization
this.startTime = (new Date()).getTime();
// Create `source` for video
this.opt.src.forEach(function (srcOb, i, arr) {
var key
, val
, source = document.createElement('source');
// Set all the attribute key=val supplied in `src` option
for (key in srcOb) {
if (srcOb.hasOwnProperty(key)) {
val = srcOb[key];
source.setAttribute(key, val);
}
}
self.videoEl.appendChild(source);
});
if (self.opt.isMobile) {
if (self.opt.playButton) {
self.opt.videoEl.addEventListener('timeupdate', function () {
if (!self.onLoadCalled) {
self.opt.onLoad && self.opt.onLoad();
self.onLoadCalled = true;
}
});
self.opt.playButton.addEventListener('click', function () {
self.opt.pauseButton.style.display = 'inline-block';
this.style.display = 'none';
self.videoEl.play();
}, false);
self.opt.pauseButton.addEventListener('click', function () {
this.style.display = 'none';
self.opt.playButton.style.display = 'inline-block';
self.videoEl.pause();
}, false);
}
}
return;
}
// Called once video metadata is available
//
// Also called when window/container is resized
this.resize = function () {
// IE/Edge still don't support object-fit: cover
if ('object-fit' in document.body.style) return;
// Video's intrinsic dimensions
var w = this.videoEl.videoWidth
, h = this.videoEl.videoHeight;
// Intrinsic ratio
// Will be more than 1 if W > H and less if H > W
var videoRatio = (w / h).toFixed(2);
// Get the container DOM element and its styles
//
// Also calculate the min dimensions required (this will be
// the container dimentions)
var container = this.opt.container
, containerStyles = global.getComputedStyle(container)
, minW = parseInt( containerStyles.getPropertyValue('width') )
, minH = parseInt( containerStyles.getPropertyValue('height') );
// If !border-box then add paddings to width and height
if (containerStyles.getPropertyValue('box-sizing') !== 'border-box') {
var paddingTop = containerStyles.getPropertyValue('padding-top')
, paddingBottom = containerStyles.getPropertyValue('padding-bottom')
, paddingLeft = containerStyles.getPropertyValue('padding-left')
, paddingRight = containerStyles.getPropertyValue('padding-right');
paddingTop = parseInt(paddingTop);
paddingBottom = parseInt(paddingBottom);
paddingLeft = parseInt(paddingLeft);
paddingRight = parseInt(paddingRight);
minW += paddingLeft + paddingRight;
minH += paddingTop + paddingBottom;
}
// What's the min:intrinsic dimensions
//
// The idea is to get which of the container dimension
// has a higher value when compared with the equivalents
// of the video. Imagine a 1200x700 container and
// 1000x500 video. Then in order to find the right balance
// and do minimum scaling, we have to find the dimension
// with higher ratio.
//
// Ex: 1200/1000 = 1.2 and 700/500 = 1.4 - So it is best to
// scale 500 to 700 and then calculate what should be the
// right width. If we scale 1000 to 1200 then the height
// will become 600 proportionately.
var widthRatio = minW / w;
var heightRatio = minH / h;
// Whichever ratio is more, the scaling
// has to be done over that dimension
if (widthRatio > heightRatio) {
var new_width = minW;
var new_height = Math.ceil( new_width / videoRatio );
}
else {
var new_height = minH;
var new_width = Math.ceil( new_height * videoRatio );
}
this.videoEl.style.width = new_width + 'px';
this.videoEl.style.height = new_height + 'px';
};
};
}(window));
使用bideo.js实现全背景视频<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>全屏HTML5网页背景视频</title>
<style>
html,
body {
margin: 0;
padding: 0;
}
#container {
overflow: hidden;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
text-align: center;
}
#main {
display: inline-block;
position: relative;
top: 50%;
transform: translateY(-50%);
color: #fff;
}
#video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -1;
object-fit: fill;
}
</style>
<script src="./bideo.js"></script>
</head>
<body>
<div id="container">
<!-- 内容 -->
<section id="main">
<div id="head">
<h1>Bideo.js</h1>
<p class="sub_head">A JS library that makes it super easy to add fullscreen background videos.</p>
</div>
</section>
<!-- 视频背景 -->
<video id="video" src="/night.mp4" autoplay loop muted></video>
</div>
<script>
window.onload = function() {
(function () {
let bv = new Bideo();
bv.init({
// Video element
videoEl: document.querySelector("#video"),
// Container element
container: document.querySelector("body"),
// Resize
resize: true,
// Array of objects containing the src and type
// of different video formats to add
src: [],
// What to do once video loads (initial frame)
onLoad: function () {}
});
})();
}
</script>
</body>
</html>
用 bideo.js 之后,360 极速浏览器的 ie 兼容模式效果如下:
一次性弄清楚 Authentication & Authorization 以及 Cookie、Session、Token
认证和授权的区别是什么?不出意外情况,我想这是一个绝大多数人都会混淆的问题。首先先从读音上来认识这两个名词,很多人都会把它俩的读音搞混,所以我建议你先先去查一查这两个单词到底该怎么读,他们的具体含义是什么。说简单点就是:认证 (Authentication): 你是谁?Who are you?授权 (Authorization): 你有权限干什么?What do you have access to do?稍微正式点(啰嗦点)的说法就是:Authentication(认证) 是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。Authorization(授权) 发生在 Authentication(认证)之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。举个简单的例子:比如工作牌就是 Authentication(认证)你是谋谋公司,机构或组织的身份标识和凭据,根据这个凭据即可进入该单位区域工作,比如刷卡进出大门;而你个人的工作岗位和职责就类似Authorization(授权),单位授权该岗位能干些什么范围的工作;重新认识 Cookie什么是 Cookie,以及它的作用是什么?Cookie 有时也用其复数形式 Cookies,存储类型为“小型文本文件”,是某些网站为了辨别用户身份,进行 Session 跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息。Cookie 特点:存储结构:以 Key-Value 形式存储的“小型文本文件”;存储位置:存储在用户终端的某个目录下;存储大小:≤ 4KB;时效性:可暂时或永久性保存,用户终端自行决定;Cookie是一段不超过 4KB 的小型文本数据,由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。属性说明Name/Value设置 Cookie 的名称及相对应的值,对于认证 Cookie,Value 值包括 Web 服务器所提供的访问令牌。Expires设置 Cookie 的生存期。 有两种存储类型的 Cookie:会话性与持久性。Expires 属性缺省时,为会话性 Cookie,仅保存在客户端内存中,并在用户关闭浏览器时失效;持久性 Cookie 会保存在用户的硬盘中,直至生存期到或用户直接在网页中单击“注销”等按钮结束会话时才会失效。Path定义了 Web 站点上可以访问该 Cookie 的目录。Domain指定了可以访问该 Cookie 的 Web 站点或域。Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie 受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 Domain 属性中设置 .org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围。Secure指定是否使用 HTTPS 安全协议发送 Cookie。使用 HTTPS 安全协议,可以保护 Cookie 在浏览器和 Web 服务器间的传输过程中不被窃取和篡改。该方法也可用于 Web 站点的身份鉴别,即在 HTTPS 的连接建立阶段,浏览器会检查 Web 网站的 SSL 证书的有效性。但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到 SSL 证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。由于许多用户缺乏安全意识,因而仍可能连接到 Pharming 攻击所伪造的网站。HTTPOnly用于防止客户端脚本通过 document.cookie 属性访问 Cookie ,有助于保护 Cookie 不被跨站脚本攻击窃取或篡改。但是,HTTPOnly 的应用仍存在局限性,一些浏览器可以阻止客户端脚本对 Cookie 的读操作,但允许写操作;此外大多数浏览器仍允许通过 XMLHTTP 对象读取 HTTP 响应中的 Set-Cookie 头。如何使用 Cookie ?HTTP 协议中的 Cookie 包括 Web Cookie 和 浏览器 Cookie,它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。HTTP Cookie 机制是 HTTP 协议无状态的一种补充和改良。Cookie 主要用于下面三个目的:会话管理:登陆、购物车、游戏得分或者服务器应该记住的其他内容;个性化:用户偏好、主题或者其他设置;Session 追踪:记录和分析用户行为;Cookie 应用:创建 Cookie:当接收到客户端发出的 HTTP 请求时,服务器可以发送带有响应的标头,Cookie 通常由浏览器存储,然后将 Cookie 与 HTTP 标头一同向服务器发出请求。Set-Cookie 和 Cookie 标头:Set-Cookie 标头告诉客户端存储 Cookie,响应标头将 cookie 从服务器发送到用户代理;客户端请求则使用 Cookie 头将存储的 Cookie 发送回服务器;下面是 Cookie 的一些应用案例:我们在 Cookie 中保存已经登录过的用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了。除此之外,Cookie 还能保存用户首选项,主题和其他设置信息。使用 Cookie 保存 Session 或者 Token ,向后端发送请求的时候带上 Cookie,这样后端就能取到 Session 或者 token 了。这样就能记录用户当前的状态了,因为 HTTP 协议是无状态的。Cookie 还可以用来记录和分析用户行为。举个简单的例子你在网上购物的时候,因为 HTTP 协议是没有状态的,如果服务器想要获取你在某个页面的停留状态或者看了哪些商品,一种常用的实现方式就是将这些信息存放在Cookie。什么是 Session ?Session 简介在计算机中,尤其是在网络应用中,通常称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。Session 的主要作用就是通过服务端记录用户的状态。用于保持状态的基于 Web 服务器的方法。Session 允许通过将对象存储在 Web 服务器的内存中在整个用户会话过程中保持任何对象。Session 的一些应用案例:存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在 Session 对象中。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。Cookie 和 Session 有什么区别?由于 HTTP 协议无状态的缺陷。WEB 的设计者们提出了 Cookie 和 Session 两种解决机制。通过对两者的比较分析,指出了它们的区别与联系。——CookieSession效期暂时和永久常规暂时(也可实现永久)存储用户终端/客户端浏览器服务器(Session + SessionID)结构key-valuekey-value优点1.极高的扩展性和可用性。 2.通过良好的编程,控制保存在cookie中的session对象的大小。 3.通过加密和安全传输技术(SSL),减少cookie被破解的可能性。 4.只在cookie中存放不敏感数据,即使被盗也不会有重大损失。 5.控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。 1.如果要在诸多Web页间传递一个变量,那么用Session变量要比通过QueryString传递变量可使问题简化。 2.要使WEb站点具有用户化,可以考虑使用Session变量。你的站点的每位访问者都有用户化的经验,基于此,随着LDAP和诸如MS Site 3.Server等的使用,已不必再将所有用户化过程置入Session变量了,而这个用户化是取决于用户喜好的。 4.你可以在任何想要使用的时候直接使用session变量,而不必事先声明它,这种方式接近于在VB中变量的使用。使用完毕后,也不必考虑将其释放,因为它将自动释放。 5.Session实例是轻量级的,所谓轻量级:是指他的创建和删除不需要消耗太多资源; 功能缺陷1.Cookie数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。 2.安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。 3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。 1.进程依赖性,ASP Session状态存于IIS的进程中,也就是inetinfo.exe这个程序。所以当inetinfo.exe进程崩溃时,这些信息也就丢失。另外,重起或者关闭IIS服务都会造成信息的丢失。 2.CORS(跨域资源共享):Session状态使用范围的局限性,当一个用户从一个网站访问到另外一个网站时,这些Session信息并不会随之迁移过去。 3.存在Cookie的依赖性,实际上客户端的Session信息是存储在Cookie中的,如果客户端完全禁用掉了Cookie功能,也就不能享受到了Session提供的功能了。 4.每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加; 5.不是线程安全的,应该避免多个线程共享同一个Session实例。 安全 安全性相对较低; 1.Cookie捕获/重放; 2.恶意 Cookies; 3.会话定置(Session Fixation)攻击; 4.跨站请求伪造(Cross-Site Request Forgery,简称CSRF)攻击; 安全性相对较高; 1.web服务器防护; 2.主机安全防护; 3.跨站请求伪造(Cross-Site Request Forgery,简称CSRF)攻击; 因为创建 Session 变量有很大的随意性,可随时调用,不需要开发者做精确地处理,所以过度使用 Session 变量将会导致代码可读性降低,使项目维护困难。理解 SessionID 的本质客户端使用 Cookie 保存 SessionID客户端用 Cookie 保存了 SessionID,当我们请求服务器的时候,会把这个 SessionID 一起发给服务器,服务器会到内存中搜索对应的 SessionID,如果找到了对应的 SessionID,说明我们处于登录状态,有相应的权限;如果没有找到对应的 SessionID,这说明:要么是我们把浏览器关掉了(后面会说明为什么),要么 session 超时了(没有请求服务器超过 20 分钟),session 被服务器清除了,则服务器会给你分配一个新的 SessionID。你得重 新登录并把这个新的 SessionID 保存在 Cookie 中。 在没有把浏览器关掉的时候(这个时候假如已经把 SessionID 保存在 Cookie 中了)这个 SessionID 会一直保存在浏览器中,每次请求的时候都会把这个 SessionID 提交到服务器,所以服务器认为我们是登录的;当然,如果太长时间没有请求服务器,服务器会认为我们已经所以把浏览器关掉了,这个时候服务器会把该 SessionID 从内存中清除掉,这个时候如果我们再去请求服务器,SessionID 已经不存在了,所以服务器并没有在内存中找到对应的 SessionID,所以会再产生一个新的 SessionID,这个时候一般我们又要再登录一次。客户端未使用 Cookie 保存 SessionID此时如果我们请求服务器,因为没有提交 SessionID 上来,服务器会认为你是一个全新的请求,服务器会给你分配一个新的 SessionID,这就是为什么我们每次打开一个新的浏览器的时候(无论之前我们有没有登录过)都会产生一个新的 SessionID(或者是会让我们重新登录)。当我们一旦把浏览器关掉后,再打开浏览器再请求该页面,它会让我们登录,这是为什么?我们明明已经登录了,而且还没有超时,SessionID 肯定还在服 务器上的,为什么现在我们又要再一次登录呢?这是因为我们关掉浏览再请求的时候,我们提交的信息没有把刚才的 SessionID 一起提交到服务器,所以服务器不知道我们是同一个人,所以这时服务器又为我们分配一个新的 SessionID。打个比方:浏览器就好像一个要去银行开户的人,而服务器就好比银行, 这个要去银行开户的人这个时候显然没有帐号( SessionID),所以到银行后,银行工作人员问有没有帐号,他说没有,这个时候银行就会为他开通一个帐号。所以可以这么说,每次打开一个新的浏览器去请求的一个页面的时候,服务器都会认为,这是一个新的请求,他为你分配一个新的 SessionID。基于以上问题,于是有人就会思考,服务器为什么要保存这些信息呢, 只让每个客户端去保存该多好?服务端只需把关好验证即可,因此在这种情况下,Token 应用而生。关于 Token 的理解什么是 Token ?Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。Token 的引入:Token 是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token 便应运而生。Token 的定义:Token 是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token 便将此 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次带上用户名和密码。使用 Token 的目的:Token 的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。什么是 JWT?说到 Token 我们不得不谈 JWT,Why...? JWT 是 JSON Web Token 的缩写,是目前最流行的跨域认证解决方案。关于跨域认证的问题互联网服务离不开用户认证。一般流程是下面这样。用户向服务器发送用户名和密码。服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。服务器向用户返回一个 session_id,写入用户的 Cookie。用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。举例说明:A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。JWT 的原理JWT 的原理是,服务器认证以后,生成一个 Base64URL 编码后的 JSON 对象,发回给用户,JSON 明文信息如下(后面叙述)。以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。JWT 的数据结构Header(头部):是一个 JSON 对象,描述 JWT 的元数据;Payload(负载):也是一个 JSON 对象,用来存放实际需要传递的数据;Signature(签名):对前两部分的签名,防止数据篡改;说明:JWT 实际是一个很长的字符串,分别由【Header.Payload.Signature】组成,注意中间使用【.】分隔成三个部分。Header(头部)通常如下对象:{
"alg": "HS256", //alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)
"typ": "JWT" //typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
}Payload(负载)JWT 规定了 7 个官方字段,供选用:iss (issuer):签发人exp (expiration time):过期时间sub (subject):主题aud (audience):受众nbf (Not Before):生效时间iat (Issued At):签发时间jti (JWT ID):编号除了上面官方规定的字段,你还可以在这个部分自定义私有字段,下面就是一个例子:{
"sub": "1234567890",
"name": "chait",
"admin": true
}注意:JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。Signature(签名)首先,需要指定一个密钥(secret),这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)算出签名以后,把 Header(Base64URL编码)、Payload(Base64URL编码)、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。Base64URL 算法前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同,区别如下:JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_。JWT 的使用方式客户端接收到服务器返回的 JWT,可以存储在 Cookie 或 Local Storage;客户端每次与服务器通信,都要带上这个 JWT;你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,针对跨域提供两种方案:更好的做法是放在 HTTP 请求的头信息【Authorization】字段里面或者是自定义约定字段;JWT 就放在 POST 请求的数据体里面;JWT 的几个特点(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。基于 Token 的身份验证原理基于 Token 的身份验证是无状态的,我们不用将用户信息存在服务器或 Session 中。这种概念解决了在服务端存储信息时的许多问题。没有 session 信息意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录和已经登录到了哪里。虽然基于 Token 的身份验证实现的方式很多,但大致过程如下:用户通过用户名和密码发送请求。程序验证。程序返回一个签名的 token 给客户端。客户端储存 token, 并且每次请求都会附带它。服务端验证 token 并返回数据。每一次请求都需要 Token。Token 应该在 HTTP的头部发送从而保证了 Http 请求无状态。我们也需要设置服务器属性 【Access-Control-Allow-Origin: * 】来让服务器能接受到来自所有域的请求。需要注意的是,在 ACAO 头部指定 * 时,不得带有像 HTTP 认证,客户端 SSL 证书和 cookies 的证书。执行流程如下:执行流程说明:用户登录校验,校验成功后就返回 Token 给客户端。客户端收到 Token 以后可以把它存储起来,比如放在 localStorage 中。客户端每次访问 API 都(通常 http 请求头)携带 Token 到服务器端。服务器端采用 filter 过滤器校验。校验成功则返回请求数据,校验失败则返回错误码。当我们在程序中认证了信息并取得 token 之后,我们便能通过这个 token 做许多的事情。我们甚至能基于创建一个基于权限的 token 传给第三方应用程序,这些第三方程序能够获取到我们的数据(当然只限于该 token 被允许访问的数据)。Tokens 的优势和缺陷那么相对于 Cookie 和 Session,Token 有哪些优缺点呢?1、Token 的优势支持跨域访问: Cookie 是不允许垮域访问的,token 支持;无状态、可扩展: token 无状态,session 有状态的;去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在 你的API被调用的时候, 你可以进行 Token 生成调用即可.多平台支持: Cookie 不支持手机端访问的,token 支持,更适用于移动应用;基于标准化: 你的 API 可以采用标准化的 JSON Web Token (JWT)。这个标准已经存在多个后端库(.NET,Ruby,Java,Python,PHP 等)和多家公司的支持(如:Firebase,Google,Microsoft 等);2、Token 的缺陷带宽占用:正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多。安全隐患:无法在服务端注销,服务器一旦生产 Token 并下发客户端,在 Token 有效期内很难解决劫持问题。性能问题:JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。但是大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。听着似乎很牛逼,但是没有任何优势,为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。关于 JWT、JWS 与 JWE 的区别,此处不再详述,请自行查看 => https://blog.csdn.net/ChaITSimpleLove/article/details/120178667
基于 Debain11 构建 asp.net core 6.x 的基础运行时镜像
Linux 环境说明此处我们基于 Debian11 的 Linux 发行版,实现目标是编写 Dockerfile 构建 asp.net core 6.x 框架的 runtime 基础镜像。在 Docker 容器化运行环境中,应用程序运行中存在异常情况,此时可以借助一些常用的基础工具方便排查,因此我们需要在 asp.net core 6.x runtime 基础镜像添加 linux 环境常用的基础工具。注意:基础镜像的构建需要考虑镜像的体积和打包工具的安全隐患,此处不做过多讨论。Debian 简介1、Debian 是一个社区来自世界各地的数以千计的志愿者共同为 Debian 操作系统工作,注重自由和开源软件。认识 Debian 计划。2、Debian 是一个操作系统Debian 是一个自由的操作系统,由 Debian 计划开发和维护。Debian 是一个自由的 Linux 发行版,添加了数以千计的应用程序以满足用户的需要。关于 Debian 更多信息,请查看 => https://www.debian.org/intro/index.zh-cn.htmlDebian 发行版本Debian 一直维护着至少三个发行版本:稳定版(stable),测试版(testing)和不稳定版(unstable)。1、稳定版(stable稳定版包含了 Debian 官方最近一次发行的软件包。作为 Debian 的正式发行版本,它是我们优先推荐给用户您选用的版本。当前 Debian 的稳定版版本号是 11,开发代号为 bullseye。最初版本为 11.0,于 2021 年 08 月 14 日发布,其更新 11.6 已于 2022 年 12 月 17 日发布。2、测试版(testing)测试版包含了那些暂时未被收录进入稳定版的软件包,但它们已经进入了候选队列。使用这个版本的最大益处在于它拥有更多版本较新的软件。想要了解 什么是测试版以及 如何成为稳定版的更多信息,请看 Debian FAQ。当前的测试版版本代号是 bookworm。3、不稳定版(unstable)不稳定版存放了 Debian 现行的开发工作。通常,只有开发者和那些喜欢过惊险刺激生活的人选用该版本。推荐使用不稳定版的用户订阅 debian-devel-announce 邮件列表,以接收关于重大变更的通知,比如有可能导致问题的升级。不稳定版的版本代号永远都被称为 sid。4、发行生命周期Debian 通常会按照一定的规律每隔一段时间发布一个新稳定版。 对每个稳定发行版本,用户可以得到三年的完整支持以及额外两年的长期支持。请查看 Debian Releases 维基页面和 Debian LTS 维基页面以了解详细信息。更多详细信息,请查看 => https://www.debian.org/releases/关于 Debian 11Debian 11 带有 Linux 5.10 内核,这是一个长期支持(LTS)版本。一个新的内核显然意味着对硬件有更好的支持,特别是较新的硬件以及性能的改进。这里我们使用 Debian 11,代号为 bullseye,网络安装,用于 64 位 PC(amd64) debian-11.6.0-amd64-netinst.iso。关于 Debian 11 更多信息:【debian-11.6.0-amd64-netinst.iso】下载地址 => https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-11.6.0-amd64-netinst.iso新发布的 Debian 11 “Bullseye” Linux 发行版的 7 大亮点 => https://linux.cn/article-13695-1.htmlLinux 常用基础工具当在 Linux 服务器执行 telnet 命令时,如果提示 command not found: telnet,说明服务器上并未安装 telnet 命令,需要安装此命令。下面介绍在 linux 服务器如何安装 telnet、curl、ifconfig、vim、ping 等工具。首先,介绍一个安装工具时必须的命令 apt install。apt install 是应用程序管理器,用于一键安装软件包,与源码安装不同的是,该指令会自动检测并安装依赖,而且用 apt 安装的包都是成熟的软件包,基本不存在安装包有严重 bug 或者文件缺失的情况。# 1、首先执行如下命令,更新相关资源。将所有包的来源更新,也就是提取最新的包信息,这一命令使用率非常高。
apt update
# 2、安装 telnet
apt install telnet
# 3、安装 curl
apt install curl
# 4、安装 ifconfig
apt install net-tools
# 5、安装 vim
apt install vim
# 6、安装ping
apt install inetutils-ping
# 7、安装 ipaddr
apt install iproute2上面这些基础工具的安装,可以整合一条命令,执行操作如下:apt update && apt install -y net-tools iproute2 iputils-ping telnet curl vim执行上面命令,如果不是 root 用户,需在前面添加 sudo 提权,继续执行操作。Dockerfile 中 RUN 指令上面我们介绍了 linux 环境中常用的基础工具,此处我们 app 应用程序是容器化运行环境,为了方便排查异常信息,通常会在 runtime 基础镜像中添加一些常用工具。在编写 Dickerfile 构建 asp.net core runtime 镜像环境时,我们先来了解下 Dockerfile 中的 RUN 指令。RUN 语法格式在 Dockerfile 中 RUN 指令的编写格式有两种:【shell 形式】,命令在 shell 中运行,默认情况下,Linux 是 /bin/sh -c、Windows 是 cmd /S /C;【exec 形式】,按照 JSON Array 格式解析,意味着必须使用双引号【"】包含参数,而不能使用单引号【’】;RUN 语义说明RUN 指令在当前镜像的顶层上新建层执行命令,同时提交执行结果。提交的结果会在接下来的 Dockerfile 处理。分层 RUN 指令和生成提交符合 Docker 的核心理念,即:提交便利,容器可以依据任意历史镜像构建,像源代码管理一样。exec 形式能够避免 shell 形式表达含义模糊的问题,同时能够在一个不包含 shell 命令的基础镜像上执行 RUN 指令。shell 形式的默认 shell 可以通过 SHELL 修改。shell 形式中,若是指令参数过长,可以使用符号【\】换行显示。# RUN 参数不换行.
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
# RUN 参数换行.
RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'exec 形式是按照 JSON Array 格式解析,必须使用双引号【"】包含参数。与 shell 形式不同,exec 形式不会调用 shell 命令行,意味着不会进行 shell 处理。=> 例如:运行 `RUN ["echo", "$HOME"]` 不会对 `$HOME` 进行变量替换。
如果需要 shell 处理,那么可以使用 shell 形式或直接执行 shell;
=> 例如:`RUN["sh","-c","echo $HOME"]`。
当使用 exec 形式直接执行 shell 时,与 shell 形式类似,应用的 shell 是宿主机而非 Docker。exec 形式中的 JSON,必须转译反斜杠【\】。Windows 系统中,反斜杠【\】是路径分隔符,是需要特别关注的。否则,由于不是有效的 JSON,执行时会出现异常从而失败。# 错误写法
RUN ["c:\windows\system32\tasklist.exe"]
# 正确写法
RUN ["c:\\windows\\system32\\tasklist.exe"]RUN 指令的缓存不会在下次构建时自动失效。RUN apt dist-upgrade -y 指令的缓存将在下次构建时重用。
RUN 指令的缓存可以通过使用 `--no-cache` 标志置为无效,例如:docker build --no-cacheRUN 指令的缓存可由 ADD 和 COPY 指令置为无效。编写 Dockerfile 构建 Runtime 基础镜像在 Docker 中,编写 Dockerfile 是有个细节需要注意,RUN 指令执行多个命令时,可以合并写成一个,在 Dockerfile 中每执行一个指令都会对应的生成一个层,相应的构建镜像的体积也会随之增加。ASP.NET Core Runtime 基础镜像微软 MCR 容器镜像仓库,ASP.NET Core Runtime访问地址:https://mcr.microsoft.com/en-us/product/dotnet/aspnet/aboutDockerfile 编写上面我们介绍了 RUN 指令的语法格式,同样的这里我们为了尽量建设镜像构建的层,通常情况我们会把多个命令整合为一个 RUN 指令执行,完整 Dockerfile 编写如下:FROM mcr.microsoft.com/dotnet/aspnet:6.0
# Debian 源添加参考
# https://developer.aliyun.com/mirror/debian?spm=a2c6h.13651102.0.0.3e221b1137LtM1
# https://mirrors.ustc.edu.cn/help/debian.html
# https://mirrors.tuna.tsinghua.edu.cn/help/debian/
# RUN 使用 shell 语法
RUN sed -i -E 's/(deb|security).debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
RUN sed -i 's/snapshot.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN apt update && apt install -y net-tools iproute2 iputils-ping telnet curl vim
# RUN 使用 exec 语法
#RUN ["apt", "update"]
#RUN ["apt","install","-y","vim","curl","telnet","iputils-ping","iproute2","net-tools"]Windows 环境使用 WSL2 安装 Docker Desktop 工具注意:在 windows 环境使用 Dockerfile 构建镜像,需要安装 Docker Desktop 工具并启动运行。Windows 环境安装 Docker Desktop 工具,推荐使用 WSL2 模式运行。此处不过多讲解安装细节,请自行查看资料镜像安装。Docker Engine 添加如下信息:{
"builder": {
"gc": {
"defaultKeepStorage": "20GB",
"enabled": true
}
},
"dns": [
"8.8.8.8",
"8.8.4.4"
],
"experimental": false,
"features": {
"buildkit": true
},
"insecure-registries": [
"https://hub.atguigu.com"
],
"registry-mirrors": [
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://mirror.ccs.tencentyun.com",
"https://docker.mirrors.ustc.edu.cn",
"https://cr.console.aliyun.com/"
]
}docker build 构建 image 镜像执行 docker build 构建命令:docker build -t aspnet:6.0-debian11-amd64 ./输出如下 image 镜像构建步骤信息:[+] Building 0.6s (9/9) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 32B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for mcr.microsoft.com/dotnet/aspnet:6.0 0.4s
=> [1/5] FROM mcr.microsoft.com/dotnet/aspnet:6.0@sha256:a4ac0ac8b96842c3d4161339e641d335e44f52647bdeb4ed619ac83 0.0s
=> CACHED [2/5] RUN sed -i -E 's/(deb|security).debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list 0.0s
=> CACHED [3/5] RUN sed -i 's/snapshot.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list 0.0s
=> CACHED [4/5] RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list 0.0s
=> CACHED [5/5] RUN apt update && apt install -y net-tools iproute2 iputils-ping telnet curl vim 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:8e4e01b9340eef513279899468870ff57826a1e4f3a6f0b3689212d88f3119eb 0.0s
=> => naming to docker.io/library/aspnet:6.0-debian11-amd64 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them从上面输出的信息可以看出,Dockerfile 文件中每一行单独编写的命令,都会当做一个步骤执行,一共执行 [5/5] 个步骤。查看 docker 镜像信息此时我打开 Docker Desktop 桌面端工具,选择 Images ,搜索框输入 tag 名称【aspnet-debian11-amd64:6.0】就可以看到刚才构建的镜像,体积 270.54 MB 。和原生【mcr.microsoft.com/dotnet/aspnet:6.0】镜像相比,体积增加 170.51 MB,新增的这部分体积,主要是我们在这个镜像的基础上,添加了一些 linux 环境的常用小工具(net-tools、iproute2、iputils-ping、telnet、curl、vim)。我们可以点击【aspnet-debian11-amd64:6.0】镜像,即可进入镜像查看构建的层信息,如下所示:查看上面的镜像层信息,同样的我们也可以使用命令操作查看,操作如下:docker inspect : 获取容器/镜像的元数据。docker inspect [OPTIONS] NAME|ID [NAME|ID...]OPTIONS 说明:-f :指定返回值的模板文件。-s :显示总的文件大小。--type :为指定类型返回JSON。具体命令如下: docker inspect aspnet:6.0-debian11-amd64输出如下信息:[
{
"Id": "sha256:8e4e01b9340eef513279899468870ff57826a1e4f3a6f0b3689212d88f3119eb",
"RepoTags": [
"aspnet:6.0-debian11-amd64"
],
"RepoDigests": [],
"Parent": "",
"Comment": "buildkit.dockerfile.v0",
"Created": "2023-02-19T07:44:04.106566658Z",
"Container": "",
"ContainerConfig": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": null,
"Cmd": null,
"Image": "",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"DockerVersion": "",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"ASPNETCORE_URLS=http://+:80",
"DOTNET_RUNNING_IN_CONTAINER=true",
"DOTNET_VERSION=6.0.14",
"ASPNET_VERSION=6.0.14"
],
"Cmd": [
"bash"
],
"Image": "",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"Architecture": "amd64",
"Os": "linux",
"Size": 270542384,
"VirtualSize": 270542384,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/z1zc2q5ucw0srd1q6fz9dv5zs/diff:/var/lib/docker/overlay2/4vf1uowirpwps4nw4guvfr610/diff:/var/lib/docker/overlay2/xzob6lo9w8ita21r9o6qdowh3/diff:/var/lib/docker/overlay2/1a04b4cea9f8ea832c5f91a20d4ddd4f29f6ec5acb8ebbda6848282ec8159590/diff:/var/lib/docker/overlay2/5036b7ee8c50e873affdb40f9d1fd0e37573631587ed6b1f45347a10995ad935/diff:/var/lib/docker/overlay2/f7d1f588113cb3de37d0cd574614db511a098ba64e18f02ade45cb4b1d431fb6/diff:/var/lib/docker/overlay2/f0c4b961c76c425d8383575ca78c176a890d78d871a5cf60daab2ddc96ffee8d/diff:/var/lib/docker/overlay2/f7c839b494ad006d157084d919c15e812e10e57a8ac1c5cdda4d0150bf494f21/diff",
"MergedDir": "/var/lib/docker/overlay2/9zxjyc54rcaf107f6cj6fhiax/merged",
"UpperDir": "/var/lib/docker/overlay2/9zxjyc54rcaf107f6cj6fhiax/diff",
"WorkDir": "/var/lib/docker/overlay2/9zxjyc54rcaf107f6cj6fhiax/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:4695cdfb426a05673a100e69d2fe9810d9ab2b3dd88ead97c6a3627246d83815",
"sha256:f30d150c01520fbbbdd1bedcef3f940d809c82a46e08301dcc713903d7272ba3",
"sha256:fe674e2b138caf1ef6a67419d5fb0a9081ca0759d97ce711be80d5d4d67145e1",
"sha256:ff13768cb51ea8fe1831d93c2d18690c3fdca8cfee40b75a738f62b133413573",
"sha256:355b7bb8c23e0d867141b0af69ecb6df39730bcd1b6c754cfae9ad36ca3f5572",
"sha256:8d56eaae8bfea0a5eac4caf4350fa1987c162cd7dba828aa292cc15c3e90d91b",
"sha256:b7d0c701d1f04294b2b98d3103c3856c9cbabc592cde48d3e07eb03bff7cb18a",
"sha256:46ef367d7575beda39ffd3774b4d47b29c2fe0b628f1269b4626053ca44169d5",
"sha256:6809bcac556f83ca4489ba09980a5bef05c9bc7463a45e4dac7fb600beec1c4e"
]
},
"Metadata": {
"LastTagTime": "2023-02-19T08:46:50.196739744Z"
}
}
]镜像构建后,此时如果有镜像仓库(腾讯云平台准备 Docker 私有镜像仓库或者 IDC 机房搭建),确保 vm 和宿主机之间通信可以正常登录访问,此处以腾讯云 Docker 私有镜像仓库为例:# 登录腾讯云docker registry
sudo docker login --username=[user] ccr.ccs.tencentyun.com
# 提示输入对应的密码即可
# 从 registry 拉取镜像
sudo docker pull ccr.ccs.tencentyun.com/dotnet/image-name:[tag]
# 将镜像推送到 registry
sudo docker login --username=[user] ccr.ccs.tencentyun.com
sudo docker tag [ImageId|image-name:tag] ccr.ccs.tencentyun.com/dotnet/aspnet:6.0-debian11-amd64
sudo docker push ccr.ccs.tencentyun.com/dotnet/aspnet:6.0-debian11-amd64到这里我们就演示完 Dockerfile 的编写,以及 image 镜像构建的全过程。总结掌握 Dockerfile 文件语法格式的编写,熟悉 RUN 指令中多个命令的编写方式(减少 image 镜像构建层,相应的减少镜像体积大小),顺便熟悉下 Docker 镜像的构建 docker build 和 镜像的 push(推送)和 pull(拉取) 基本操作,感兴趣的小伙伴可以跟随上面描述动手实践起来,可以加深对 Docker 镜像构建的理解。
快速理解 IdentityServer4 中的认证 & 授权
前言在实际的生产环境中,存在各种各样的应用程序相互访问,当用户访问 app 应用的时候,为了安全性考虑,通常都会要求搭配授权码或者安全令牌服务一并访问,这样可有效地对 Server 端的 API 资源起到一定程度的有效保护,大概流程如下:接下来我们就针对这个 API 请求访问的过程进行详细的了解,那么通常会存在哪些交互模式呢?常见的交互模式在应用请求访问的过程中,最常见的交互模式有以下几种:browser(浏览器)与 web app (web 应用程序)通信;web app 与 web APIs 进行通信;基于 browser(浏览器)的 app 应用程序与 web APIs 通信;native app (原生应用)与 web APIs 通信;基于 service 服务的 app 应用程序与 web APIs 通信;web APIs 与 web APIs 进行通信;通常情况,每个层(前端、中间层和后端)都必须保护资源,并实现身份 认证(Authentication)和 授权 (Authorization),所以它们通常是针对同一个 用户(User)进行存储。将这些基本安全功能外包给 STS(Security Token Service,安全令牌服务),可以防止在这些 App 应用程序和 端点(Endpoint)之间复制该功能。依据上面罗列的几种常见的交互模式,接下来我们对 app 应用程序进行重构以支持 STS,这将形成以下体系结构和协议:认证 & 授权在应用程序中,通常的 认证(Authentication)& 授权(Authorization) 服务流程如下:认证与授权是两个概念,不能混淆为一,接下来我们对这两个概念分别逐个了解。什么是 Authentication(认证)?对请求方或访问者的身份鉴别,意思就是确认你的身份是你。举个例子,比如早晨你去公司上班,到公司门口需要刷厂牌或工牌,然后公司的门禁卡会识别你个人的身份信息,接着鉴别或确认你是否所属公司的成员,如果是那么就可以进入公司范围,反之就不能进入。这个过程就称为认证。常见的身份验证协议:SAML2p,安全断言标记语言(英语:Security Assertion Markup Language,简称:SAML);WS-Federation,联合身份验证是 (安全域) 领域集合,这些领域已建立安全共享资源的关系。OpenID Connect,通常也叫 OIDC,是一套基于 OAuth 2.0 协议的轻量级规范,提供通过 API 进行身份交互的框架。其中 SAML2p 是最流行和最广泛部署的,而 OIDC 是三款中最新的,但被认为是未来的趋势,因为它具有现代应用的最大潜力。 它是从一开始就构建用于移动应用场景的,并且被设计为 API 友好。相关文章:干货|理解 SAML2 协议,https://baijiahao.baidu.com/s?id=1742115008490452461&wfr=spider&for=pcAPI 网关 OpenID Connect 使用指南,https://help.aliyun.com/document_detail/48019.htmlOpenID Connect 是什么?http://t.zoukankan.com/lexiaofei-p-7233230.htmlSAML2.0对接,https://help.aliyun.com/document_detail/114853.html?scm=20140722.184.2.173OAuth2.0对接,https://help.aliyun.com/document_detail/114852.html什么是 Authorization(授权)?在确认你的身份信息之后对你进行相应的授权。接着上面的例子,当公司的门禁卡鉴别了你的个人身份信息后,确定你是公司的所属成员,就可以顺利的进入公司,而公司里面又有很多的部门,每个部门有不同的职责范围,当然你对应的也所属其中某一个(或多个)部门,其中每个部门具有一定的权限范围,那么这个部门范围的划分过程就类似授权。常见的授权协议:OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。OAuth 在全世界得到广泛应用,目前的版本是 2.1 版(https://oauth.net/2.1/)。OAuth 2.0 协议官方地址,https://www.rfc-editor.org/rfc/rfc6749OAuth 2.0 协议特点:简单:不管是 OAuth 服务提供者还是应用开发者,都很易于理解与使用;安全:没有涉及到用户密钥等信息,更安全更灵活;开放:任何服务提供商都可以实现 OAuth,任何软件开发商都可以使用 OAuth;OIDC 和 OAuth 2.0 非常相似,实际上 OIDC 是在 OAuth 2.0 之上的扩展。两个基本的安全考虑,身份验证和API 访问,被组合成了一个单一的协议 IdentityServer4( 简称 IDS4,文章下面会讲述),通常与 STS(安全令牌服务)一起单一往返。相关文章:OAuth 2.0,https://oauth.net/2/OAuth2.0 详解,https://zhuanlan.zhihu.com/p/509212673asp.net core 项目中的认证 & 授权通过对前面的 认证(Authentication)& 授权(Authorization) 的了解,我们来回顾下 asp.net core mvc/webapi 项目中 Filter 的 认证 & 授权 流程(你是否有似曾相识的感觉呢?),如下所示:IdentityServer4 框架什么是 IdentityServer4 ?通常情况下,我们会把 OIDC 和 OAuth 2.0 搭配使用,认为该组合是在可预见的未来保护现代应用程序的最佳方法。而 IdentityServer4 是这两个协议的实现,并且经过高度优化,可以解决当今 移动(mobile)、原生(native)和 Web 应用程序 的典型安全问题。官方解释:IdentityServer4 是基于 ASP.NET Core 实现的认证和授权框架,是对 OpenID Connect 和 OAuth 2.0 协议的实现。通俗理解:服务端 Server 对需要认证授权的资源(Resource,客户端请求资源)在外层使用 IdentityServer4 框架进行封装加壳,用户只能通过获取 IdentityServer4 颁发的令牌(Token)后,才能有效地进行后续的资源访问。总之 IdentityServer 是一个身份 认证(Authentication)& 授权(Authorization) 程序(或框架),该程序实现了 OIDC(OpenID Connect) 和 OAuth 2.0 协议。说明:同一种概念,不同的文献使用不同的术语,比如有些文献把他叫做 安全令牌服务(STS,Security Token Service)、身份提供(IP,Identity Provider)、授权服务器(Authorization Server)、IP-STS 等等。其实他们都是一个意思,目的 都是 在软件应用中为客户端颁发 Token 令牌并用于安全访问的。IdentityServer4 有哪些功能?IdentityServer4 提供如下功能:Resource 资源保护;使用本地帐户或通过外部身份提供程序对用户(User)进行身份验证;提供会话管理和单点登录(SSO) & 注销;管理和验证客户机(Clients);向客户颁发标识(Identity)和访问令牌(Access token);验证 Token 令牌;IdentityServer4 工作原理关于 IDS4 的术语解释:User(用户):用户是使用注册的客户端访问资源的人。Clients(客户端):客户端是从 IdentityServer 请求令牌的软件,用于验证用户(请求身份令牌)或访问资源(请求访问令牌)。 必须首先向 IdentityServer 注册客户端才能请求令牌。客户端的示例可以是:Web 应用程序、移动或桌面应用程序、SPA、服务器进程等。Resources(资源):资源是要用 IdentityServer 保护的资源,包括用户的身份信息或API。每个资源都有一个唯一的名称,客户端使用这个名称来指定他们想要访问的资源。用户的身份信息,包括名称或电子邮件等。API 资源则是客户端想要调用的功能,它们通常是 Web API,但不一定。Identity Token(身份令牌):身份令牌表示身份验证过程的结果。它至少包含:1、用户的标识;2、用户如何以及何时进行身份验证的信息。它也可以包含其他身份信息。Access Token(访问令牌):访问令牌允许用户访问 API 资源,客户端请求访问令牌并将其转发到 API。访问令牌包含有关客户端和用户的信息,API 使用该信息来授权用户访问它的数据。关于 IdentityServer 更多信息请参考相关文档:http://www.identityserver.com.cn/Home/Detail/shuyuhttps://identityserver4docs.readthedocs.io/zh_CN/latest/index.htmlhttps://identityserver4.readthedocs.io/en/latest/IdentityServer4 应用示例为了演示下面模式,创建项目结构如下:1、基于内存模式(Sample.WebIdentityServer)1.1 Client 模式,Sample.ConsoleApp 访问 Sample.WebIdentityServer1.2 Server 模式,Sample.WebApi 访问 Sample.WebIdentityServer2、基于 db 模式(Sample.WebIdentityServer)2.1 数据持久化 db1、安装 IdentityServer4 模板(可选)说明:此处使用 dotnet cli 安装 IdentityServer4 的前提是务必确保宿主机已经安装 dotnet sdk。此处为了更快的入门 IdentityServer4 框架,我们参照官方文档的快速入门部分,首先通过 dotnet cli 安装模板,执行如下命令:dotnet new -i IdentityServer4.Templates安装成功后,输出如下信息:将安装以下模板包:
IdentityServer4.Templates
成功: IdentityServer4.Templates::4.0.1 已安装以下模板:
模板名 短名称 语言 标记
---------------------------------------------------- -------- ---- -------------------
IdentityServer4 Empty is4empty [C#] Web/IdentityServer4
IdentityServer4 Quickstart UI (UI assets only) is4ui [C#] Web/IdentityServer4
IdentityServer4 with AdminUI is4admin [C#] Web/IdentityServer4
IdentityServer4 with ASP.NET Core Identity is4aspid [C#] Web/IdentityServer4
IdentityServer4 with Entity Framework Stores is4ef [C#] Web/IdentityServer4
IdentityServer4 with In-Memory Stores and Test Users is4inmem [C#] Web/IdentityServer42、创建 Sample.WebIdentityServer 项目首先为应用程序创建一个目录,然后使用 is4empty 模板创建一个包含基本 IdentityServer 设置的 ASP.NET Core 应用程序。cli 执行如下命令:cd E:\CodeStuday # 进入目标盘符(此处是E盘 CodeStuday 文件夹)
md quickstart # mkdir 简写 md,创建 quickstart 文件夹
cd quickstart # 进入 quickstart 文件夹
md src # 创建 src 文件夹
cd src # 进入 src 文件夹
# cli 查看模板创建列表
dotnet new -l
# 使用 is4empty 模板创建一个包含基本 IdentityServer 设置的 ASP.NET Core 应用程序
dotnet new is4empty -n Sample.WebIdentityServer或者通过 vs2022 手动创建一个 asp.net core empty 项目(等效同上),这里先给出改造好的项目结构如下:新建的 Sample.WebIdentityServer 项目改造如下:2.1、添加 Nuget 包 IdentityServer4添加完成后,项目 Sample.WebIdentityServer.csproj 工程文件如下:<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.2" />
</ItemGroup>
</Project>2.2、新增 IdentityServerConfig.cs 类,添加如下代码:using IdentityServer4.Models;
using System.Collections.Generic;
namespace Sample.WebIdentityServer;
public static class IdentityServerConfig
{
public static IEnumerable<ApiScope> ApiScopes => new[]
{
new ApiScope
{
Name = "sample_api",
DisplayName = "Sample API"
}
};
public static IEnumerable<Client> Clients => new[]
{
new Client
{
ClientId = "sample_client",
ClientSecrets =
{
new Secret("sample_client_secret".Sha256())
},
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "sample_api" }
}
};
}2.3、改造为 Program.cs & Startup.cs 模式由于 .net6 中的 asp.net core 默认只有 Program.cs,这里我们改造为 Program.cs & Startup.cs 模式。3.1 先添加 Startup.cs 文件,代码如下:namespace Sample.WebIdentityServer;
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// Add services to the container. 注册服务到 Ioc 容器
public void RegisterServices(IServiceCollection services, IHostBuilder host)
{
# 添加 IdentityServer
var builder = services.AddIdentityServer();
# 添加开发人员签名凭据
builder.AddDeveloperSigningCredential();
# 使用内存模式,注册 ApiScopes 和 Clients
builder.AddInMemoryApiScopes(IdentityServerConfig.ApiScopes);
builder.AddInMemoryClients(IdentityServerConfig.Clients);
}
// Configure the HTTP request pipeline. 配置 HTTP 请求管道(中间件管道即中间件委托链)
public void SetupMiddlewares(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
# 使用 IdentityServer
app.UseIdentityServer();
}
}3.2 接下来改造 Program.cs 文件中代码,修改如下:namespace Sample.WebIdentityServer;
public class Program
{
/*
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
}*/
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var startup = new Startup(builder.Configuration);
startup.RegisterServices(builder.Services, builder.Host);
var app = builder.Build();
startup.SetupMiddlewares(app, builder.Environment);
app.Run();
}
}到此处 Sample.WebIdentityServer 项目就改造完毕,可以尝试启动运行测试,成功运行后,会在该项目的根目录生成一个 tempkey.jwk 文件,文件内容如下:{
"alg": "RS256",
"d": "gc7bg0NCxsOf8AMxX76ZubsTHblmN5WLSkbbk9miPBYIVhO4TdYAZX7rU65rU5v9Z6Kn9Tm-4gslOEfnivdTQAlq5SWkDe6S152so32z0xlH5gUL02686_IY6dHi89gwMNJziEU39PqrtQXhLpoVz2H1JXDzrqt5_QacueuZUG_tS1xEps1xelrjCk1isIRb3y05xLqWH4c4zNDXLqRaF9PR43FZ7Ea6mxAa3meZ0HZ23QN0075BByI0jkhV3TpQjYZ7oWjsXCeFrLGJILo6bWoP7Y-sQRgXmZChRvHXuYmEPeQlFSA1xwEJL8eadd8YL_T5o_wFWEg1rNwncoFYZQ",
"dp": "I6s-o6dW1CjvO_SGLunlmwAJeHFT3sFjC9MXAIzjW2AhXWbocGEP5oS6jnHOJ-x3F5_FhRzabTOj-drZ9eAVD9HPDbhX0a6GvOX5l9orFC6Bd5EogZ2PfkaaDWRa-AjJULiLkEvS3EgSa0T223V1gKMQyj3bEjU0o2ZMoLnAiIk",
"dq": "cmT7rXskO9aBk980wR32x3emigZK9GJCHg_U308zeGm4_WfPAbQrvd8zSQ8sdLMGi1bloiBeaGqHtLrQBQYpRw1W8IP6qvTwGHwqc5X9WQvuEtqBAmp0Dsc0eCHwrBDVtgzIxQGC63BAPIzMErnr2-pPGG1-yTmNuQkB7zf1fZ8",
"e": "AQAB",
"kid": "7D56F0331FF5B9BFBCFCB25DF9D81E44",
"kty": "RSA",
"n": "183z7AQ4qrQzOOoNg3ven3wgFeavnf2xncJ85bAL4df18rAKtlPaH6xc1ELhu01RUmpvHJCv4dQ9F4JTs0kXZMw3ZcIKw6fDuVKZThEMfkAkJ3ANkAVnwlOb_eoELi4ER5S_VLgX6YWXsP3GHZhLIdNjwBzXNW8E61IM0pbtuDsG8gmMMhoe6qxY_6IehLtL-FU1As9MxsRVfRFnnUl0paHSjUQsxHcqMDy8Oa-FKbnCIaj9ZI8Y5y1HF5YEOH2XA_6T8AUUCaMTZd92ojleMEh9INnrSvpc81epAZVFXMV4f1bwac_O1N5VKRWGxnqczBWaUwDucjHSYJTxTUlCmQ",
"p": "_4j2ratx8BkrO4sylG5FwwpPtmx5yjGGzlwXYoBjoZ9rR0_ALQprAASB744tRVzErQXaTU2POZfud71kbYgV79l2TeQrtkyToPxDjJYT4O0ET8XLd5tJpmWl4_ifTBw-82c2wsDYCpqpHlFTZ6mci-_rxqZ8sxtkbKk_44XMy_8",
"q": "2DJ7Qp4ZhVbZjDwY9HdyW1ah98CqhiW-G1-lZYuwPM90ym6Ejhc0yr4qL150VMacZEsZ4gcEacCsrxmV6dqlr2N6XMn6EkpsKbKjowOtC0hKHEdm1iFfPB_T0ZjfpJZZPsL1-wL1tWMfDIkcXfpkyJTaCaVSyMoX3UOE85qo0Wc",
"qi": "hyrZ9xBy3P4ql7tN0TxUIiiHaIi9HUs2DBHibaRH61_g_Hlf9nzY6KCSlVjRVPC5TSGQy3sj-z7Im-Cpu9r1PQWuXk3zGCxmDcX5ppeJXqoxXT92SZh4IYQMKkgOZwUMKB7ebvw25yJf-BJOmsRu8KhKNNMWkWeOERQw8jrnz0A"
}2.4 启动【Sample.WebIdentityServer】项目运行测试创建好项目后,习惯性的启动项目运行测试,正常启动后输出如下信息:PS E:\CodeStuday\dotnet6\Sample.WebIdentityServer> dotnet run
正在生成...
info: IdentityServer4.Startup[0]
Starting IdentityServer4 version 4.1.2+997a6cdd643e46cd5762b710c4ddc43574cbec2e
info: IdentityServer4.Startup[0]
You are using the in-memory version of the persisted grant store. This will store consent decisions, authorizaon codes, refresh and reference tokens in memory only. If you are using any of those features in production, you wanto switch to a different store implementatio
info: IdentityServer4.Startup[0]
Using the default authentication scheme idsrv for IdentityServer
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:5019
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5018
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: E:\CodeStuday\dotnet6\Sample.WebIdentityServer\从上面的日志信息可以看出:1. Starting IdentityServer4 version 4.1.2+997a6cdd643e46cd5762b710c4ddc43574cbec2e.
2. You are using the in-memory version of the persisted grant store.
3. Now listening on: https://localhost:5019 & http://localhost:5018.接下来使用 ApiPost6 工具访问服务监听端口,操作如下:2.4.1、请求【.well-known/openid-configuration】配置文档:# url 格式
http://ip:port/.well-known/openid-configuration
# 这里是本地主机,端口 5018
http://localhost:5018/.well-known/openid-configuration当 ApiPost 工具访问 url 地址后,控制台输出如下日志信息:info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration【/.well-known/openid-configuration】接口返回如下信息:{
"issuer": "http://localhost:5018",
"jwks_uri": "http://localhost:5018/.well-known/openid-configuration/jwks",
"authorization_endpoint": "http://localhost:5018/connect/authorize",
"token_endpoint": "http://localhost:5018/connect/token",
"userinfo_endpoint": "http://localhost:5018/connect/userinfo",
"end_session_endpoint": "http://localhost:5018/connect/endsession",
"check_session_iframe": "http://localhost:5018/connect/checksession",
"revocation_endpoint": "http://localhost:5018/connect/revocation",
"introspection_endpoint": "http://localhost:5018/connect/introspect",
"device_authorization_endpoint": "http://localhost:5018/connect/deviceauthorization",
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true,
"scopes_supported": [
"sample_api",
"offline_access"
],
"claims_supported": [],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"implicit",
"urn:ietf:params:oauth:grant-type:device_code"
],
"response_types_supported": [
"code",
"token",
"id_token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"response_modes_supported": [
"form_post",
"query",
"fragment"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"subject_types_supported": [
"public"
],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"request_parameter_supported": true
}2.4.2、请求【token_endpoint】接口,获取 access_token在 ApiPost 的 Body 中配置参数信息(参见下图),使用 POST 方式访问如下地址:http://localhost:5018/connect/tokenhttps://localhost:5019/connect/token【connect/token】接口响应信息:{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdENTZGMDMzMUZGNUI5QkZCQ0ZDQjI1REY5RDgxRTQ0IiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2NjcxOTMyNTMsImV4cCI6MTY2NzE5Njg1MywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAxOSIsImNsaWVudF9pZCI6InNhbXBsZV9jbGllbnQiLCJqdGkiOiJFRTRBNkEwOUFFMDU2RUYxRTg5NjQzQUQ4RDc4MzkzMSIsImlhdCI6MTY2NzE5MzI1Mywic2NvcGUiOlsic2FtcGxlX2FwaSJdfQ.LKxAAFNjDj3YWXubprAdekWuSEw2ymPAv0YE5u9pEK2zn9ycuYKeqSN29yfaZc0oj4G_QoGKTmlAAdQ5uy72scT4hJYrFXtEjw880GY49rDVd579pFpv7jVWW_324LbNgrAhhJr7l37X3G8wC2sW9ZEvhZM89m6I9DgIUVMUboFUEADcTD5h4twQZ1RjQtcl0DLohcz6c3LPfERqs1QFF8A1bgu_Wszt4ZJADmPLE7wInxPcHpcnEkHl-3xKr1sP0MxhHqQkMpH8SYYnpmUiIwiqRm0qWAfqxEfQmOeVaSqqyGP25vRx2mgGez9DOm7jyOJdiswsGUA2WOQAys8EjA",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "sample_api"
}3、创建【Sample.WebApi】项目3.1 使用 cli 创建项目(和上面的项目保存在同一个跟目录)dotnet new webapi -n Sample.WebApi3.2 使用 vs 2022 打开项目,添加 nuget 依赖包:项目【Sample.WebApi.csproj】配置文件如下:<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.10" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>3.3 项目改造为 Program.cs & Startup.cs 模式项目同样改造为 Program.cs & Startup.cs 模式,详细操作请看下面。3.3.1 添加 Startup.cs 文件,代码如下:新增【Startup.cs】文件,添加如下代码:namespace Sample.WebApi;
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// Add services to the container. 注册服务到 Ioc 容器
public void RegisterServices(IServiceCollection services, IHostBuilder host)
{
services.AddControllers();
// 1.添加 IDS4 认证服务
services.AddAuthentication(defaultScheme: "Bearer")
.AddJwtBearer(authenticationScheme: "Bearer", configureOptions: options =>
{
options.Authority = "https://localhost:5019";
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateAudience = false //不验证
};
});
// 2.添加 IDS4 授权策略
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", builder =>
{
builder.RequireAuthenticatedUser();
builder.RequireClaim("scope","sample_api");
});
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
// 配置 Swagger 中间件
services.AddSwaggerGen(options => {
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "Sample.WebApi",
Version = "v1"
});
});
}
// Configure the HTTP request pipeline. 配置 HTTP 请求管道(中间件管道即中间件委托链)
public void SetupMiddlewares(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
//app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(options => {
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Sample.WebApi");
});
}
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // 认证
app.UseAuthorization(); // 授权
app.UseEndpoints(endopints =>
{
endopints.MapControllers();
});
}
}注意:务必确保下面两点的配置信息和 IdentityServer 项目的 Config 配置保持一致。添加 IDS4 认证服务添加 IDS4 授权策略3.3.2 修改 Program.cs 文件namespace Sample.WebApi;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var startup = new Startup(builder.Configuration);
// Add services to the container.
startup.RegisterServices(builder.Services, builder.Host);
var app = builder.Build();
// Configure the HTTP request pipeline.
startup.SetupMiddlewares(app, builder.Environment);
app.Run();
}
}3.3.3 新增 IdentityController.cs 文件(webapi)此处为了和项目默认的 WeatherForecastController(webapi) 参照对比,因此新增 IdentityController.cs 文件,添加如下代码:using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Sample.WebApi.Controllers;
/// <summary>
/// identity
/// </summary>
//[Route(template:"Identity")]
[Route("[controller]")]
[Authorize("ApiScope")]
[ApiController]
public class IdentityController : ControllerBase
{
[Authorize]
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from claim in User.Claims select new { claim.Type, claim.Issuer, claim.ValueType });
}
}WeatherForecastController.cs 默认代码如下:using Microsoft.AspNetCore.Mvc;
namespace Sample.WebApi.Controllers;
/// <summary>
/// WeatherForecast
/// </summary>
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}3.4 启动【Sample.WebApi】项目运行测试PS E:\CodeStuday\dotnet6\Sample.WebApi> dotnet run
正在生成...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5006
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: E:\CodeStuday\dotnet6\Sample.WebApi\访问 Swagger 页面:http://localhost:5006/swagger/index.html分别访问上面两个 api 接口,响应信息如下:Identitycurl -X 'GET' \
'http://localhost:5006/Identity' \
-H 'accept: */*'接口显示 Unauthorized 未授权。WeatherForecastcurl -X 'GET' \
'http://localhost:5006/WeatherForecast' \
-H 'accept: text/plain'由于该接口未设置特性权限,所以可正常访问。通过上面两个 api 接口的对比测试,说明代码中的授权中间件生效了。配置 ApiPost6 工具访问 Identity 接口在 ApiPost6 工具的【认证】栏添加授权访问的 access_token,使用 GET 方式访问 Identity 接口:上面步骤就演示了基于内存模式使用 IdentityServer4 的案例,该案例是 WebAPI 访问 WebAPI 的项目场景,接下来我们继续创建一个控制台项目,模拟 Console 项目访问 WebAPI 的场景。4、创建【Sample.ConsoleApp】项目4.1 创建【Sample.ConsoleApp】项目使用 dotnet cli 创建项目,执行命令: dotnet new console -n Sample.ConsoleApp4.2 【Sample.ConsoleApp】项目添加 nuget 包项目新建好后,添加 nuget 包依赖:4.3 改造【Sample.ConsoleApp】项目然后修改 Program.cs 文件,在 Main 方法中添加如下代码:using IdentityModel.Client;
namespace Sample.ConsoleApp;
internal class Program
{
static async Task Main(string[] args)
{
//阶段一:访问 IDS4 获取身份认证信息
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5019");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest {
Address = disco.TokenEndpoint,
ClientId = "sample_client",
ClientSecret = "sample_client_secret"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
//阶段二:访问受保护权限的 api 接口
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:5006/Identity");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
return;
}
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
Console.ReadKey();
}
}上述代码,为了模拟两阶段的请求(先认证,再授权),此处直接 new 了两个 HttpClient 对象,生产环境可以使用构造函数 DI 方式注入该对象,或者使用扩展 nuget 包:Refit.HttpClientFactory4.4 启动【Sample.ConsoleApp】项目运行测试关于该 nuget 包的使用,请自行查看相关资料,这里不再叙述,接下来我们启动项目运行测试,输出如下信息:{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdENTZGMDMzMUZGNUI5QkZCQ0ZDQjI1REY5RDgxRTQ0IiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2NjcyMDAwMDQsImV4cCI6MTY2NzIwMzYwNCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAxOSIsImNsaWVudF9pZCI6InNhbXBsZV9jbGllbnQiLCJqdGkiOiJFNDk5QjAwRTAyQTc5NkFBRjhCMTIxNkIwODhCRjY4NyIsImlhdCI6MTY2NzIwMDAwNCwic2NvcGUiOlsic2FtcGxlX2FwaSJdfQ.sZ3XKDIjrhiVu-1Twt6BUZMiqQ99coFb_kkThZECO6N68TOpch1_4h0rHggyyu4j-jDHh4itkv5iuCE4nGYiGVF196bh0fd68AqVl6A7IlMN3WfmwnLBgEMSIMNBJMWToEPb8l7y3jB9Uv6QoPbKzFw1k4ghePiFq0Qn7s3qXAefnSfYreUdHaIewJXk5egSOsePQpU_Rm33CvfCT3pKecJ1-mbwpjqn_euCuYmE4sWm8aGwi9DGnZt1W9jUZCoVW8_MoONW1pKnmcN5s-07JBwjFDhdY-EyPwY3b-U7B7jfvTxp7krr4XYOWKIvUZdN8afPDbr-lNbx7dC0mSdWPg",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "sample_api"
}
[{
"type": "nbf",
"issuer": "https://localhost:5019",
"valueType": "http://www.w3.org/2001/XMLSchema#integer"
}, {
"type": "exp",
"issuer": "https://localhost:5019",
"valueType": "http://www.w3.org/2001/XMLSchema#integer"
}, {
"type": "iss",
"issuer": "https://localhost:5019",
"valueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"type": "client_id",
"issuer": "https://localhost:5019",
"valueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"type": "jti",
"issuer": "https://localhost:5019",
"valueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"type": "iat",
"issuer": "https://localhost:5019",
"valueType": "http://www.w3.org/2001/XMLSchema#integer"
}, {
"type": "scope",
"issuer": "https://localhost:5019",
"valueType": "http://www.w3.org/2001/XMLSchema#string"
}]从上面输出的信息中,我们来分下 access_token 是个啥东东,其实它是一个 jwt 格式的数据,我们使用在线格式化工具来更佳直观的查看数据,如下图所示:https://jwt.ms/格式化解析数据:JWT 数据格式我们简单的回顾下 JWT 的数据格式,由以下三部分组成。JWT 的组成结构注意:推荐 jwt 使用 https 协议,不应使用 http ,生产环境安全性考虑,防止信息泄漏。Header,头部包含元信息,对明文信息使用 base64 编码值;Payload,负载、载荷配置一些客户端相关信息使用 base64 编码值;Signature,签名部分是使用 Header 部分的加密算法对(Header + Payload )的编码值数据再次加密;JWT 参数解释1、头部(Header)=> alg:签名(加密)算法;=> kid:令牌ID;=> typ:令牌类型;2、载荷(Payload)=> nbf:jwt 生效(签发)的时间,再此之前 jwt 不可用;=> exp:jwt 过期时间(大于 nbf 签发时间),在此之后 jwt 失效;=> iss:jwt 签发人,通常情况为签发服务器地址;=> client_id:客户端地址;=> jti:jwt 的唯一标识,作用辨识每一次的 jwt 不会重复;=> iat:jwt 的签发时间,一般情况和 nbf 相同;=> scope:api 范围(理解成分组),在该范围内合法有效;总结通过上面的项目示例,可以看出 IdentityServer4 框架的核心部分依然是 JWT,通过该框架整合了 认证和授权 两阶段的相关操作,给用户的感觉就是一步到位,无感体验。对应常规性需求的项目,使用 内存模式 亦可满足,只需在 IdentityServerConfig.cs 类中注册相应的配置信息即可,但对应动态改变的配置信息,还可以持久化配置 DB模式,关于该模式的改造步骤,感兴趣的小伙伴自行参看相关文档进行研究,本篇文章的目的是快速入门 IdentityServer4 框架,以帮助更多的小伙伴快速上手体验。
基于.net core 开发的轻量级配置中心 - AgileConfig
AgileConfig 简介是一个基于 .net core 开发的轻量级配置中心。目标或解决的问题AgileConfig 并不是为了什么微服务,更多的是为了那些分布式、容器化部署的应用能够更加简单的读取、修改配置。AgileConfig 特点秉承轻量化的,部署简单、配置简单、使用简单、学习简单,它只提取了必要的一些功能,并没有像 Apollo 那样复杂且庞大。但是它的功能也已经足够你替换 web.config,appsettings.json 这些文件了。如果你不想用微服务全家桶,不想为了部署一个配置中心而需要看 N 篇教程跟几台服务器那么你可以试试 AgileConfig。部署简单,最少只需要一个数据节点,支持 docker 部署支持多节点分布式部署来保证高可用配置支持按应用隔离,应用内配置支持分组隔离支持多环境应用支持继承,可以把公共配置提取到一个应用然后其它应用继承它使用长连接技术,配置信息实时推送至客户端支持 IConfiguration,IConfigClient,IOptions 模式读取配置,原程序几乎可以不用改造配置修改支持版本记录,随时回滚配置如果所有节点都故障,客户端支持从本地缓存读取配置支持 Restful API 维护配置v-1.6.0 以上已支持服务注册与发现参考地址Github:https://github.com/dotnetcore/AgileConfigGitee:https://gitee.com/kklldog/AgileConfigAgileConfig releases:https://github.com/dotnetcore/AgileConfig/releasesAgileConfig Change log:https://github.com/dotnetcore/AgileConfig/blob/master/CHANGELOG.mdDocker 镜像:https://hub.docker.com/r/kklldog/agile_config作者博客:https://www.cnblogs.com/kklldog/p/agile-config.htmlAgileConfig 架构介绍AgileConfig 的架构比较简单,主要是分 3 块:客户端(Client)客户端程序是使用 netstandard2.0 开发的一个类库,方便 .net core 程序接入,nuget 搜AgileConfig.Client 就可以安装。可以在启动客户端的时候配置多个节点的地址,客户端会随机挑选一个进行连接,连接成功后会维持一个 websocket 长连接。如果连接的节点发生故障导致连接中断,客户端会继续随机一个节点进行连接,直到连接成功。节点、管理程序(Node Server、Console)节点是使用 asp.net core 开发的一个服务。为了部署简单,直接把管理程序跟节点服务合二为一了。任何一个节点都可以在启动的时候配置环境变量开启管理程序功能。数据库(Database)使用数据库来存储数据,目前支持 Sqlserver, Mysql, Sqlite, PostgreSql, Oracle 五种数据库。最新版本已经切换为 FreeSql 为数据访问组件。FreeSql 对多数据库的支持更加强劲,特别是对国产数据库的支持。但是因为没有国产数据库的测试环境,本项目并未支持,如果有需要我可是开分支尝试支持,但是测试工作就要靠用户啦。注意:如果使用 <=1.0.4 之前版本的用户请不要更新,因为 EFCore 跟 FreeSql 自动建的库可能存在稍许差异,保险起见不要更新吧。关于高可用AgileConfig 的节点都是无状态的,所以可以横向部署多个节点来防止单点故障。在客户端配置多个节点地址后,客户端会随机连接至某个节点。问题影响说明控制台下线无法维护配置,客户端无影响因为控制台跟节点是共存的,所以某个控制台下线一般来说同样意味着一个节点的下线某个节点下线客户端重连至其他节点无任何影响所有节点下线客户端从内存读取配置启动的客户端会从内存读取配置,未启动的客户端会再尝试连接到节点多次失败后,尝试从本地文件缓存读取配置,保证应用可以启动注意:结合 DB 数据库的高可用技术搭配使用。AgileConfig 服务端搭建初始化 DB 数据库用户只需要手工建一个空库,所有的表在第一次启动的时候都会自动生成。目前支持 SqlServer,MySQL,Sqlite, PostgreSQL,Oracle 五种数据库。 docker 运行服务端项目即可初始化对应的数据库,此处以 PostgreSQL 数据库为例。DB Provider 对照表数据名称:agile_config,用户:chait,密码:123456db providerdb type连接字符串sqlserverSQL ServerData Source=127.0.0.1,1433;Initial Catalog=agile_config;User Id=chait;Password=123456;TrustServerCertificate=true;Pooling=true;Max Pool Size=50mysqlMySQLData Source=127.0.0.1;Port=3306;Initial Catalog=agile_config;User ID=chait;Password=123456;Charset=utf8mb4;SslMode=none;Max pool size=50sqliteSqliteData Source=agile_config.db;Version=3;UseUTF16Encoding=True;Password=123456;Pooling=true;FailIfMissing=false;Max Pool Size=50npgsqlPostgreSQLServer=127.0.0.1;Port=5432;Database=agile_config;Username=chait;Password=123456;Pooling=true;Maximum Pool Size=50oracleOracleData Source=//127.0.0.1:1521/XE;User Id=chait;Password=123456;Pooling=true;Max Pool Size=50Docker 部署服务端docker run 命令运行服务端拉取 agile_config 镜像docker pull kklldog/agile_config:latest运行 agile_config 容器sudo docker run \
--name agile_config \
-e TZ=Asia/Shanghai \
-e adminConsole=true \
-e db:provider=sqlite \
-e db:conn="Data Source=agile_config.db;Version=3;UseUTF16Encoding=True;Password=123456;Pooling=true;FailIfMissing=false;Max Pool Size=50" \
-p 15000:5000 \
-v /agileconfig:/app/db \
-d \
kklldog/agile_config:latest参数说明:name:容器名,指定个容器名。TZ:指定时区。adminConsole:配置程序是否使用管理控制台。如果为 true 则启用控制台功能,访问该实例会出现管理界面。每个实例都可以选择使用管理界面,共用一套数据源只是呈现端口不同。默认账号为 admin,首次登录需要设置密码,设置后多个管理界面都可以通用。db:provider:配置程序的数据库类型。目前程序支持:sqlite,mysql,sqlserver,npgsql, oracle 五种数据库。按照项目中允许的数据库使用即可。首个节点启动后会创建数据表(相当好~)。db:env:{env}:provider,可以指定特定环境下使用某个数据库,如【db:env:PROD.provider=sqlserver, db:env:DEVELOPMENT.provider=mysql】。db:conn:配置数据库连接串。按照不同的数据库类型设置不同的数据库连接字符串。数据库使用第二步创建的库。 默认内置了 DEV, TEST, STAGING, PROD 四个常用的环境,如不够,可直接操作 agc_setting 表,增加自定义环境。db:env:{env}:conn,可以指定特定环境下使用某个数据库,如 【db:env:PROD.conn=xxx, db:env:DEVELOPMENT.conn=xxx】。p:指定对外端口,用户客户端去连接。设置允许使用的对外端口即可。v:节点的数据卷挂载。此处挂载到第一步设置的文件夹路径下,可按实际需要设置挂载路径或是不设置 -v 参数也行。-d:设置容器后台运行。通过 docker 建立一个 agile_config 容器实例,其中有 3 个环境变量需要配置:adminConsole 配置程序是否为管理控制台。如果为 true 则启用控制台功能,访问该实例会出现管理界面。db:provider 配置程序的数据库类型。目前程序支持:sqlite,mysql,sqlserver,npgsql,oracle 五种数据库。db:conn 配置数据库连接串。docker compose 运行服务端除了上面的 docker run 的方式运行 agile_config 容器实例,还可以使用 docker compose 来快速创建,此处以 postgresql 数据库为例,编写 yaml 文件如下:version: '3'
services:
agile_config_admin:
image: "kklldog/agile_config:latest"
ports:
- "15000:5000"
networks:
- net0
volumes:
- /etc/localtime:/etc/localtime
environment:
- TZ=Asia/Shanghai
- adminConsole=true
- nodes=agile_config_admin:5000,agile_config_node1:5000,agile_config_node2:5000
- db:provider=npgsql
- db:conn= Server=Server=127.0.0.1;Port=5432;Database=agile_config;Username=chait;Password=123456;Pooling=true;Maximum Pool Size=50
agile_config_node1:
image: "kklldog/agile_config:latest"
ports:
- "15001:5000"
networks:
- net0
volumes:
- /etc/localtime:/etc/localtime
environment:
- TZ=Asia/Shanghai
- db:provider=npgsql
- db:conn= Server=127.0.0.1;Port=5432;Database=agile_config;Username=chait;Password=123456;Pooling=true;Maximum Pool Size=50
depends_on:
- agile_config_admin
agile_config_node2:
image: "kklldog/agile_config:latest"
ports:
- "15002:5000"
networks:
- net0
volumes:
- /etc/localtime:/etc/localtime
environment:
- TZ=Asia/Shanghai
- db:provider=npgsql
- db:conn= Server=127.0.0.1;Port=5432;Database=agile_config;Username=chait;Password=123456;Pooling=true;Maximum Pool Size=50
depends_on:
- agile_config_admin
networks:
net0:注意:如果通过 IIS 或者别的方式部署,请自行从主页上的 releases 页面下载最新的部署包。如果自己使用源码编译,请先编译 react-ui-antd 项目把 dist 内的产物复制到 apisite 项目的 wwwroot/ui 目录下。浏览器查看 AgileConfig 管理界面浏览器输入如下地址:http://localhost:15000/ui#/user/login
http://localhost:15000/ui#/home看到如下界面,说明 AgileConfig 服务端已经部署成功。注意:第一次运行程序需要初始化管理员密码。AgileConfig 管理界面介绍菜单说明菜单说明首页显示一些相关指标的统计数据。节点AgileConfig 支持多节点部署,所有的节点都是平行的。为了简化部署,AgileConfig 并没有单独的控制台程序,请直接使用任意一个节点作为控制台。当环境变量 adminConsole=true 时,该节点同时兼备数据节点跟控制台功能。为了控制台能够管理节点,所以需要在控制台配置节点的信息。注意:即使是作为控制台的数据节点同样需要添加到管理程序,以便管理它。应用AgileConfig 支持多应用程序接入。需要为每个应用程序配置名称、ID、秘钥等信息。每个应用可以设置是否可以被继承,可以被继承的应用类似 apollo 的公共 namespace 的概念。公共的配置可以提取到可继承应用中,其它应用只要继承它就可以获得所有配置。如果子应用跟被继承应用之间的配置键发生重复,子应用的配置会覆盖被继承的应用的配置。子应用可以继承多个应用,如果多个应用之间发生重复键,按照继承的顺序,后继承的应用的配置覆盖前面的应用。配置项配置完应用信息后可以为每个应用配置配置项。配置项支持分组。新添加的配置并不会被客户端感知到,需要手工点击“上线”才会推送给客户端。已上线的配置如果发生修改、删除、回滚操作,会实时推送给客户端。版本历史记录了配置的历史信息,可以回滚至任意版本。客户端控制台可以查看已连接的客户端。日志系统日志记录了 AgileConfig 生产中的一些关键信息。用户可以添加管理配置中心的用户人员信息,用户组,角色。AgileConfig 客户端使用查看 nuget 包 =》https://www.nuget.org/packages/AgileConfig.Client/新建 asp.net core webapi 项目,并添加 nuget 包【AgileConfig.Client】新建项目 WebApplication.AgileConfig,项目结构如下:注意:chait.agileconfig.client.configs.cache 该文件是本地缓存文件,运行项目后生成。在新建项目添加 nuget 包 AgileConfig.Client。Install-Package AgileConfig.Client -Version 1.6.1修改 appsettings.json/appsettings.Development.json 配置文件,添加 AgileConfig 配置。{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
//agile_config
"AgileConfig": {
"appId": "chait",
"secret": "xxx",
"nodes": "http://192.168.10.251:15000,http://192.168.10.251:15001,http://192.168.10.251:15002", //多个节点使用逗号分隔,
"name": "client_name",
"tag": "tag1",
"env": "DEV",
"httpTimeout": "100",
"cache": {
"directory": "Config"
}
}
}agile_config 配置项说明:修改 Program.cs 文件,在 Program.cs 设置使用 AgileConfig,如此增加环境后,AgileConfigProvider 便会从相应环境 appsettings.json 中读取上述配置。在 appsettings.json 文件(默认根文件)配置 agileconfig 的配置信息。 public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseAgileConfig(e => Console.WriteLine($"configs {e.Action}"))
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});使用 UseAgileConfig 扩展方法设置一个配置源。public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseAgileConfig(new ConfigClient($"Config\\appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json"), e => Console.WriteLine($"Action={e.Action},Key={e.Key}"))
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});使用 ConfigureAppConfiguration 扩展方法设置一个配置源。public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.Sources.Clear();
// 获取宿主机环境变量
var env = hostingContext.HostingEnvironment;
//Console.WriteLine($"HostingEnvironment:ApplicationName={env.ApplicationName},EnvironmentName={env.EnvironmentName},ContentRootPath={env.ContentRootPath}");
//string basePath = Path.Join(AppContext.BaseDirectory, "Config");
//string basePath = Path.Join(Directory.GetCurrentDirectory(), "Config");
string basePath = Path.Join(env.ContentRootPath, "Config");
// 设置 json 配置文件路径
config.SetBasePath(basePath: basePath)
.AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile(path: $"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
if (args != null)
{
config.AddCommandLine(args);
}
string configClientPath = string.Empty;
if (env.IsProduction())
{
configClientPath = $"{basePath}\\appsettings.json";
}
else
{
// 指定参数获取环境变量名称,等效于 env.EnvironmentName
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
configClientPath = $"{basePath}\\appsettings.{environmentName}.json";
}
// new一个client实例,传参指定本地 appsettings.json 文件路径读取配置
var configClient = new ConfigClient(configClientPath);
// 使用 AddAgileConfig 配置一个新的 IConfigurationSource 注册项,并输出修改事件信息
config.AddAgileConfig(configClient, e => Console.WriteLine($"Action={e.Action},Key={e.Key}"));
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});注意上面的 2、3 两种扩展方法是等效的,UseAgileConfig 扩展方法内部会设置 basePath。在 Startup.cs 中添加服务services.AddAgileConfig();从 AgileConfig 配置中心读取客户端项目配置信息AgileConfig 支持以下方式读取配置信息:支持 asp.net core 标准的 IConfigurationIOptions 模式读取配置AgileConfigClient 实例直接读取IConfigClient 模式读取配置新建 HomeController 文件,此处以为 IConfiguration,IConfigClient 为例,编写如下代码:using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace WebApplication.AgileConfig.Controllers;
[Route("api/[controller]")]
[ApiController]
public class HomeController : ControllerBase
{
private readonly ILogger<HomeController> _logger;
private readonly IConfiguration _configuration;
private readonly IConfigClient _configClient;
public HomeController(ILogger<HomeController> logger, IConfiguration configuration, IConfigClient _configClient)
{
_logger = logger;
_configuration = configuration;
_configClient = configClient;
}
/// <summary>
/// 使用 IConfiguration 读取配置
/// </summary>
/// <returns></returns>
[HttpGet(ByIConfiguration)]
public IActionResult ByIConfiguration()
{
string aaa = _configuration["Abc:aaa"];
string info = $"Abc.aaa={aaa}";
_logger.LogInformation(info);
return Ok(info);
}
/// <summary>
/// 使用 IConfigClient 读取配置
/// </summary>
/// <returns></returns>
[HttpGet("ByIConfigClient")]
public IActionResult ByIConfigClient()
{
var aaa = _configClient["Abc:aaa"];
string info = $"Abc.aaa={aaa}";
_logger.LogInformation(info);
foreach (var item in _configClient.Data)
{
Console.WriteLine($"{item.Key} = {item.Value}");
}
return Ok(info);
}
}到此处客户端测试程序已经准备就绪,接下来在 AgileConfig 管理页面【应用】添加相关配置信息。AgileConfig 配置中心读取配置的优先级如果在 AgileConfig 中有则默认从那取值,没有再去找机密文件,再去找 appsettings.{env}.json,最后 appsettings.json,注意优先级最高的还是环境变量和命令行的配置。https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-6.0#default-configurationhttps://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/?view=aspnetcore-6.0#default-configurationAgileConfig 配置中心添加客户端项目配置信息选择 AgileConfig 管理页面 =》【应用】,新建客户端应用的配置信息,注意和上面的 appsettings.json 文件里面的配置保持一致。如下所示:点击【配置项】=》【新建】按钮,即可添加配置信息。从上图中可以看到, AgileConfig 配置中心默认提供 4 个环境变量:DEV、TEST、STAGING、PROD,可以新增 key-value 键值对、json 结构数据和 text 文本信息配置。新建配置信息:新建配置信息后,页面显示如下:点击【发布】即可让新增的配置信息生效。项目测试dotnet cli 启动项目使用 dotnet cli 启动 WebApplication.AgileConfig 项目,输出如下信息:Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows
PS C:\Users\sws-dev-server\Desktop\dapr-demo\dapr-demo\WebApplication.AgileConfig> dotnet run
欢迎使用 .NET 6.0!
---------------------
SDK 版本: 6.0.300
遥测
---------
.NET 工具会收集用法数据,帮助我们改善你的体验。它由 Microsoft 收集并与社区共享。你可通过使用喜欢的 shell 将 DOTNET_CLI_TELEMETRY_OPTOUT 环境变量设置为 "1" 或 "true" 来选择退出遥测。
阅读有关 .NET CLI 工具遥测的更多信息: https://aka.ms/dotnet-cli-telemetry
----------------
已安装 ASP.NET Core HTTPS 开发证书。
若要信任该证书,请运行 "dotnet dev-certs https --trust" (仅限 Windows 和 macOS)。
了解 HTTPS: https://aka.ms/dotnet-https
----------------
编写你的第一个应用: https://aka.ms/dotnet-hello-world
查找新增功能: https://aka.ms/dotnet-whats-new
浏览文档: https://aka.ms/dotnet-docs
在 GitHub 上报告问题和查找源: https://github.com/dotnet/core
使用 "dotnet --help" 查看可用命令或访问: https://aka.ms/dotnet-cli
--------------------------------------------------------------------------------------
正在生成...
trce: AgileConfig.Client.ConfigClient[0]
client try connect to server ws://192.168.10.251:15001/ws?client_name=client_name&client_tag=tag1
trce: AgileConfig.Client.ConfigClient[0]
client connect server ws://192.168.10.251:15001/ws?client_name=client_name&client_tag=tag1 successful .
trce: AgileConfig.Client.ConfigClient[0]
client load all the configs success by API: http://192.168.10.251:15002/api/config/app/chait?env=DEV , try count: 0.
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Users\sws-dev-server\Desktop\dapr-demo\dapr-demo\WebApplication.AgileConfig查看【节点】信息浏览器查看【节点】信息,如下所示:输入 http://localhost:15000/ui#/nodeAPI 接口获取配置信息访问 HomeController 获取配置信息以上就是在 asp.net core webapi 项目中使用 AgileConfig 配置中心的全部过程,欢迎更多的小伙伴使用,基于 .net core 开发的轻量级配置中心,方便好用!参考资料https://github.com/dotnetcore/AgileConfighttps://hub.docker.com/r/kklldog/agile_confighttps://www.cnblogs.com/kklldog/p/agile-config.html
阿里云轻量级服务器和ecs的区别(2023更新)
阿里云服务器ECS和轻量应用服务器有什么区别?云服务器ECS是明星级云服务器,轻量应用服务器可以理解为简化版的云服务器ECS,轻量适用于单机应用,云服务器ECS适用于集群类高可用高容灾应用,阿里云百科来详细说下阿里云轻量应用服务器和云服务器的区别,包括运维区别、镜像系统、使用场景、网络线路等详细说明:阿里云轻量服务器和云服务器ECS区别阿里云服务器分为轻量应用服务器和云服务器ECS,轻量是在云服务器ECS基础上推出的轻量级云服务器,轻量应用服务器是一款可快速搭建且易于管理的轻量级云服务器,提供基于单台服务器的应用部署、安全管理、可视化运维监控等服务,使用门槛低。轻量应用服务器结合WordPress等应用镜像,可以快速搭建所需Web环境。阿里云轻量应用服务器和ECS云服务器ECS云服务器:https://www.aliyun.com/product/ecs轻量应用服务器:https://www.aliyun.com/product/swas云服务器ECS是阿里云明星级云产品,企业上云必备,云服务器ECS和结合负载均衡SLB、云数据库、对象存储OSS等产品搭建集群高可用容灾应用,云服务器ECS相对于轻量应用服务器而言使用门槛更高。这也是为什么已经有了云服务器ECS,却还要推出轻量应用服务器的原因,这是因为并不是所有应用都需要云服务器ECS,像企业官网、流量较少的Web建站应用、程序测试等应用场景,一台轻量应用服务器就可以搞定。下面阿里云百科从轻量应用服务器和云服务器使用场景、适用人群、镜像系统、计费方式、网络带宽等方面来详细对比轻量服务器和云服务器ECS,最后再来说说轻量和ECS的选择方法:区别一:使用场景轻量应用服务器属于简化版的云服务器ECS,适合网站搭建、学习环境测试、Web开发等单机应用,适合轻量级应用使用。云服务器ECS是专业级服务器,一些集群类应用首选云服务器ECS,例如:深度学习、大数据分析、在线游戏及大流量的Web网站应用均可以使用云服务器ECS。例如:阿里云百科需要搭建个人博客网站,可以选择轻量应用服务器,性价比更高,没有必要购买云服务器ECS;如果是企业网站,访问流量较高,可能需要将数据库分离出去,需要多台ECS实例和负载均衡实例,那么肯定要选择云服务器ECS;假设用于深度学习、科学计算等应用场景,轻量应用服务器是肯定不行的,应该选择云服务器ECS中的GPU实例。区别二:适用人群轻量应用服务器个人开发者或学生使用,云服务器ECS适合具备一定开发技术能力的个人或企业用户。区别三:应用镜像阿里云轻量应用服务器官方提供应用镜像,使用应用镜像可以一键搭建所需Web环境,如WordPress、LAMP、LNMP、Node.JS、ASP/.NET、Tomcat、宝塔面板、Typecho、NextCloud、Drupal、phpwind、ECShop、Docker等,如下图:轻量应用服务器应用镜像例如:阿里云百科想要使用云服务器搭建一个WordPress网站,如果选择云服务器ECS的话,需要先为ECS实例安装LAMP或LNMP环境,然后再安装WordPress网站程序;如果使用轻量应用服务器的话,操作系统直接选择WordPress应用镜像即可,网站可以一键上线。区别四:计费方式云服务器ECS支持多种计费模式,如包年包月、按量付费和抢占式实例,使用更自由,如下图:阿里云服务器ECS计费模式例如云服务器ECS的按量付费模式可以结合弹性伸缩AS按需弹性伸缩,在业务高峰增加ECS实例,在业务需求下降自动释放ECS实例。而轻量应用服务器仅支持包年包月计费模式。区别五:公网带宽网络线路区别阿里云服务器ECS公网带宽是独享的,购买云服务器ECS选择带宽的话会分配独立公网IP地址,轻量应用服务器公网IP也是独享的。以按固定带宽计费模式为例,云服务器ECS按固定带宽计费是不限制月流量的,而轻量应用服务器是按套餐售卖的,套餐是有月流量限制的。如下图:轻量应用服务器套餐流量包,可以使用阿里云测速工具aliyunping.com测试一下本地到阿里云服务器各个地域节点的Ping值网络延迟。另外,如果购买中国香港地域,阿里云百科发现香港地域的云服务器ECS网络线路要比轻量服务器线路好,轻量香港节点电信网络要绕道日本NTT网络,而香港ECS云服务器无论是电信、移动还是联通都是通过广东出口出去,电信网络全程走的都是CN2线路(59.43开头的ip)。阿小云的阿里云账号香港节点ECS和轻量均有,感觉香港节点的云服务器ECS网络线路更好一些。可以使用阿里云测速工具aliyunping.com测试一下本地到阿里云服务器各个地域节点的Ping值网络延迟。区别六:运维管理对比阿里云轻量应用服务器支持一站式的域名解析、网站搭建、安全、运维、应用管理等可视化运维,而云服务器ECS的运维管理要更复杂一些。区别七:安全组和防火墙出于安全考虑,阿里云服务器ECS和轻量应用服务器默认只开放了22和3389端口,其他的如网站所需的80、443端口,数据库3306端口等都需要手动设置开启。云服务器ECS是通过安全组来管理的,如下图:阿里云服务器ECS安全组而轻量应用服务器是通过防火墙来操作的,如下图:阿里云轻量应用服务器防火墙区别八:轻量和ECS限制说明相对于云服务器ECS,轻量应用服务器限制更多一些,详细如下:不支持安装虚拟化软件和二次虚拟化。不支持声卡应用。内网连通性上存在一定限制。更多信息,请参见地域与网络连通性。仅支持挂载一个数据盘,且数据盘只能在创建轻量应用服务器时挂载。不支持配置IPv6地址。不支持部署集、资源编排、弹性伸缩、标签和资源组等ECS支持的高级功能。云服务器ECS限制:仅弹性裸金属服务器和超级计算集群支持二次虚拟化,其他规格族不支持安装虚拟化软件和二次虚拟化。另外,也不支持声卡应用。云服务器ECS和轻量服务器怎么选择?阿里云百科详细对比了云服务器ECS和轻量应用服务器的区别,那么如何选择呢?阿小云建议根据实际自身应用情况来选择,例如个人用户或开发者来简单网站、测试环境等,可以选择轻量应用服务器,简单方便并且性价比高;如果是企业级应用,流量较大的网站、游戏应用、深度计算或大数据分析等使用场景,建议首选云服务器ECS这种更为专业级的云服务器。更多区别对比请以官方页面为准:ECS云服务器:https://www.aliyun.com/product/ecs轻量应用服务器:https://www.aliyun.com/product/swas
阿里云轻量级服务器和ecs的区别
阿里云服务器ECS和轻量应用服务器有什么区别?云服务器ECS是明星级云服务器,轻量应用服务器可以理解为简化版的云服务器ECS,轻量适用于单机应用,云服务器ECS适用于集群类高可用高容灾应用,阿里云百科来详细说下阿里云轻量应用服务器和云服务器的区别,包括运维区别、镜像系统、使用场景、网络线路等详细说明:阿里云轻量服务器和云服务器ECS区别阿里云服务器分为轻量应用服务器和云服务器ECS,轻量是在云服务器ECS基础上推出的轻量级云服务器,轻量应用服务器是一款可快速搭建且易于管理的轻量级云服务器,提供基于单台服务器的应用部署、安全管理、可视化运维监控等服务,使用门槛低。轻量应用服务器结合WordPress等应用镜像,可以快速搭建所需Web环境。阿里云轻量应用服务器和ECS云服务器ECS云服务器:https://www.aliyun.com/product/ecs轻量应用服务器:https://www.aliyun.com/product/swas云服务器ECS是阿里云明星级云产品,企业上云必备,云服务器ECS和结合负载均衡SLB、云数据库、对象存储OSS等产品搭建集群高可用容灾应用,云服务器ECS相对于轻量应用服务器而言使用门槛更高。这也是为什么已经有了云服务器ECS,却还要推出轻量应用服务器的原因,这是因为并不是所有应用都需要云服务器ECS,像企业官网、流量较少的Web建站应用、程序测试等应用场景,一台轻量应用服务器就可以搞定。下面阿里云百科从轻量应用服务器和云服务器使用场景、适用人群、镜像系统、计费方式、网络带宽等方面来详细对比轻量服务器和云服务器ECS,最后再来说说轻量和ECS的选择方法:区别一:使用场景轻量应用服务器属于简化版的云服务器ECS,适合网站搭建、学习环境测试、Web开发等单机应用,适合轻量级应用使用。云服务器ECS是专业级服务器,一些集群类应用首选云服务器ECS,例如:深度学习、大数据分析、在线游戏及大流量的Web网站应用均可以使用云服务器ECS。例如:阿里云百科需要搭建个人博客网站,可以选择轻量应用服务器,性价比更高,没有必要购买云服务器ECS;如果是企业网站,访问流量较高,可能需要将数据库分离出去,需要多台ECS实例和负载均衡实例,那么肯定要选择云服务器ECS;假设用于深度学习、科学计算等应用场景,轻量应用服务器是肯定不行的,应该选择云服务器ECS中的GPU实例。区别二:适用人群轻量应用服务器个人开发者或学生使用,云服务器ECS适合具备一定开发技术能力的个人或企业用户。区别三:应用镜像阿里云轻量应用服务器官方提供应用镜像,使用应用镜像可以一键搭建所需Web环境,如WordPress、LAMP、LNMP、Node.JS、ASP/.NET、Tomcat、宝塔面板、Typecho、NextCloud、Drupal、phpwind、ECShop、Docker等,如下图:轻量应用服务器应用镜像例如:阿里云百科想要使用云服务器搭建一个WordPress网站,如果选择云服务器ECS的话,需要先为ECS实例安装LAMP或LNMP环境,然后再安装WordPress网站程序;如果使用轻量应用服务器的话,操作系统直接选择WordPress应用镜像即可,网站可以一键上线。区别四:计费方式云服务器ECS支持多种计费模式,如包年包月、按量付费和抢占式实例,使用更自由,如下图:阿里云服务器ECS计费模式例如云服务器ECS的按量付费模式可以结合弹性伸缩AS按需弹性伸缩,在业务高峰增加ECS实例,在业务需求下降自动释放ECS实例。而轻量应用服务器仅支持包年包月计费模式。区别五:公网带宽网络线路区别阿里云服务器ECS公网带宽是独享的,购买云服务器ECS选择带宽的话会分配独立公网IP地址,轻量应用服务器公网IP也是独享的。以按固定带宽计费模式为例,云服务器ECS按固定带宽计费是不限制月流量的,而轻量应用服务器是按套餐售卖的,套餐是有月流量限制的。如下图:轻量应用服务器套餐流量包,可以使用阿里云测速工具aliyunping.com测试一下本地到阿里云服务器各个地域节点的Ping值网络延迟。另外,如果购买中国香港地域,阿里云百科发现香港地域的云服务器ECS网络线路要比轻量服务器线路好,轻量香港节点电信网络要绕道日本NTT网络,而香港ECS云服务器无论是电信、移动还是联通都是通过广东出口出去,电信网络全程走的都是CN2线路(59.43开头的ip)。阿小云的阿里云账号香港节点ECS和轻量均有,感觉香港节点的云服务器ECS网络线路更好一些。可以使用阿里云测速工具aliyunping.com测试一下本地到阿里云服务器各个地域节点的Ping值网络延迟。区别六:运维管理对比阿里云轻量应用服务器支持一站式的域名解析、网站搭建、安全、运维、应用管理等可视化运维,而云服务器ECS的运维管理要更复杂一些。区别七:安全组和防火墙出于安全考虑,阿里云服务器ECS和轻量应用服务器默认只开放了22和3389端口,其他的如网站所需的80、443端口,数据库3306端口等都需要手动设置开启。云服务器ECS是通过安全组来管理的,如下图:阿里云服务器ECS安全组而轻量应用服务器是通过防火墙来操作的,如下图:阿里云轻量应用服务器防火墙区别八:轻量和ECS限制说明相对于云服务器ECS,轻量应用服务器限制更多一些,详细如下:不支持安装虚拟化软件和二次虚拟化。不支持声卡应用。内网连通性上存在一定限制。更多信息,请参见地域与网络连通性。仅支持挂载一个数据盘,且数据盘只能在创建轻量应用服务器时挂载。不支持配置IPv6地址。不支持部署集、资源编排、弹性伸缩、标签和资源组等ECS支持的高级功能。云服务器ECS限制:仅弹性裸金属服务器和超级计算集群支持二次虚拟化,其他规格族不支持安装虚拟化软件和二次虚拟化。另外,也不支持声卡应用。云服务器ECS和轻量服务器怎么选择?阿里云百科详细对比了云服务器ECS和轻量应用服务器的区别,那么如何选择呢?阿小云建议根据实际自身应用情况来选择,例如个人用户或开发者来简单网站、测试环境等,可以选择轻量应用服务器,简单方便并且性价比高;如果是企业级应用,流量较大的网站、游戏应用、深度计算或大数据分析等使用场景,建议首选云服务器ECS这种更为专业级的云服务器。更多区别对比请以官方页面为准:ECS云服务器:https://www.aliyun.com/product/ecs轻量应用服务器:https://www.aliyun.com/product/swas