├── Anet ├── AnetOptions.cs ├── Entity │ ├── IEntity.cs │ ├── Entity.cs │ ├── IAuditEntity.cs │ ├── IFullAuditEntity.cs │ ├── FullAuditEntity.cs │ ├── AuditEntity.cs │ └── ColumnAttributes.cs ├── Extensions │ ├── DateTimeExtensions.cs │ ├── LinqExtensions.cs │ ├── EnumExtenstions.cs │ ├── TypeExtensions.cs │ └── StringExtensions.cs ├── Security │ ├── HashUtil.cs │ ├── MD5.cs │ ├── SHA1.cs │ ├── AES.cs │ └── PasswordHasher.cs ├── Models │ ├── TypedValue.cs │ ├── PagedResult.cs │ └── SelectOption.cs ├── Anet.csproj ├── Atrributes │ └── DisplayAttribute.cs ├── Utilities │ ├── BitUtil.cs │ ├── RadixUtil.cs │ ├── EnumUtil.cs │ ├── JsonUtil.cs │ └── StringUtil.cs ├── IdGenOptions.cs ├── IdGen.Static.cs ├── ServicesExtensions.cs ├── Guard.cs ├── Error.cs └── IdGen.cs ├── Anet.Web ├── AppBuilderExtensions.cs ├── IApiResult.cs ├── Jwt │ ├── GrantTypes.cs │ ├── NoopAuthenticator.cs │ ├── IRefreshTokenStore.cs │ ├── JwtResult.cs │ ├── IAuthenticator.cs │ ├── JwtParams.cs │ ├── AuthenticateResult.cs │ ├── DefaultRefreshTokenStore.cs │ ├── JwtOptions.cs │ ├── JwtProvider.cs │ └── JwtProviderMiddleware.cs ├── ContentTypes.cs ├── Anet.Web.csproj ├── ApiResult.cs ├── ApiResponseAttribute.cs └── ServicesExtensions.cs ├── .gitignore ├── Tests ├── UtilityTests.cs └── Tests.csproj ├── README.md ├── share.props ├── LICENSE ├── Anet.sln ├── .gitattributes └── .editorconfig /Anet/AnetOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Anet; 2 | 3 | public class AnetOptions 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /Anet.Web/AppBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Builder; 2 | 3 | public static class AppBuilderExtensions 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /Anet.Web/IApiResult.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web; 2 | 3 | public interface IApiResult 4 | { 5 | public int Code { get; set; } 6 | public string Message { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | .vs/ 4 | .vscode/ 5 | bin/ 6 | obj/ 7 | _Resharper* 8 | NuGet.exe 9 | *.nupkg 10 | .nupkgs/ 11 | *.lock.json 12 | *.coverage 13 | TestResults/ 14 | .dotnet/* -------------------------------------------------------------------------------- /Anet/Entity/IEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Entity; 2 | 3 | public interface IEntity 4 | { 5 | TKey Id { get; set; } 6 | } 7 | 8 | public interface IEntity : IEntity 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/GrantTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web.Jwt; 2 | 3 | public struct GrantTypes 4 | { 5 | public const string PASSWORD = "password"; 6 | public const string REFRESH_TOKEN = "refresh_token"; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /Anet.Web/ContentTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web; 2 | 3 | public struct ContentTypes 4 | { 5 | public const string JsonContentType = "application/json"; 6 | public const string FormUrlencodedContentType = "application/x-www-form-urlencoded"; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /Anet/Extensions/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace System; 2 | 3 | public static class DateTimeExtensions 4 | { 5 | public static long ToTimestamp(this DateTime time) 6 | { 7 | return ((DateTimeOffset)time).ToUnixTimeSeconds(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Anet/Entity/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Entity; 2 | 3 | public abstract class Entity : IEntity 4 | where TKey : IEquatable 5 | { 6 | public virtual TKey Id { get; set; } 7 | } 8 | 9 | public abstract class Entity : Entity, IEntity 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/NoopAuthenticator.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web.Jwt; 2 | 3 | public class NoopAuthenticator : IAuthenticator 4 | { 5 | public Task AuthenticateAsync(JwtParams jwtParams) 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /Anet/Extensions/LinqExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace System.Linq; 2 | 3 | public static class LinqExtensions 4 | { 5 | public static IQueryable Paged(this IQueryable query, int page, int size) 6 | { 7 | return query.Skip((page - 1) * size).Take(size); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/IRefreshTokenStore.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web.Jwt; 2 | 3 | public interface IRefreshTokenStore 4 | { 5 | Task SaveTokenAsync(JwtResult jwtResult); 6 | 7 | Task GetTokenAsync(string refreshToken); 8 | 9 | Task DeleteTokenAsync(string refreshToken); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/JwtResult.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web.Jwt; 2 | 3 | public class JwtResult 4 | { 5 | public string AccessToken { get; set; } 6 | 7 | public long ExpiresAt { get; set; } 8 | 9 | public string RefreshToken { get; set; } 10 | 11 | public object UserInfo { get; set; } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /Anet/Entity/IAuditEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Entity; 2 | 3 | public interface IAuditEntity : IEntity 4 | where TKey : IEquatable 5 | { 6 | DateTime CreatedAt { get; set; } 7 | DateTime UpdatedAt { get; set; } 8 | } 9 | 10 | public interface IAuditEntity : IAuditEntity 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/IAuthenticator.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web.Jwt; 2 | 3 | public interface IAuthenticator 4 | { 5 | /// 6 | /// JWT 授权验证 7 | /// 8 | /// 请求参数 9 | /// 验证结果 10 | Task AuthenticateAsync(JwtParams jwtParams); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Anet/Entity/IFullAuditEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Entity; 2 | 3 | public interface IFullAuditEntity : IAuditEntity 4 | where TKey : IEquatable 5 | { 6 | TKey CreatedBy { get; set; } 7 | TKey UpdatedBy { get; set; } 8 | } 9 | 10 | public interface IFullAuditEntity : IFullAuditEntity, IAuditEntity 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/JwtParams.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web.Jwt; 2 | 3 | public class JwtParams 4 | { 5 | public string Username { get; set; } 6 | 7 | public string Password { get; set; } 8 | 9 | public string GrantType { get; set; } = GrantTypes.PASSWORD; 10 | 11 | public string RefreshToken { get; set; } 12 | 13 | public Dictionary Payload { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Anet/Security/HashUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Anet.Security; 4 | 5 | public static class HashUtil 6 | { 7 | public static string BytesToString(byte[] bytes) 8 | { 9 | var sb = new StringBuilder(); 10 | foreach (byte b in bytes) 11 | { 12 | sb.Append(b.ToString("x2")); 13 | } 14 | return sb.ToString(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Anet/Models/TypedValue.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Models; 2 | 3 | public class TypedValue 4 | { 5 | public TypedValue() 6 | { 7 | } 8 | 9 | public TypedValue(T value) 10 | { 11 | Value = value; 12 | } 13 | public T Value { get; set; } 14 | } 15 | 16 | public class TypedValue : TypedValue 17 | { 18 | public TypedValue() 19 | { 20 | } 21 | public TypedValue(string value) : base(value) 22 | { 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Anet/Entity/FullAuditEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Entity; 2 | 3 | public abstract class FullAuditEntity : AuditEntity, IFullAuditEntity 4 | where TKey : IEquatable 5 | { 6 | public virtual TKey CreatedBy { get; set; } 7 | public virtual TKey UpdatedBy { get; set; } 8 | } 9 | 10 | public abstract class FullAuditEntity : AuditEntity, IFullAuditEntity 11 | { 12 | public virtual long CreatedBy { get; set; } 13 | public virtual long UpdatedBy { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/UtilityTests.cs: -------------------------------------------------------------------------------- 1 | using Anet.Atrributes; 2 | using Anet.Utilities; 3 | 4 | namespace Tests 5 | { 6 | public class UtilityTests 7 | { 8 | [Fact] 9 | public void EnumUtil_GetSelectOptions_Test() 10 | { 11 | var options = EnumUtil.GetSelectOptions(); 12 | 13 | Assert.Equal(2, options.Count()); 14 | } 15 | } 16 | 17 | public enum Enum1 18 | { 19 | [Display("Foo display text")] Foo, 20 | [Display("Bar display text")] Bar 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Anet/Anet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Anet 7 | True 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Anet/Atrributes/DisplayAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Atrributes; 2 | 3 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] 4 | public class DisplayAttribute : Attribute 5 | { 6 | public DisplayAttribute() 7 | { 8 | } 9 | public DisplayAttribute(string name) 10 | { 11 | Name = name; 12 | } 13 | public string Name { get; set; } 14 | public int Order { get; set; } 15 | public bool Visible { get; set; } = true; 16 | public string Group { get; set; } 17 | public string Description { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /Anet/Models/PagedResult.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Models; 2 | 3 | public class PagedResult 4 | { 5 | public PagedResult() 6 | { 7 | } 8 | 9 | public PagedResult(int page, int size) 10 | { 11 | Page = page; 12 | Size = size; 13 | } 14 | 15 | public int Page { get; set; } 16 | public int Size { get; set; } 17 | public int Total { get; set; } 18 | public IEnumerable Items { get; set; } = new List(); 19 | public T Summary { get; set; } 20 | 21 | public int TotalPages => Size == 0 ? 0 : (int)Math.Ceiling((decimal)Total / Size); 22 | } 23 | -------------------------------------------------------------------------------- /Anet/Entity/AuditEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Entity; 2 | 3 | public abstract class AuditEntity : Entity, IAuditEntity 4 | where TKey : IEquatable 5 | { 6 | [Datetime] 7 | public virtual DateTime CreatedAt { get; set; } = DateTime.Now; 8 | [Datetime] 9 | public virtual DateTime UpdatedAt { get; set; } = DateTime.Now; 10 | } 11 | 12 | public abstract class AuditEntity : Entity, IAuditEntity 13 | { 14 | [Datetime] 15 | public virtual DateTime CreatedAt { get; set; } = DateTime.Now; 16 | [Datetime] 17 | public virtual DateTime UpdatedAt { get; set; } = DateTime.Now; 18 | } 19 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/AuthenticateResult.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Anet.Web.Jwt; 4 | 5 | public class AuthenticateResult 6 | { 7 | /// 8 | /// JWT 验证是否通过 9 | /// 10 | public bool Success { get; set; } 11 | /// 12 | /// 返回消息 13 | /// 14 | public string Message { get; set; } 15 | /// 16 | /// 验证通过后的 Claim 信息 17 | /// 18 | public IList Claims { get; set; } = new List(); 19 | /// 20 | /// 用户信息 21 | /// 22 | public object UserInfo { get; set; } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Anet.Web/Anet.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Anet.Web 7 | True 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Anet/Utilities/BitUtil.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Utilities; 2 | 3 | public static class BitUtil 4 | { 5 | /// 6 | /// 获取指定长度二进制的最大整型数。例如:5 返回 000..011111。 7 | /// 8 | /// 9 | /// 10 | public static long GetMaxOfBits(byte bits) 11 | { 12 | return (1L << bits) - 1; // 或 -1 ^ -1 << bits 13 | } 14 | 15 | /// 16 | /// 获取数字的二进制位长度。例如 5 的位长度是 3。 17 | /// 18 | /// 19 | /// 20 | public static int GetBitsLength(long number) 21 | { 22 | return (int)Math.Log(number, 2) + 1; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Anet/IdGenOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Anet; 2 | 3 | public class IdGenOptions 4 | { 5 | public const byte DefaultMachineIdBits = 6; 6 | public const byte DefaultSequenceBits = 12; 7 | 8 | /// 9 | /// 当前机器码(唯一机会编号) 10 | /// 11 | public uint MachineId { get; set; } 12 | 13 | /// 14 | /// 机器码位数(0-10之间) 15 | /// 16 | public byte MachineIdBits { get; set; } = DefaultMachineIdBits; 17 | 18 | /// 19 | /// 序列号位数(值在0-20之间) 20 | /// 注意: 21 | /// 1. 并发量越大,此值也要越大,例如:10 可以 1 秒内生成 2^10=1024 个 ID。 22 | /// 2. 每台机器此参数务必相同。 23 | /// 24 | public byte SequenceBits { get; set; } = DefaultSequenceBits; 25 | } 26 | -------------------------------------------------------------------------------- /Anet.Web/ApiResult.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace Anet.Web; 4 | 5 | public class ApiResult : IApiResult 6 | { 7 | public ApiResult() { } 8 | 9 | public virtual int Code { get; set; } 10 | 11 | public virtual string Message { get; set; } 12 | 13 | public virtual T Data { get; set; } 14 | } 15 | 16 | public class ApiResult : ApiResult 17 | { 18 | public static ApiResult Success(object data = default) 19 | { 20 | return new ApiResult { Data = data }; 21 | } 22 | 23 | public static ApiResult Error(string message, HttpStatusCode code = HttpStatusCode.BadRequest) 24 | { 25 | return new ApiResult { Message = message, Code = (ushort)code }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Anet/Security/MD5.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Hasher = System.Security.Cryptography.MD5; 4 | 5 | namespace Anet.Security; 6 | 7 | public static class MD5 8 | { 9 | public static string Hash(string input) 10 | { 11 | var result = Hasher.HashData(Encoding.UTF8.GetBytes(input)); 12 | return HashUtil.BytesToString(result); 13 | } 14 | 15 | public static string Hash(byte[] buffer) 16 | { 17 | var result = Hasher.HashData(buffer); 18 | return HashUtil.BytesToString(result); 19 | } 20 | 21 | public static string Hash(Stream stream) 22 | { 23 | using var md5 = Hasher.Create(); 24 | byte[] result = md5.ComputeHash(stream); 25 | return HashUtil.BytesToString(result); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Anet/Security/SHA1.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Hasher = System.Security.Cryptography.SHA1; 4 | 5 | namespace Anet.Security; 6 | 7 | public static class SHA1 8 | { 9 | public static string Hash(string input) 10 | { 11 | var result = Hasher.HashData(Encoding.UTF8.GetBytes(input)); 12 | return HashUtil.BytesToString(result); 13 | } 14 | 15 | public static string Hash(byte[] buffer) 16 | { 17 | var result = Hasher.HashData(buffer); 18 | return HashUtil.BytesToString(result); 19 | } 20 | 21 | public static string Hash(Stream stream) 22 | { 23 | using var sha1 = Hasher.Create(); 24 | byte[] result = sha1.ComputeHash(stream); 25 | return HashUtil.BytesToString(result); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Anet/IdGen.Static.cs: -------------------------------------------------------------------------------- 1 | namespace Anet; 2 | 3 | public partial class IdGen 4 | { 5 | private static IdGen _instance; 6 | 7 | internal static void SetDefaultOptions(Action configure) 8 | { 9 | if (_instance != null) 10 | throw new InvalidOperationException("Can't set machine id twice."); 11 | 12 | var options = new IdGenOptions(); 13 | configure?.Invoke(options); 14 | 15 | _instance = new IdGen(options); 16 | } 17 | 18 | /// 19 | /// Generate a new sequence id. 20 | /// 21 | /// The generated id. 22 | public static long NewId() 23 | { 24 | if (_instance == null) 25 | throw new Exception("The IdGen has no default instance."); 26 | return _instance.NewSequenceId(); 27 | } 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 什么是 Anet 2 | 3 | Anet 是一个 .NET 通用库,适用于 .NET 6 及以上版本。 4 | 5 | > A .NET Core Common Lib, Framework and Boilerplate. 6 | 7 | 它的取名正是来自于这个定义的前面四个字母:ANET。Anet 的宗旨是使 .NET 项目开发变得简单和快速。 8 | 9 | ## Anet 的使用 10 | 11 | TODO... 12 | 13 | ## Anet 的目前状态 14 | 15 | Anet 正在重构中,请勿用于生产环境! 16 | 17 | ## 贡献者 18 | 19 | Thanks goes to these wonderful people: 20 | 21 | | [
Liam Wang](https://github.com/liamwang) | [
Zae](https://github.com/Zaeworks) | 22 | | :---------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------: | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Anet/Models/SelectOption.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Models; 2 | 3 | public class SelectOption : SelectOption 4 | { 5 | public SelectOption() { } 6 | public SelectOption(long value, string label) : base(value, label) { } 7 | 8 | public new List Children { get; set; } 9 | } 10 | 11 | public class SelectOption 12 | where TValue : IEquatable 13 | { 14 | public SelectOption() 15 | { 16 | } 17 | public SelectOption(TValue value, string label) 18 | { 19 | Value = value; 20 | Label = label; 21 | } 22 | 23 | public TValue Value { get; set; } 24 | public string Label { get; set; } 25 | public string Name { get; set; } 26 | public int Order { get; set; } 27 | public string Group { get; set; } 28 | public bool Checked { get; set; } 29 | 30 | public List> Children { get; set; } 31 | } 32 | -------------------------------------------------------------------------------- /Anet/Extensions/EnumExtenstions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Anet.Atrributes; 3 | using Anet.Models; 4 | 5 | namespace System; 6 | 7 | public static class EnumEx 8 | { 9 | public static T GetCustomAttribute(this Enum value) where T : Attribute 10 | { 11 | var field = value.GetType().GetField(value.ToString()); 12 | return field == null ? default : field.GetCustomAttribute(); 13 | } 14 | 15 | public static string GetDisplayName(this Enum value) 16 | { 17 | var attribute = value.GetCustomAttribute(); 18 | return attribute == null ? value.ToString() : attribute.Name; 19 | } 20 | 21 | public static string GetDisplayDescription(this Enum value) 22 | { 23 | var attribute = value.GetCustomAttribute(); 24 | return attribute == null ? value.ToString() : attribute.Description; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Anet/Utilities/RadixUtil.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Utilities; 2 | 3 | public class RadixUtil 4 | { 5 | public const string BASE64 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"; 6 | 7 | public static string Encode(long number, int radix) 8 | { 9 | var stack = new Stack(); 10 | while (number >= radix) 11 | { 12 | var remainder = (byte)(number % radix); 13 | stack.Push(BASE64[remainder]); 14 | number /= radix; 15 | } 16 | stack.Push(BASE64[(byte)number]); 17 | return new string(stack.ToArray()); 18 | } 19 | 20 | public static long Decode(string text, int radix) 21 | { 22 | long result = 0; 23 | for (int i = 0; i < text.Length; i++) 24 | { 25 | result += (long)Math.Pow(radix, text.Length - i - 1) * BASE64.IndexOf(text[i]); 26 | } 27 | return result; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Anet/ServicesExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Anet; 3 | using Anet.Utilities; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection; 6 | 7 | public static class ServicesExtensions 8 | { 9 | /// 10 | /// Adds Anet services to the specified . 11 | /// 12 | /// The so that additional calls can be chained. 13 | public static IServiceCollection AddAnet(this IServiceCollection services, 14 | Action configureAnet = null, 15 | Action configureIdGen = null, 16 | Action configureJsonUtil = null) 17 | { 18 | var options = new AnetOptions(); 19 | configureAnet?.Invoke(options); 20 | 21 | IdGen.SetDefaultOptions(configureIdGen); 22 | JsonUtil.SetDefaultSerializerOptions(configureJsonUtil); 23 | 24 | return services; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /share.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8.1.0 4 | net8.0 5 | enable 6 | dotnet,aspnet,common,lib,framework,boilerplate 7 | A .NET Common Lib, Framework and Boilerplate. 8 | liamwang 9 | https://github.com/anet-lib/anet 10 | MIT 11 | true 12 | https://github.com/anet-lib/anet 13 | 14 | 15 | 16 | 1701;1702;1591; 17 | 18 | 19 | 20 | 1701;1702;1591; 21 | 22 | -------------------------------------------------------------------------------- /Anet/Utilities/EnumUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Anet.Atrributes; 3 | using Anet.Models; 4 | 5 | namespace Anet.Utilities; 6 | 7 | public class EnumUtil 8 | { 9 | public static IEnumerable GetSelectOptions(bool includeVisible = false) 10 | where TEnum : struct, Enum 11 | { 12 | var type = typeof(TEnum); 13 | foreach (var name in Enum.GetNames()) 14 | { 15 | var member = type.GetMember(name); 16 | var display = member[0].GetCustomAttribute(); 17 | 18 | if (display == null || (!display.Visible && !includeVisible)) continue; 19 | 20 | yield return new SelectOption() 21 | { 22 | Value = Convert.ToInt64(Enum.Parse(type, name)), 23 | Name = name, 24 | Label = display.Name ?? name, 25 | Order = display.Order, 26 | Group = display.Group 27 | }; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Anet/Guard.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Anet; 4 | 5 | public static class Guard 6 | { 7 | public static void NotNull(object argument, [CallerArgumentExpression(nameof(argument))] string paramName = null) 8 | { 9 | if (argument is null) 10 | { 11 | throw new ArgumentNullException(paramName); 12 | } 13 | } 14 | 15 | public static void NotNullOrEmpty(string argument, [CallerArgumentExpression(nameof(argument))] string paramName = null) 16 | { 17 | NotNull(argument, paramName); 18 | if (argument == string.Empty) 19 | { 20 | throw new ArgumentException("The argument can not be empty.", paramName); 21 | } 22 | } 23 | 24 | public static void NotNullOrEmpty(IEnumerable argument, [CallerArgumentExpression(nameof(argument))] string paramName = null) 25 | { 26 | NotNull(argument, paramName); 27 | if (!argument.Any()) 28 | { 29 | throw new ArgumentException("The collection can not be empty.", paramName); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Liam Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Anet.Web/Jwt/DefaultRefreshTokenStore.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Anet.Web.Jwt; 4 | 5 | public class DefaultRefreshTokenStore : IRefreshTokenStore 6 | { 7 | private readonly HttpContext _httpContext; 8 | 9 | public DefaultRefreshTokenStore(IHttpContextAccessor httpContextAccessor) 10 | { 11 | _httpContext = httpContextAccessor.HttpContext; 12 | } 13 | 14 | public Task SaveTokenAsync(JwtResult jwtResult) 15 | { 16 | return Task.CompletedTask; 17 | } 18 | 19 | public Task GetTokenAsync(string refreshToken) 20 | { 21 | if (string.IsNullOrEmpty(refreshToken)) 22 | { 23 | string prefix = "Bearer "; 24 | string token = _httpContext.Request.Headers["Authorization"]; 25 | if (!string.IsNullOrEmpty(token) && token.StartsWith(prefix)) 26 | { 27 | return Task.FromResult(token[prefix.Length..]); 28 | } 29 | } 30 | return Task.FromResult(default(string)); 31 | } 32 | 33 | public Task DeleteTokenAsync(string refreshToken) 34 | { 35 | return Task.CompletedTask; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Anet/Error.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace Anet; 4 | 5 | public class Error(string message, HttpStatusCode code = HttpStatusCode.BadRequest) : Exception(message) 6 | { 7 | public HttpStatusCode Code { get; } = code; 8 | 9 | public static void Throw(string message, HttpStatusCode code = HttpStatusCode.BadRequest) 10 | { 11 | throw new Error(message, code); 12 | } 13 | 14 | public static void Throw(bool predicate, string message, HttpStatusCode code = HttpStatusCode.BadRequest) 15 | { 16 | if (predicate) throw new Error(message, code); 17 | } 18 | 19 | public static void ThrowUnauthorized(bool predicate = true, string message= "Unauthorized") 20 | { 21 | if (predicate) throw new Error(message, HttpStatusCode.Unauthorized); 22 | } 23 | 24 | public static void ThrowForbidden(bool predicate = true, string message = "Forbidden") 25 | { 26 | if (predicate) throw new Error(message, HttpStatusCode.Forbidden); 27 | } 28 | 29 | public static void ThrowNotFound(bool predicate = true, string message = "Not Found") 30 | { 31 | if (predicate) throw new Error(message, HttpStatusCode.NotFound); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/JwtOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Web.Jwt; 2 | 3 | /// 4 | /// Provides options for . 5 | /// 6 | public class JwtOptions 7 | { 8 | /// 9 | /// The key for signature validation. 10 | /// 11 | public string Key { get; set; } 12 | 13 | /// 14 | /// The Issuer (iss) claim for generated tokens. 15 | /// 16 | public string Issuer { get; set; } 17 | 18 | /// 19 | /// The Audience (aud) claim for the generated tokens. 20 | /// 21 | public string Audience { get; set; } 22 | 23 | /// 24 | /// Lifetime of the the token, in seconds. Set to 0 to never expire. 25 | /// 26 | public int Lifetime { get; set; } 27 | 28 | /// 29 | /// Gets or sets the clock skew seconds to apply when validating the token lifetime. 30 | /// 31 | public int ClockSkew { get; set; } 32 | 33 | /// 34 | /// The fallback cookie key that read token from. 35 | /// This will give the application an opportunity to retrieve a token from cookies as an alternative location. 36 | /// 37 | public string FallbackCookieKey { get; set; } = "Authorization"; 38 | } 39 | -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Anet/Entity/ColumnAttributes.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace Anet.Entity; 5 | 6 | public class DecimalAttribute : ColumnAttribute 7 | { 8 | /// 9 | /// M is the maximum number of digits (the precision). It has a range of 1 to 65. 10 | /// 11 | /// 12 | /// D is the number of digits to the right of the decimal point (the scale). 13 | /// It has a range of 0 to 30 and must be no larger than M. 14 | /// 15 | public DecimalAttribute(ushort M, ushort D) 16 | { 17 | TypeName = $"decimal({M},{D})"; 18 | } 19 | } 20 | 21 | public class VarcharAttribute : MaxLengthAttribute 22 | { 23 | public VarcharAttribute(int length = 255) : base(length) 24 | { 25 | } 26 | } 27 | 28 | public class CharAttribute : ColumnAttribute 29 | { 30 | public CharAttribute(ushort length) 31 | { 32 | TypeName = $"char({length})"; 33 | } 34 | } 35 | 36 | public class TextAttribute : ColumnAttribute 37 | { 38 | public TextAttribute() 39 | { 40 | TypeName = "text"; 41 | } 42 | } 43 | 44 | public class DateAttribute : ColumnAttribute 45 | { 46 | public DateAttribute() 47 | { 48 | TypeName = "date"; 49 | } 50 | } 51 | 52 | public class DatetimeAttribute : ColumnAttribute 53 | { 54 | public DatetimeAttribute(ushort precision = 0) 55 | { 56 | TypeName = $"datetime({precision})"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Anet.Web/ApiResponseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | 5 | namespace Anet.Web; 6 | 7 | public class ApiResponseAttribute : ActionFilterAttribute 8 | { 9 | public override void OnActionExecuting(ActionExecutingContext context) 10 | { 11 | if (context.ModelState.IsValid) return; 12 | 13 | var errorMessage = "The request data is invalid."; 14 | 15 | foreach (var key in context.ModelState.Keys) 16 | { 17 | var value = context.ModelState[key]; 18 | if (value.Errors != null && value.Errors.Count > 0) 19 | { 20 | var message = value.Errors[0].ErrorMessage; 21 | errorMessage = string.IsNullOrEmpty(message) 22 | ? $"The value of {key} field is invalid." : message; 23 | break; 24 | } 25 | } 26 | 27 | context.Result = new JsonResult(ApiResult.Error(errorMessage)); 28 | } 29 | 30 | public override void OnActionExecuted(ActionExecutedContext context) 31 | { 32 | if (context.Result is FileResult || 33 | context.HttpContext.WebSockets.IsWebSocketRequest) 34 | { 35 | return; 36 | } 37 | 38 | var result = new ApiResult(); 39 | if (context.Exception != null) 40 | { 41 | result.Code = (int)(context.Exception is Error err ? err.Code : HttpStatusCode.InternalServerError); 42 | result.Message = context.Exception.Message; 43 | context.ExceptionHandled = result.Code < 500; 44 | } 45 | else if (context.Result is ObjectResult rst) 46 | { 47 | result.Code = rst.StatusCode >= 200 && rst.StatusCode < 300 ? 0 : (rst.StatusCode ?? 0); 48 | result.Data = rst.Value; 49 | } 50 | 51 | var jsonResult = new JsonResult(result); 52 | if (result.Code != 0) 53 | { 54 | jsonResult.StatusCode = result.Code; 55 | } 56 | context.Result = jsonResult; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/JwtProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Tokens; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.Security.Claims; 4 | using System.Text; 5 | 6 | namespace Anet.Web.Jwt; 7 | 8 | public class JwtProvider 9 | { 10 | private readonly JwtOptions _options; 11 | private readonly IRefreshTokenStore _refreshTokenStore; 12 | 13 | public JwtProvider(JwtOptions options, IRefreshTokenStore refreshTokenStore) 14 | { 15 | _options = options; 16 | _refreshTokenStore = refreshTokenStore; 17 | } 18 | 19 | public async Task GenerateToken(IEnumerable claims) 20 | { 21 | var expires = DateTime.UtcNow.AddSeconds(_options.Lifetime); 22 | 23 | var jwtSecurityToken = new JwtSecurityToken( 24 | claims: claims, 25 | issuer: _options.Issuer, 26 | audience: _options.Audience, 27 | expires: _options.Lifetime > 0 ? expires : null, 28 | signingCredentials: new SigningCredentials( 29 | new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key)), 30 | SecurityAlgorithms.HmacSha256) 31 | ); 32 | 33 | var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); 34 | 35 | var result = new JwtResult 36 | { 37 | AccessToken = accessToken, 38 | ExpiresAt = expires.ToTimestamp() 39 | }; 40 | 41 | await _refreshTokenStore.SaveTokenAsync(result); 42 | 43 | return result; 44 | } 45 | 46 | public async Task RefreshToken(string refreshToken) 47 | { 48 | var token = await _refreshTokenStore.GetTokenAsync(refreshToken); 49 | if (token == null) return null; 50 | 51 | var securityToken = new JwtSecurityTokenHandler().ReadJwtToken(token); 52 | if (securityToken == null || securityToken.ValidTo < DateTime.Now) 53 | return null; 54 | var newToken = await GenerateToken(securityToken.Claims.ToList()); 55 | 56 | await _refreshTokenStore.DeleteTokenAsync(refreshToken); 57 | await _refreshTokenStore.SaveTokenAsync(newToken); 58 | 59 | return newToken; 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Anet/Utilities/JsonUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | using System.Text.Json; 3 | 4 | namespace Anet.Utilities; 5 | 6 | public class JsonUtil 7 | { 8 | private static readonly JsonSerializerOptions _defaultOptions= new() 9 | { 10 | // ref: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/character-encoding 11 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 12 | 13 | PropertyNameCaseInsensitive = true, 14 | }; 15 | 16 | private static readonly JsonSerializerOptions _camelCaseOptions = new() 17 | { 18 | Encoder = _defaultOptions.Encoder, 19 | PropertyNameCaseInsensitive = _defaultOptions.PropertyNameCaseInsensitive, 20 | 21 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 22 | }; 23 | 24 | private static readonly JsonSerializerOptions _snakeCaseOptions = new() 25 | { 26 | Encoder = _defaultOptions.Encoder, 27 | PropertyNameCaseInsensitive = _defaultOptions.PropertyNameCaseInsensitive, 28 | 29 | PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, 30 | }; 31 | 32 | internal static void SetDefaultSerializerOptions(Action configure) 33 | { 34 | configure?.Invoke(_defaultOptions); 35 | } 36 | 37 | public static string Serialize(object value, JsonSerializerOptions options = null) 38 | { 39 | return JsonSerializer.Serialize(value, options ?? _defaultOptions); 40 | } 41 | 42 | public static T Deserialize(string json, JsonSerializerOptions options = null) 43 | { 44 | return JsonSerializer.Deserialize(json, options); 45 | } 46 | 47 | public static string SerializeSnakeCase(object value) 48 | { 49 | return Serialize(value, _snakeCaseOptions); 50 | } 51 | 52 | public static T DeserializeSnakeCase(string json) 53 | { 54 | return Deserialize(json, _snakeCaseOptions); 55 | } 56 | 57 | public static string SerializeCamelCase(object value) 58 | { 59 | return Serialize(value, _camelCaseOptions); 60 | } 61 | 62 | public static T DeserializeCamelCase(string json) 63 | { 64 | return Deserialize(json, _camelCaseOptions); 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /Anet/Security/AES.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace Anet.Security; 6 | 7 | public static class AES 8 | { 9 | public static byte[] Encrypt(byte[] data, byte[] key, byte[] iv, CipherMode cipherMode = CipherMode.CBC, PaddingMode paddingMode = PaddingMode.PKCS7) 10 | { 11 | Guard.NotNull(data, nameof(data)); 12 | Guard.NotNull(key, nameof(key)); 13 | 14 | using var aes = Aes.Create(); 15 | aes.Key = key; 16 | aes.IV = iv; 17 | aes.Mode = cipherMode; 18 | aes.Padding = paddingMode; 19 | 20 | using var encryptor = aes.CreateEncryptor(); 21 | return encryptor.TransformFinalBlock(data, 0, data.Length); 22 | } 23 | 24 | public static byte[] Decrypt(byte[] data, byte[] key, byte[] iv, CipherMode cipherMode = CipherMode.CBC, PaddingMode paddingMode = PaddingMode.PKCS7) 25 | { 26 | Guard.NotNull(data, nameof(data)); 27 | Guard.NotNull(key, nameof(key)); 28 | 29 | using var aes = Aes.Create(); 30 | aes.Key = key; 31 | aes.IV = iv; 32 | aes.Mode = cipherMode; 33 | aes.Padding = paddingMode; 34 | 35 | using var decryptor = aes.CreateDecryptor(); 36 | return decryptor.TransformFinalBlock(data, 0, data.Length); 37 | } 38 | 39 | public static string Encrypt(string data, string key, string iv, CipherMode cipherMode = CipherMode.CBC, PaddingMode paddingMode = PaddingMode.PKCS7) 40 | { 41 | Guard.NotNull(iv, nameof(iv)); 42 | 43 | var dataBytes = Encoding.UTF8.GetBytes(data); 44 | var keyBytes = Encoding.UTF8.GetBytes(key); 45 | var ivBytes = Encoding.UTF8.GetBytes(iv); 46 | 47 | var resultBytes = Encrypt(dataBytes, keyBytes, ivBytes, cipherMode, paddingMode); 48 | 49 | return Convert.ToBase64String(resultBytes); 50 | } 51 | 52 | public static string Decrypt(string data, string key, string iv, CipherMode cipherMode = CipherMode.CBC, PaddingMode paddingMode = PaddingMode.PKCS7) 53 | { 54 | Guard.NotNull(iv, nameof(iv)); 55 | 56 | var dataBytes = Convert.FromBase64String(data); 57 | var keyBytes = Encoding.UTF8.GetBytes(key); 58 | var ivBytes = Encoding.UTF8.GetBytes(iv); 59 | 60 | var resultBytes = Decrypt(dataBytes, keyBytes, ivBytes, cipherMode, paddingMode); 61 | 62 | return Encoding.UTF8.GetString(resultBytes); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Anet/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace System; 4 | #pragma warning disable IDE0049 // Simplify Names 5 | public static class TypeExtensions 6 | { 7 | public static bool IsInteger(this ValueType value) 8 | { 9 | return value is SByte or Int16 or Int32 or Int64 or Byte or UInt16 or UInt32 or UInt64 or BigInteger; 10 | } 11 | 12 | public static bool IsFloat(this ValueType value) 13 | { 14 | return value is Single or Double or Decimal; 15 | } 16 | 17 | public static bool IsNumeric(this ValueType value) 18 | { 19 | return IsInteger(value) || IsFloat(value); 20 | } 21 | 22 | /// 23 | /// Returns true if the type is one of the built in simple types. 24 | /// 25 | public static bool IsSimpleType(this Type type) 26 | { 27 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) 28 | type = type.GetGenericArguments()[0]; // or Nullable.GetUnderlyingType(type) 29 | 30 | // The primitive types are Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single. 31 | if (type.IsPrimitive || type.IsEnum || type == typeof(Guid)) 32 | return true; 33 | 34 | TypeCode tc = Type.GetTypeCode(type); 35 | return tc switch 36 | { 37 | TypeCode.Decimal or 38 | TypeCode.String or 39 | TypeCode.DateTime => true, 40 | TypeCode.Object => (typeof(TimeSpan) == type) || (typeof(DateTimeOffset) == type) || (typeof(DateOnly) == type) || (typeof(TimeOnly) == type), 41 | _ => false, 42 | }; 43 | } 44 | 45 | public static Type GetAnyElementType(this Type type) 46 | { 47 | // short-circuit if you expect lots of arrays 48 | if (type.IsArray) 49 | return type.GetElementType(); 50 | 51 | // type is IEnumerable; 52 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 53 | return type.GetGenericArguments()[0]; 54 | 55 | // type implements/extends IEnumerable; 56 | var enumType = type.GetInterfaces() 57 | .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 58 | .Select(t => t.GenericTypeArguments[0]).FirstOrDefault(); 59 | 60 | return enumType ?? type; 61 | } 62 | } 63 | #pragma warning restore IDE0049 // Simplify Names 64 | -------------------------------------------------------------------------------- /Anet.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32104.313 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Anet", "Anet\Anet.csproj", "{ED670328-88E3-41DF-98D2-4DEA96C976DB}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Anet.Web", "Anet.Web\Anet.Web.csproj", "{41F97159-01FD-49F4-91B8-D25800078D15}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution items", "solution items", "{6BC8D288-5768-4CEE-86F6-F80AA0828DF5}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | .gitignore = .gitignore 14 | build.cmd = build.cmd 15 | build.ps1 = build.ps1 16 | README.md = README.md 17 | share.props = share.props 18 | EndProjectSection 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{AAEFB83E-5939-443B-9FB0-CD11E8DB6AD5}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {ED670328-88E3-41DF-98D2-4DEA96C976DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {ED670328-88E3-41DF-98D2-4DEA96C976DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {ED670328-88E3-41DF-98D2-4DEA96C976DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {ED670328-88E3-41DF-98D2-4DEA96C976DB}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {41F97159-01FD-49F4-91B8-D25800078D15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {41F97159-01FD-49F4-91B8-D25800078D15}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {41F97159-01FD-49F4-91B8-D25800078D15}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {41F97159-01FD-49F4-91B8-D25800078D15}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {AAEFB83E-5939-443B-9FB0-CD11E8DB6AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {AAEFB83E-5939-443B-9FB0-CD11E8DB6AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {AAEFB83E-5939-443B-9FB0-CD11E8DB6AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {AAEFB83E-5939-443B-9FB0-CD11E8DB6AD5}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(ExtensibilityGlobals) = postSolution 45 | SolutionGuid = {3380AE0E-41C5-4701-9A2B-5E65F82945A3} 46 | EndGlobalSection 47 | EndGlobal 48 | -------------------------------------------------------------------------------- /Anet/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace System; 4 | 5 | public static class StringExtensions 6 | { 7 | public static bool Equals(this string source, string value, bool ignoreCase) 8 | { 9 | return source.Equals(value, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); 10 | } 11 | 12 | public static string ToSnakeCase(this string source) 13 | { 14 | if (string.IsNullOrEmpty(source)) return source; 15 | 16 | //return Regex.Replace(input, @"([a-z0-9])([A-Z])", "$1_$2").ToLower(); 17 | 18 | var sb = new StringBuilder(); 19 | var state = SnakeCaseState.Start; 20 | 21 | var nameSpan = source.AsSpan(); 22 | 23 | for (int i = 0; i < nameSpan.Length; i++) 24 | { 25 | if (nameSpan[i] == ' ') 26 | { 27 | if (state != SnakeCaseState.Start) 28 | { 29 | state = SnakeCaseState.NewWord; 30 | } 31 | } 32 | else if (char.IsUpper(nameSpan[i])) 33 | { 34 | switch (state) 35 | { 36 | case SnakeCaseState.Upper: 37 | bool hasNext = i + 1 < nameSpan.Length; 38 | if (i > 0 && hasNext) 39 | { 40 | char nextChar = nameSpan[i + 1]; 41 | if (!char.IsUpper(nextChar) && nextChar != '_') 42 | { 43 | sb.Append('_'); 44 | } 45 | } 46 | break; 47 | case SnakeCaseState.Lower: 48 | case SnakeCaseState.NewWord: 49 | sb.Append('_'); 50 | break; 51 | } 52 | sb.Append(char.ToLowerInvariant(nameSpan[i])); 53 | state = SnakeCaseState.Upper; 54 | } 55 | else if (nameSpan[i] == '_') 56 | { 57 | sb.Append('_'); 58 | state = SnakeCaseState.Start; 59 | } 60 | else 61 | { 62 | if (state == SnakeCaseState.NewWord) 63 | { 64 | sb.Append('_'); 65 | } 66 | 67 | sb.Append(nameSpan[i]); 68 | state = SnakeCaseState.Lower; 69 | } 70 | } 71 | 72 | return sb.ToString(); 73 | } 74 | 75 | private enum SnakeCaseState 76 | { 77 | Start, 78 | Lower, 79 | Upper, 80 | NewWord 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Anet/IdGen.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Anet.Utilities; 3 | 4 | namespace Anet; 5 | 6 | /// 7 | /// 类似 Twitter Snowflake(41 + 10 + 12) 算法的 Id 生成器。 8 | /// 格式:{32 位时间戳, 0-10 位机器码, 0-20 位递增系列号}。 9 | /// 注意:程序启动前请确保系统时间正确。 10 | /// 11 | public partial class IdGen 12 | { 13 | private readonly long _maxSequence = 0; 14 | private readonly object _lockObject = new(); 15 | private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); 16 | 17 | private long _sequence = 0; 18 | private long _lastTimestamp = 0; 19 | 20 | // 637134336000000000 = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks 21 | private static readonly long _offsetTicks = DateTime.UtcNow.Ticks - 637134336000000000; 22 | 23 | private readonly IdGenOptions _options; 24 | 25 | /// 26 | /// The constructor of . 27 | /// 28 | public IdGen(IdGenOptions options) 29 | { 30 | if (options.SequenceBits > 20) 31 | throw new ArgumentOutOfRangeException(nameof(options.SequenceBits), "序列号不能超过 20 位。"); 32 | 33 | if (options.MachineIdBits > 10) 34 | throw new ArgumentOutOfRangeException(nameof(options.MachineIdBits), "机器码不能超过 10 位。"); 35 | 36 | var maxMachineId = BitUtil.GetMaxOfBits(options.MachineIdBits); 37 | if (options.MachineId > maxMachineId) 38 | throw new ArgumentOutOfRangeException(nameof(options.MachineId), $"机器码不能大于 {maxMachineId}。"); 39 | 40 | _options = options; 41 | _maxSequence = BitUtil.GetMaxOfBits(options.SequenceBits); 42 | } 43 | 44 | private long GetTimestampNow() 45 | { 46 | // 10000000 = TimeSpan.FromSeconds(1).Ticks 47 | return (_offsetTicks + _stopwatch.Elapsed.Ticks) / 10000000L; 48 | } 49 | 50 | private long GetNextTimestamp() 51 | { 52 | long timestamp = GetTimestampNow(); 53 | if (timestamp < _lastTimestamp) 54 | throw new Exception("新的时间戳比旧的小,请检查系统时间。"); 55 | 56 | while (timestamp == _lastTimestamp) 57 | { 58 | if (_sequence < _maxSequence) 59 | { 60 | _sequence++; 61 | return timestamp; 62 | } 63 | Thread.Sleep(0); // 降低CPU消耗 64 | timestamp = GetTimestampNow(); 65 | } 66 | _sequence = 0; 67 | 68 | return timestamp; 69 | } 70 | 71 | /// 72 | /// 生成新的ID 73 | /// 74 | /// ID 75 | public long NewSequenceId() 76 | { 77 | lock (_lockObject) 78 | { 79 | _lastTimestamp = GetNextTimestamp(); 80 | 81 | //int bitsLength = GetBitsLength(_lastTimestamp); 82 | //Console.WriteLine($"Timestamp bits: {bitsLength}"); 83 | 84 | int timestampShift = _options.MachineIdBits + _options.SequenceBits; 85 | int machineIdShift = _options.SequenceBits; 86 | return (_lastTimestamp << timestampShift) | (_options.MachineId << machineIdShift) | _sequence; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Anet/Utilities/StringUtil.cs: -------------------------------------------------------------------------------- 1 | namespace Anet.Utilities; 2 | 3 | public static class StringUtil 4 | { 5 | /// 6 | /// 打乱字符串 7 | /// 8 | public static string Mixup(string text) 9 | { 10 | int sum = 0; 11 | foreach (char c in text) 12 | { 13 | sum += c; 14 | } 15 | int x = sum % text.Length; 16 | char[] arr = text.ToCharArray(); 17 | char[] newArr = new char[arr.Length]; 18 | Array.Copy(arr, x, newArr, 0, text.Length - x); 19 | Array.Copy(arr, 0, newArr, text.Length - x, x); 20 | return new string(newArr); 21 | } 22 | 23 | /// 24 | /// 恢复打乱的字符串 25 | /// 26 | public static string UnMixup(string cipher) 27 | { 28 | int sum = 0; 29 | foreach (char c in cipher) 30 | { 31 | sum += c; 32 | } 33 | int x = cipher.Length - sum % cipher.Length; 34 | char[] arr = cipher.ToCharArray(); 35 | char[] newArr = new char[arr.Length]; 36 | Array.Copy(arr, x, newArr, 0, cipher.Length - x); 37 | Array.Copy(arr, 0, newArr, cipher.Length - x, x); 38 | return new string(newArr); 39 | } 40 | 41 | /// 42 | /// 拼接多个路径 43 | /// 44 | public static string CombinePath(char separator, params string[] paths) 45 | { 46 | Guard.NotNull(separator, nameof(separator)); 47 | Guard.NotNull(paths, nameof(paths)); 48 | for (int i = 0; i < paths.Length; i++) 49 | { 50 | if (i != 0) 51 | paths[i] = paths[i].TrimStart(separator); 52 | if (i != paths.Length - 1) 53 | paths[i] = paths[i].TrimEnd(separator); 54 | } 55 | return string.Join(separator, paths); 56 | } 57 | 58 | /// 59 | /// 拼接多个路径 60 | /// 以路径参数中首次出现的分隔符(“/"或“\”)作为拼接分隔符 61 | /// 如果路径参数中无分隔符,则使用当前系统路径分隔符 62 | /// 63 | public static string CombinePath(params string[] paths) 64 | { 65 | Guard.NotNull(paths, nameof(paths)); 66 | char seprator = string.Join("", paths).ToCharArray().FirstOrDefault(c => c == '/' || c == '\\'); 67 | if (seprator == default(char)) 68 | { 69 | seprator = Path.DirectorySeparatorChar; 70 | } 71 | return CombinePath(seprator, paths); 72 | } 73 | 74 | /// 75 | /// 拼接URL路径 76 | /// 77 | public static string CombineUrl(params string[] paths) 78 | { 79 | return CombinePath('/', paths); 80 | } 81 | 82 | public static string GenRandomString(int size, bool useUpper = true, bool useLower = true, bool useNumber = true) 83 | { 84 | string alphabet = ""; 85 | if (useUpper) alphabet += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 86 | if (useLower) alphabet += "abcdefghijklmnopqrstuvwyxz"; 87 | if (useNumber) alphabet += "0123456789"; 88 | 89 | var rand = new Random(); 90 | char[] chars = new char[size]; 91 | for (int i = 0; i < size; i++) 92 | { 93 | chars[i] = alphabet[rand.Next(alphabet.Length)]; 94 | } 95 | return new string(chars); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Anet.Web/ServicesExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Anet.Web.Jwt; 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.IdentityModel.Tokens; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection; 7 | 8 | public static class ServicesExtensions 9 | { 10 | public static IServiceCollection AddAnetJwt( 11 | this IServiceCollection services, 12 | Action configure) 13 | { 14 | return services.AddAnetJwt(configure); 15 | } 16 | 17 | public static IServiceCollection AddAnetJwt( 18 | this IServiceCollection services, 19 | Action configure, 20 | Action configureJwtBearer) 21 | { 22 | return services.AddAnetJwt(configure, configureJwtBearer); 23 | } 24 | 25 | public static IServiceCollection AddAnetJwt( 26 | this IServiceCollection services, 27 | Action configure, 28 | Action configureJwtBearer = null) 29 | where TRefreshTokenStore : class, IRefreshTokenStore 30 | { 31 | var options = new JwtOptions(); 32 | configure(options); 33 | 34 | ArgumentNullException.ThrowIfNull(options); 35 | ArgumentNullException.ThrowIfNull(options.Key); 36 | 37 | services.AddSingleton(options); 38 | services.AddHttpContextAccessor(); 39 | services.AddTransient(); 40 | services.AddTransient(); 41 | services 42 | .AddAuthentication(options => 43 | { 44 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 45 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 46 | }) 47 | .AddJwtBearer(jwtBearerOptions => 48 | { 49 | jwtBearerOptions.TokenValidationParameters = new() 50 | { 51 | ValidIssuer = options.Issuer, 52 | ValidateIssuer = !string.IsNullOrEmpty(options.Issuer), 53 | ValidAudience = options.Audience, 54 | ValidateAudience = !string.IsNullOrEmpty(options.Audience), 55 | ValidateLifetime = options.Lifetime > 0, 56 | ValidateIssuerSigningKey = true, 57 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.Key)), 58 | ClockSkew = TimeSpan.FromSeconds(options.ClockSkew) 59 | }; 60 | if (!string.IsNullOrEmpty(options.FallbackCookieKey)) 61 | { 62 | jwtBearerOptions.Events = new JwtBearerEvents 63 | { 64 | OnMessageReceived = context => 65 | { 66 | var token = context.Request.Cookies[options.FallbackCookieKey]; 67 | if (!string.IsNullOrEmpty(token)) 68 | { 69 | context.Token = token; 70 | } 71 | return Task.CompletedTask; 72 | } 73 | }; 74 | } 75 | configureJwtBearer?.Invoke(jwtBearerOptions); 76 | }); 77 | 78 | return services; 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /Anet/Security/PasswordHasher.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Security.Cryptography; 3 | 4 | namespace Anet.Security; 5 | 6 | /// 7 | /// Salted Hasher 8 | /// PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations. 9 | /// Format: { salt, subkey } 10 | /// 11 | public static class PasswordHasher 12 | { 13 | private const int _salt_size = 128 / 8; // 128 bits 14 | private const int _subkey_size = 256 / 8; // 256 bits 15 | private const int _pbkdf2_iteration = 1000; // default for Rfc2898DeriveBytes. 16 | 17 | public static readonly HashAlgorithmName DefaultAlgorithm = HashAlgorithmName.MD5; 18 | 19 | /// 20 | /// Hash the password using . 21 | /// 22 | /// The password to hash. 23 | /// Salted hash result. 24 | public static string Hash(string password) 25 | { 26 | return Hash(password, DefaultAlgorithm); 27 | } 28 | 29 | /// 30 | /// Hash the password. 31 | /// 32 | /// The password to hash. 33 | /// The hash algorithm to use to derive the key. 34 | /// Salted hash result. 35 | public static string Hash(string password, HashAlgorithmName algorithm) 36 | { 37 | using var rng = RandomNumberGenerator.Create(); 38 | var salt = new byte[_salt_size]; 39 | rng.GetBytes(salt); 40 | 41 | byte[] subkey = Pbkdf2(password, salt, algorithm); 42 | 43 | var hash = new byte[_salt_size + _subkey_size]; 44 | Buffer.BlockCopy(salt, 0, hash, 0, _salt_size); 45 | Buffer.BlockCopy(subkey, 0, hash, _salt_size, _subkey_size); 46 | 47 | return Convert.ToBase64String(hash); 48 | } 49 | 50 | /// 51 | /// Validates a password given a salt and a hash using . 52 | /// 53 | /// The password to check. 54 | /// A hash of the correct password. 55 | /// True if the password is correct. False otherwise. 56 | public static bool Verify(string password, string hash) 57 | { 58 | return Verify(password, hash, DefaultAlgorithm); 59 | } 60 | 61 | /// 62 | /// Validates a password given a salt and a hash. 63 | /// 64 | /// The password to check. 65 | /// A hash of the correct password. 66 | /// The hash algorithm to use to derive the key. 67 | /// True if the password is correct. False otherwise. 68 | public static bool Verify(string password, string hash, HashAlgorithmName algorithm) 69 | { 70 | byte[] decoded = Convert.FromBase64String(hash); 71 | if (decoded.Length != _salt_size + _subkey_size) 72 | { 73 | return false; 74 | } 75 | 76 | byte[] salt = new byte[_salt_size]; 77 | Buffer.BlockCopy(decoded, 0, salt, 0, _salt_size); 78 | 79 | byte[] expectedSubkey = new byte[_subkey_size]; 80 | Buffer.BlockCopy(decoded, _salt_size, expectedSubkey, 0, _subkey_size); 81 | 82 | byte[] actualSubkey = Pbkdf2(password, salt, algorithm); 83 | return ByteArraysEqual(expectedSubkey, actualSubkey); 84 | } 85 | 86 | // Computes the PBKDF2-SHA1 hash of a password. 87 | private static byte[] Pbkdf2(string password, byte[] salt, HashAlgorithmName algorithm) 88 | { 89 | using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, _pbkdf2_iteration, algorithm); 90 | return pbkdf2.GetBytes(_subkey_size); 91 | } 92 | 93 | // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. 94 | [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] 95 | private static bool ByteArraysEqual(byte[] a, byte[] b) 96 | { 97 | if (a == null && b == null) 98 | { 99 | return true; 100 | } 101 | if (a == null || b == null || a.Length != b.Length) 102 | { 103 | return false; 104 | } 105 | var areSame = true; 106 | for (var i = 0; i < a.Length; i++) 107 | { 108 | areSame &= (a[i] == b[i]); 109 | } 110 | return areSame; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Anet.Web/Jwt/JwtProviderMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text; 3 | using Anet.Utilities; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Anet.Web.Jwt; 7 | 8 | [Obsolete("已废弃")] 9 | internal class JwtProviderMiddleware 10 | { 11 | private readonly string _path; 12 | private readonly RequestDelegate _next; 13 | private JwtProvider _provider; 14 | private HttpContext _context; 15 | private IAuthenticator _authenticator; 16 | 17 | public JwtProviderMiddleware( 18 | RequestDelegate next, string tokenPath) 19 | { 20 | _next = next; 21 | _path = tokenPath; 22 | } 23 | 24 | public async Task InvokeAsync( 25 | HttpContext context, 26 | JwtProvider provider, 27 | IAuthenticator authenticator) 28 | { 29 | var request = context.Request; 30 | if (!request.Path.Equals(_path, StringComparison.OrdinalIgnoreCase) || request.Method != HttpMethods.Post) 31 | { 32 | await _next(context); 33 | return; 34 | } 35 | 36 | _context = context; 37 | _provider = provider; 38 | _authenticator = authenticator; 39 | 40 | var requestParams = await ResolveTokenRequest(request); 41 | if (requestParams == null) return; 42 | 43 | if (GrantTypes.REFRESH_TOKEN.Equals(requestParams.GrantType, true)) 44 | { 45 | await RefreshToken(requestParams); 46 | } 47 | else 48 | { 49 | await GenerateToken(requestParams); 50 | } 51 | } 52 | 53 | private async Task ResolveTokenRequest(HttpRequest request) 54 | { 55 | var contentType = request.ContentType?.ToLower(); 56 | if (contentType.Contains(ContentTypes.FormUrlencodedContentType)) 57 | { 58 | return new JwtParams 59 | { 60 | Username = request.Form[nameof(JwtParams.Username)].FirstOrDefault(), 61 | Password = request.Form[nameof(JwtParams.Password)].FirstOrDefault(), 62 | // Payload = request.Form[nameof(JwtParams.Payload)].FirstOrDefault(), 63 | GrantType = request.Form[nameof(JwtParams.GrantType)].FirstOrDefault(), 64 | RefreshToken = request.Form[nameof(JwtParams.RefreshToken)].FirstOrDefault() 65 | }; 66 | } 67 | if (contentType.Contains(ContentTypes.JsonContentType)) 68 | { 69 | using var reader = new StreamReader(request.Body, Encoding.UTF8); 70 | string body = await reader.ReadToEndAsync(); 71 | try 72 | { 73 | return JsonUtil.DeserializeCamelCase(body); 74 | } 75 | catch 76 | { 77 | await ResponseErrorAsync("Unrecognized request body."); 78 | return null; 79 | } 80 | } 81 | await ResponseErrorAsync("Unsurported Content-Type."); 82 | return null; 83 | } 84 | 85 | private async Task GenerateToken(JwtParams jwtParams) 86 | { 87 | var authResult = await _authenticator.AuthenticateAsync(jwtParams); 88 | 89 | if (authResult == null || !authResult.Success) 90 | { 91 | await ResponseErrorAsync(authResult?.Message); 92 | return; 93 | } 94 | 95 | var jwtResult = await _provider.GenerateToken(authResult.Claims); 96 | 97 | jwtResult.UserInfo = authResult.UserInfo; 98 | 99 | await ResponseSuccessAsync(jwtResult); 100 | } 101 | 102 | private async Task RefreshToken(JwtParams jwtParams) 103 | { 104 | var newToken = _provider.RefreshToken(jwtParams.RefreshToken); 105 | if (newToken == null) 106 | { 107 | await ResponseErrorAsync("Invalid refresh token."); 108 | return; 109 | } 110 | await ResponseSuccessAsync(newToken); 111 | } 112 | 113 | private Task ResponseSuccessAsync(T data) 114 | { 115 | return ResponseAsync(ApiResult.Success(data)); 116 | } 117 | 118 | private Task ResponseErrorAsync(string message) 119 | { 120 | return ResponseAsync(ApiResult.Error(message, HttpStatusCode.BadRequest)); 121 | } 122 | 123 | private Task ResponseAsync(ApiResult apiResult) 124 | { 125 | _context.Response.ContentType = ContentTypes.JsonContentType; 126 | _context.Response.StatusCode = (int)HttpStatusCode.OK; 127 | var json = JsonUtil.SerializeCamelCase(apiResult); 128 | return _context.Response.WriteAsync(json); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | insert_final_newline = true 15 | charset = utf-8-bom 16 | 17 | # Xml project files 18 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 19 | indent_size = 4 20 | 21 | # Xml config files 22 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 23 | indent_size = 2 24 | 25 | # JSON files 26 | [*.json] 27 | indent_size = 2 28 | 29 | # Dotnet code style settings: 30 | [*.{cs,vb}] 31 | # Sort using and Import directives with System.* appearing first 32 | dotnet_sort_system_directives_first = true 33 | # Avoid "this." and "Me." if not necessary 34 | dotnet_style_qualification_for_field = false:suggestion 35 | dotnet_style_qualification_for_property = false:suggestion 36 | dotnet_style_qualification_for_method = false:suggestion 37 | dotnet_style_qualification_for_event = false:suggestion 38 | 39 | # Use language keywords instead of framework type names for type references 40 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 41 | dotnet_style_predefined_type_for_member_access = true:suggestion 42 | 43 | # Suggest more modern language features when available 44 | dotnet_style_object_initializer = true:suggestion 45 | dotnet_style_collection_initializer = true:suggestion 46 | dotnet_style_coalesce_expression = true:suggestion 47 | dotnet_style_null_propagation = true:suggestion 48 | dotnet_style_explicit_tuple_names = true:suggestion 49 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 50 | tab_width = 4 51 | end_of_line = crlf 52 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 53 | dotnet_style_prefer_auto_properties = true:silent 54 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 55 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 56 | 57 | # CSharp code style settings: 58 | [*.cs] 59 | # Prefer "var" everywhere 60 | #csharp_style_var_for_built_in_types = true:suggestion 61 | #csharp_style_var_when_type_is_apparent = false:suggestion 62 | #csharp_style_var_elsewhere = true:suggestion 63 | 64 | # Prefer method-like constructs to have a expression-body 65 | csharp_style_expression_bodied_methods = true:none 66 | csharp_style_expression_bodied_constructors = true:none 67 | csharp_style_expression_bodied_operators = true:none 68 | 69 | # Prefer property-like constructs to have an expression-body 70 | csharp_style_expression_bodied_properties = true:none 71 | csharp_style_expression_bodied_indexers = true:none 72 | csharp_style_expression_bodied_accessors = true:none 73 | 74 | # Suggest more modern language features when available 75 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 76 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 77 | csharp_style_inlined_variable_declaration = true:suggestion 78 | csharp_style_throw_expression = true:suggestion 79 | csharp_style_conditional_delegate_call = true:suggestion 80 | 81 | # Newline settings 82 | csharp_new_line_before_open_brace = all 83 | csharp_new_line_before_else = true 84 | csharp_new_line_before_catch = true 85 | csharp_new_line_before_finally = true 86 | csharp_new_line_before_members_in_object_initializers = true 87 | csharp_new_line_before_members_in_anonymous_types = true 88 | csharp_indent_labels = one_less_than_current 89 | csharp_space_around_binary_operators = before_and_after 90 | csharp_using_directive_placement = outside_namespace:silent 91 | csharp_prefer_simple_using_statement = true:suggestion 92 | csharp_prefer_braces = true:silent 93 | csharp_style_namespace_declarations = block_scoped:silent 94 | csharp_style_expression_bodied_lambdas = true:silent 95 | csharp_style_expression_bodied_local_functions = false:silent 96 | [*.cs] 97 | #### Naming styles #### 98 | 99 | # Naming rules 100 | 101 | dotnet_naming_rule.private_or_internal_field_should_be__fieldname.severity = suggestion 102 | dotnet_naming_rule.private_or_internal_field_should_be__fieldname.symbols = private_or_internal_field 103 | dotnet_naming_rule.private_or_internal_field_should_be__fieldname.style = _fieldname 104 | 105 | # Symbol specifications 106 | 107 | dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field 108 | dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected 109 | dotnet_naming_symbols.private_or_internal_field.required_modifiers = 110 | 111 | # Naming styles 112 | 113 | dotnet_naming_style._fieldname.required_prefix = _ 114 | dotnet_naming_style._fieldname.required_suffix = 115 | dotnet_naming_style._fieldname.word_separator = 116 | dotnet_naming_style._fieldname.capitalization = camel_case 117 | 118 | [*.{cs,vb}] 119 | #### Naming styles #### 120 | 121 | # Naming rules 122 | 123 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 124 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 125 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 126 | 127 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 128 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 129 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 130 | 131 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 132 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 133 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 134 | 135 | # Symbol specifications 136 | 137 | dotnet_naming_symbols.interface.applicable_kinds = interface 138 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 139 | dotnet_naming_symbols.interface.required_modifiers = 140 | 141 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 142 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 143 | dotnet_naming_symbols.types.required_modifiers = 144 | 145 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 146 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 147 | dotnet_naming_symbols.non_field_members.required_modifiers = 148 | 149 | # Naming styles 150 | 151 | dotnet_naming_style.begins_with_i.required_prefix = I 152 | dotnet_naming_style.begins_with_i.required_suffix = 153 | dotnet_naming_style.begins_with_i.word_separator = 154 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 155 | 156 | dotnet_naming_style.pascal_case.required_prefix = 157 | dotnet_naming_style.pascal_case.required_suffix = 158 | dotnet_naming_style.pascal_case.word_separator = 159 | dotnet_naming_style.pascal_case.capitalization = pascal_case 160 | 161 | dotnet_naming_style.pascal_case.required_prefix = 162 | dotnet_naming_style.pascal_case.required_suffix = 163 | dotnet_naming_style.pascal_case.word_separator = 164 | dotnet_naming_style.pascal_case.capitalization = pascal_case 165 | --------------------------------------------------------------------------------