2020/01/28, ASP.NET Core 3.1, VS2019,Newtonsoft.Json 12.0.3, Microsoft.AspNetCore.Cryptography.KeyDerivation 3.1.1html
摘要:基於ASP.NET Core 3.1 WebApi搭建後端多層網站架構【2-公共基礎庫】
Snowflake雪花算法ID、Enum枚舉方法擴展、Lambda方法擴展、Json方法封裝git
文章目錄github
此分支項目代碼算法
本章節介紹了MS.Common類庫中一些經常使用公共方法,能夠自行添加本身積累的一些庫json
向MS.Common
類庫中添加包引用:後端
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="3.1.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> </ItemGroup>
其中Microsoft.AspNetCore.Cryptography.KeyDerivation是爲了支持PBKDF2加密方式,這個後文會用到架構
在MS.Common
類庫中新建Extensions文件夾,在其中添加EnumExtension.cs
類:less
using System; using System.ComponentModel; using System.Reflection; namespace MS.Common.Extensions { public static class EnumExtension { /// <summary> /// 根據名稱拿到枚舉 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="itemName"></param> /// <returns></returns> public static T GetEnum<T>(this string itemName) { return (T)Enum.Parse(typeof(T), itemName); } /// <summary> /// 根據枚舉值拿到枚舉 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="itemValue"></param> /// <returns></returns> public static T GetEnum<T>(this int itemValue) { return (T)Enum.Parse(typeof(T), Enum.GetName(typeof(T), itemValue)); } /// <summary> /// 根據枚舉值拿到枚舉名稱 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="itemValue"></param> /// <returns></returns> public static string GetEnumName<T>(this int itemValue) { return Enum.GetName(typeof(T), itemValue); } /// <summary> /// 根據名稱拿到枚舉值 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="itemName"></param> /// <returns></returns> public static int GetEnumValue<T>(this string itemName) { return itemName.GetEnum<T>().GetHashCode(); } /// <summary> /// 枚舉獲取描述 /// </summary> /// <param name="item"></param> /// <returns></returns> public static string GetDescription(this Enum item) { Type type = item.GetType(); MemberInfo[] memInfo = type.GetMember(item.ToString()); if (memInfo != null && memInfo.Length > 0) { object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false); if (attrs != null && attrs.Length > 0) return ((DescriptionAttribute)attrs[0]).Description; } return item.ToString();//若是不存在描述,則返回枚舉名稱 } } }
在Extensions中繼續添加LambdaExtension.cs
類:dom
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace MS.Common.Extensions { //add by yzh 2019/04/26 -用於lambda表達式拼接 public class ParameterRebinder : ExpressionVisitor { private readonly Dictionary<ParameterExpression, ParameterExpression> map; public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map) { this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>(); } public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp) { return new ParameterRebinder(map).Visit(exp); } protected override Expression VisitParameter(ParameterExpression p) { ParameterExpression replacement; if (map.TryGetValue(p, out replacement)) { p = replacement; } return base.VisitParameter(p); } } public static class LambdaExtension { public static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge) { var map = first.Parameters.Select((f, i) => new { f, s = second.Parameters[i] }).ToDictionary(p => p.s, p => p.f); var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body); return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters); } public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) { return first.Compose(second, Expression.And); } public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) { return first.Compose(second, Expression.Or); } } }
原生的Lambda表達式不支持動態拼接表達式條件,有了這個擴展方法,就彌補了這個缺點。ide
在Extensions中添加JsonExtension.cs
類:
using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace MS.Common.Extensions { public static class JsonExtension { public static JsonSerializerSettings jsonSetting = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; /// <summary> /// 序列化對象,默認禁止循環引用 /// </summary> /// <param name="data"></param> /// <returns></returns> public static string ToJsonString(this object data) { return JsonConvert.SerializeObject(data, jsonSetting); } /// <summary> /// 序列化對象 /// </summary> /// <param name="data"></param> /// <param name="timeConverter"></param> /// <returns></returns> public static string ToJsonString(this object data, IsoDateTimeConverter timeConverter) { return JsonConvert.SerializeObject(data, timeConverter); } /// <summary> /// 反序列化字符串 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="data"></param> /// <returns></returns> public static T GetDeserializeObject<T>(this string data) { if (string.IsNullOrWhiteSpace(data)) return default; return JsonConvert.DeserializeObject<T>(data, jsonSetting); } /// <summary> /// 使用序列化和反序列化得到一次深拷貝 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="data"></param> /// <returns></returns> public static T GetMemberwiseCopy<T>(this T data) { return data.ToJsonString().GetDeserializeObject<T>(); } } }
每一個方法都寫好了註釋,默認禁止循環引用
在類庫中添加Security文件夾,向其中添加Crypto.cs
類:
using Microsoft.AspNetCore.Cryptography.KeyDerivation; using System; using System.Runtime.CompilerServices; using System.Security.Cryptography; namespace MS.Common.Security { /// <summary> /// Provides helper methods for hashing/salting and verifying passwords. /// </summary> public static class Crypto { /* ======================= * HASHED PASSWORD FORMATS * ======================= * * Version 3: * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } * (All UInt32s are stored big-endian.) */ private const int PBKDF2IterCount = 10000; private const int PBKDF2SubkeyLength = 256 / 8; // 256 bits private const int SaltSize = 128 / 8; // 128 bits /// <summary> /// Returns a hashed representation of the specified <paramref name="password"/>. /// </summary> /// <param name="password">The password to generate a hash value for.</param> /// <returns>The hash value for <paramref name="password" /> as a base-64-encoded string.</returns> /// <exception cref="System.ArgumentNullException"><paramref name="password" /> is null.</exception> public static string HashPassword(string password) { if (password == null) { throw new ArgumentNullException(nameof(password)); } return HashPasswordInternal(password); } /// <summary> /// Determines whether the specified RFC 2898 hash and password are a cryptographic match. /// </summary> /// <param name="hashedPassword">The previously-computed RFC 2898 hash value as a base-64-encoded string.</param> /// <param name="password">The plaintext password to cryptographically compare with hashedPassword.</param> /// <returns>true if the hash value is a cryptographic match for the password; otherwise, false.</returns> /// <remarks> /// <paramref name="hashedPassword" /> must be of the format of HashPassword (salt + Hash(salt+input). /// </remarks> /// <exception cref="System.ArgumentNullException"> /// <paramref name="hashedPassword" /> or <paramref name="password" /> is null. /// </exception> public static bool VerifyHashedPassword(string hashedPassword, string password) { if (hashedPassword == null) { throw new ArgumentNullException(nameof(hashedPassword)); } if (password == null) { throw new ArgumentNullException(nameof(password)); } return VerifyHashedPasswordInternal(hashedPassword, password); } private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); private static string HashPasswordInternal(string password) { var bytes = HashPasswordInternal(password, KeyDerivationPrf.HMACSHA256, PBKDF2IterCount, SaltSize, PBKDF2SubkeyLength); return Convert.ToBase64String(bytes); } private static byte[] HashPasswordInternal( string password, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) { // Produce a version 3 (see comment above) text hash. var salt = new byte[saltSize]; _rng.GetBytes(salt); var subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); var outputBytes = new byte[13 + salt.Length + subkey.Length]; // Write format marker. outputBytes[0] = 0x01; // Write hashing algorithm version. WriteNetworkByteOrder(outputBytes, 1, (uint)prf); // Write iteration count of the algorithm. WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); // Write size of the salt. WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); // Write the salt. Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); // Write the subkey. Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); return outputBytes; } private static bool VerifyHashedPasswordInternal(string hashedPassword, string password) { var decodedHashedPassword = Convert.FromBase64String(hashedPassword); if (decodedHashedPassword.Length == 0) { return false; } try { // Verify hashing format. if (decodedHashedPassword[0] != 0x01) { // Unknown format header. return false; } // Read hashing algorithm version. var prf = (KeyDerivationPrf)ReadNetworkByteOrder(decodedHashedPassword, 1); // Read iteration count of the algorithm. var iterCount = (int)ReadNetworkByteOrder(decodedHashedPassword, 5); // Read size of the salt. var saltLength = (int)ReadNetworkByteOrder(decodedHashedPassword, 9); // Verify the salt size: >= 128 bits. if (saltLength < 128 / 8) { return false; } // Read the salt. var salt = new byte[saltLength]; Buffer.BlockCopy(decodedHashedPassword, 13, salt, 0, salt.Length); // Verify the subkey length >= 128 bits. var subkeyLength = decodedHashedPassword.Length - 13 - salt.Length; if (subkeyLength < 128 / 8) { return false; } // Read the subkey. var expectedSubkey = new byte[subkeyLength]; Buffer.BlockCopy(decodedHashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); // Hash the given password and verify it against the expected subkey. var actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); return ByteArraysEqual(actualSubkey, expectedSubkey); } catch { // This should never occur except in the case of a malformed payload, where // we might go off the end of the array. Regardless, a malformed payload // implies verification failed. return false; } } private static uint ReadNetworkByteOrder(byte[] buffer, int offset) { return ((uint)(buffer[offset + 0]) << 24) | ((uint)(buffer[offset + 1]) << 16) | ((uint)(buffer[offset + 2]) << 8) | ((uint)(buffer[offset + 3])); } private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) { buffer[offset + 0] = (byte)(value >> 24); buffer[offset + 1] = (byte)(value >> 16); buffer[offset + 2] = (byte)(value >> 8); buffer[offset + 3] = (byte)(value >> 0); } // Compares two byte arrays for equality. // The method is specifically written so that the loop is not optimized. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] private static bool ByteArraysEqual(byte[] a, byte[] b) { if (ReferenceEquals(a, b)) { return true; } if (a == null || b == null || a.Length != b.Length) { return false; } var areSame = true; for (var i = 0; i < a.Length; i++) { areSame &= (a[i] == b[i]); } return areSame; } } }
簡單說明,網站用戶密碼加密就使用該方法,這段代碼是從開源nuget包CryptoHelper中扒下來的。
在類庫中添加IDCode文件夾,在IDCode文件夾中繼續添加Snowflake文件夾,該文件夾下新建三個類:DisposableAction.cs、IdWorker.cs、TimeExtensions.cs
DisposableAction.cs:
using System; namespace MS.Common.IDCode { public class DisposableAction : IDisposable { readonly Action _action; public DisposableAction(Action action) { if (action == null) throw new ArgumentNullException("action"); _action = action; } public void Dispose() { _action(); } } }
IdWorker.cs:
/** Copyright 2010-2012 Twitter, Inc.*/ /** * An object that generates IDs. * This is broken into a separate class in case * we ever want to support multiple worker threads * per process */ using System; namespace MS.Common.IDCode { public class IdWorker { //基準時間 public const long Twepoch = 1288834974657L; //機器標識位數 const int WorkerIdBits = 5; //數據標誌位數 const int DatacenterIdBits = 5; //序列號識位數 const int SequenceBits = 12; //機器ID最大值 const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits); //數據標誌ID最大值 const long MaxDatacenterId = -1L ^ (-1L << DatacenterIdBits); //序列號ID最大值 private const long SequenceMask = -1L ^ (-1L << SequenceBits); //機器ID偏左移12位 private const int WorkerIdShift = SequenceBits; //數據ID偏左移17位 private const int DatacenterIdShift = SequenceBits + WorkerIdBits; //時間毫秒左移22位 public const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits; private long _sequence = 0L; private long _lastTimestamp = -1L; public long WorkerId { get; protected set; } public long DatacenterId { get; protected set; } public long Sequence { get { return _sequence; } internal set { _sequence = value; } } public IdWorker(long workerId, long datacenterId, long sequence = 0L) { // 若是超出範圍就拋出異常 if (workerId > MaxWorkerId || workerId < 0) { throw new ArgumentException(string.Format("worker Id 必須大於0,且不能大於MaxWorkerId: {0}", MaxWorkerId)); } if (datacenterId > MaxDatacenterId || datacenterId < 0) { throw new ArgumentException(string.Format("region Id 必須大於0,且不能大於MaxWorkerId: {0}", MaxDatacenterId)); } //先檢驗再賦值 WorkerId = workerId; DatacenterId = datacenterId; _sequence = sequence; } readonly object _lock = new Object(); public virtual long NextId() { lock (_lock) { var timestamp = TimeGen(); if (timestamp < _lastTimestamp) { throw new Exception(string.Format("時間戳必須大於上一次生成ID的時間戳. 拒絕爲{0}毫秒生成id", _lastTimestamp - timestamp)); } //若是上次生成時間和當前時間相同,在同一毫秒內 if (_lastTimestamp == timestamp) { //sequence自增,和sequenceMask相與一下,去掉高位 _sequence = (_sequence + 1) & SequenceMask; //判斷是否溢出,也就是每毫秒內超過1024,當爲1024時,與sequenceMask相與,sequence就等於0 if (_sequence == 0) { //等待到下一毫秒 timestamp = TilNextMillis(_lastTimestamp); } } else { //若是和上次生成時間不一樣,重置sequence,就是下一毫秒開始,sequence計數從新從0開始累加, //爲了保證尾數隨機性更大一些,最後一位能夠設置一個隨機數 _sequence = 0;//new Random().Next(10); } _lastTimestamp = timestamp; return ((timestamp - Twepoch) << TimestampLeftShift) | (DatacenterId << DatacenterIdShift) | (WorkerId << WorkerIdShift) | _sequence; } } // 防止產生的時間比以前的時間還要小(因爲NTP回撥等問題),保持增量的趨勢. protected virtual long TilNextMillis(long lastTimestamp) { var timestamp = TimeGen(); while (timestamp <= lastTimestamp) { timestamp = TimeGen(); } return timestamp; } // 獲取當前的時間戳 protected virtual long TimeGen() { return TimeExtensions.CurrentTimeMillis(); } } }
TimeExtensions.cs:
using System; namespace MS.Common.IDCode { public static class TimeExtensions { public static Func<long> currentTimeFunc = InternalCurrentTimeMillis; public static long CurrentTimeMillis() { return currentTimeFunc(); } public static IDisposable StubCurrentTime(Func<long> func) { currentTimeFunc = func; return new DisposableAction(() => { currentTimeFunc = InternalCurrentTimeMillis; }); } public static IDisposable StubCurrentTime(long millis) { currentTimeFunc = () => millis; return new DisposableAction(() => { currentTimeFunc = InternalCurrentTimeMillis; }); } private static readonly DateTime Jan1st1970 = new DateTime (1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static long InternalCurrentTimeMillis() { return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds; } } }
說明:這部分代碼是從snowflake-net中扒來的,使用方法readme裏也有,注意應儘可能保證全局單例的狀況下使用該方法生成ID
項目完成後,以下圖所示