diff --git a/Falcon.SugarApi.Test/JwtTokenBuilderTest.cs b/Falcon.SugarApi.Test/JwtTokenBuilderTest.cs new file mode 100644 index 0000000..6a69a56 --- /dev/null +++ b/Falcon.SugarApi.Test/JwtTokenBuilderTest.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Falcon.SugarApi.JWT; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Falcon.SugarApi.Test +{ + /// + /// JwtTokenBuilderTest + /// + [TestClass] + public class JwtTokenBuilderTest + { + /// + /// Token获取测试 + /// + [TestMethod] + public void GetTokenTest() { + var loginTime = DateTime.Now; + var playload = new LoginUserInfo { + UserName = "abdc", + LoginTime = loginTime, + Roles = new List { "admin", "user" }, + }; + if (new JwtTokenBuilder().TryGetToken(playload, out var token, out var exception)) { + Console.WriteLine("token:{0}", token); + } + else { + Console.WriteLine(exception.ToString()); + Assert.Fail("获取token失败"); + } + Assert.IsNotNull(token); + if (new JwtTokenBuilder().TryGetPlayload(token, out var pl, out var exception1)) { + + } + else { + Console.WriteLine(exception1.ToString()); + Assert.Fail("获取Playload失败"); + } + Assert.IsNotNull(pl); + Assert.AreEqual(playload.UserName, pl.UserName); + Assert.IsTrue(pl.Roles != null); + Assert.IsTrue(pl.Roles.Count == 2); + Assert.IsTrue(pl.Roles.Any(m => m == "admin")); + Assert.IsTrue(pl.Roles.Any(m => m == "user")); + } + } +} diff --git a/Falcon.SugarApi/Encryption/AESConfig.cs b/Falcon.SugarApi/Encryption/AESConfig.cs index 1e9627b..d2bdc8a 100644 --- a/Falcon.SugarApi/Encryption/AESConfig.cs +++ b/Falcon.SugarApi/Encryption/AESConfig.cs @@ -1,10 +1,15 @@ -namespace Falcon.SugarApi.Encryption +using Microsoft.Extensions.Options; + +namespace Falcon.SugarApi.Encryption { /// /// AES加密算法配置 /// - public class AESConfig + public class AESConfig : IOptions { + /// + public AESConfig Value => this; + private int keyLength = 32; /// @@ -16,5 +21,6 @@ /// 秘钥字符表 /// public string KeyChars { get; set; } = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789,.!/*\"; + } } diff --git a/Falcon.SugarApi/Falcon.SugarApi.csproj b/Falcon.SugarApi/Falcon.SugarApi.csproj index 9049b42..76bb987 100644 --- a/Falcon.SugarApi/Falcon.SugarApi.csproj +++ b/Falcon.SugarApi/Falcon.SugarApi.csproj @@ -12,6 +12,7 @@ + diff --git a/Falcon.SugarApi/JWT/AESTokenbuilder.cs b/Falcon.SugarApi/JWT/AESTokenbuilder.cs new file mode 100644 index 0000000..141f44b --- /dev/null +++ b/Falcon.SugarApi/JWT/AESTokenbuilder.cs @@ -0,0 +1,42 @@ +using Falcon.SugarApi.Encryption; + +namespace Falcon.SugarApi.JWT +{ + /// + /// 使用AES算法生成Token + /// + public class AESTokenbuilder : IJwtTokenBuilder + { + /// + /// AES加密算法 + /// + public IAESEncryption AES { get; set; } + /// + /// jwt生成参数 + /// + public JwtContext Jwt { get; } + + /// + /// 构造AES的Token + /// + /// AES参数 + /// jwt生成参数 + public AESTokenbuilder(AESConfig config, JwtContext jwt) { + Jwt = jwt; + AES = new AESProvider(config); + } + + /// / + public LoginUserInfo GetPlayload(string token) { + token.ThrowNullExceptionWhenNull(); + return this.AES.Decrypt(Jwt.SecKey, token); + } + + /// / + public string GetToken(LoginUserInfo playload) { + playload.ThrowNullExceptionWhenNull(); + return this.AES.Encrypt(Jwt.SecKey, playload); + } + } + +} diff --git a/Falcon.SugarApi/JWT/ApiAuthorizationAttribute.cs b/Falcon.SugarApi/JWT/ApiAuthorizationAttribute.cs new file mode 100644 index 0000000..2f0639e --- /dev/null +++ b/Falcon.SugarApi/JWT/ApiAuthorizationAttribute.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace Falcon.SugarApi.JWT +{ + /// + /// 验证 + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class ApiAuthorizationAttribute : Attribute, IAuthorizationFilter + { + /// + /// 用户需要具有的角色 + /// + public List Roles { get; set; } = new List(); + + /// + public void OnAuthorization(AuthorizationFilterContext context) { + if (context.Filters.Any(f => f is IAllowAnonymousFilter)) { + return; + } + var option = context.HttpContext.RequestServices.GetRequiredService(); + var key = option?.AuthHeaderKey; + key.ThrowNullExceptionWhenNull(); + var token = context.HttpContext.Request.Headers[key].ToString(); + if (token.IsNullOrEmpty()) { + Unauthorized(context); + return; + } + var jwt = option?.JwtTokenBuilder; + jwt.ThrowNullExceptionWhenNull(); + var user = jwt.GetPlayload(token); + var userLogin = option?.UserLogin; + if (userLogin != null && !userLogin.CheckUserLogin(user)) { + Unauthorized(context); + return; + } + if (this.Roles != null && this.Roles.Count > 0 && !userLogin.UserInRoles(user, this.Roles)) { + Unauthorized(context); + return; + } + return; + } + + /// + /// 返回授权失败 + /// + /// 上下文 + private static void Unauthorized(AuthorizationFilterContext context) { + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + } + } +} diff --git a/Falcon.SugarApi/JWT/IJwtTokenBuilder.cs b/Falcon.SugarApi/JWT/IJwtTokenBuilder.cs new file mode 100644 index 0000000..19ba5e5 --- /dev/null +++ b/Falcon.SugarApi/JWT/IJwtTokenBuilder.cs @@ -0,0 +1,22 @@ +namespace Falcon.SugarApi.JWT +{ + + /// + /// jwt token 生成器 + /// + public interface IJwtTokenBuilder + { + /// + /// 获取Token负载 + /// + /// 登录Token + /// 负载信息 + LoginUserInfo GetPlayload(string token); + /// + /// 获取Token + /// + /// 负载 + /// Token对象 + string GetToken(LoginUserInfo playload); + } +} \ No newline at end of file diff --git a/Falcon.SugarApi/JWT/IServiceCollectionExtend.cs b/Falcon.SugarApi/JWT/IServiceCollectionExtend.cs new file mode 100644 index 0000000..a598a30 --- /dev/null +++ b/Falcon.SugarApi/JWT/IServiceCollectionExtend.cs @@ -0,0 +1,56 @@ +using Falcon.SugarApi.Encryption; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; + +namespace Falcon.SugarApi.JWT +{ + /// + /// 服务扩展 + /// + public static class IServiceCollectionExtend + { + /// + /// 注册Falcon.SqlSugar.JWT + /// + /// 服务集合 + /// 服务集合 + public static IServiceCollection AddFalconJWT(this IServiceCollection services) + => services.AddFalconJWT(null); + + /// + /// 注册Falcon.SqlSugar.JWT.JwtContext,并配置相关参数 + /// 消费端通过JwtContext注入 + /// + /// 服务集合 + /// 参数创建器 + /// 服务集合 + public static IServiceCollection AddFalconJWT(this IServiceCollection services, Action? OptionBuilder) { + services.AddSingleton(sp => { + var option = new JwtContext(); + var jtb = sp.GetService(); + if (jtb == null) { + var aesc = sp.GetService() ?? new AESConfig(); + jtb = new AESTokenbuilder(aesc, option); + } + option.JwtTokenBuilder = jtb; + + var ulj = sp.GetService(); + if (ulj != null) { + option.UserLogin = ulj; + } + + OptionBuilder?.Invoke(option); + + if (option.UserLogin == null) { + throw new Exception("必须为JwtOptions提供UserLogin属性!"); + } + if (option.JwtTokenBuilder == null) { + throw new Exception("必须为JwtOptions提供JwtTokenBuilder属性!"); + } + return option; + }); + return services; + } + } +} diff --git a/Falcon.SugarApi/JWT/IUserLogin.cs b/Falcon.SugarApi/JWT/IUserLogin.cs new file mode 100644 index 0000000..c335415 --- /dev/null +++ b/Falcon.SugarApi/JWT/IUserLogin.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace Falcon.SugarApi.JWT +{ + /// + /// 用户登录验证接口 + /// + public interface IUserLogin + { + /// + /// 登录是否成功 + /// + /// 登录凭据 + /// 成功返回用户信息,否则返回null + LoginUserInfo? Login(LoginDto login); + + /// + /// 用户登出 + /// + /// 用户名 + /// True成功,False失败 + bool LogOut(LoginUserInfo userInfo); + + /// + /// 登录用户验证。检查客户端提供的已登录用户是否仍然处于登录中。 + /// + /// 登录凭据 + /// 登录中返回True,否则返回False + bool CheckUserLogin(LoginUserInfo userInfo); + + /// + /// 用户是否具有某个角色 + /// + /// 用户信息 + /// 需要具有的角色组 + /// True具有,False不具有 + bool UserInRoles(LoginUserInfo userInfo, List roles); + } +} diff --git a/Falcon.SugarApi/JWT/JwtContext.cs b/Falcon.SugarApi/JWT/JwtContext.cs new file mode 100644 index 0000000..1e8a1c0 --- /dev/null +++ b/Falcon.SugarApi/JWT/JwtContext.cs @@ -0,0 +1,29 @@ +namespace Falcon.SugarApi.JWT +{ + /// + /// JWT认证上下文 + /// + public class JwtContext + { + /// + /// 验证头的键值 + /// + public string AuthHeaderKey { get; set; } = "auth"; + + /// + /// 用户Token加密的秘钥 + /// + public string SecKey { get; set; } = "fefafwefwf464664f64e64f63"; + + /// + /// jwtToken生成器 + /// + public IJwtTokenBuilder? JwtTokenBuilder { get; set; } + + /// + /// 用户登录管理 + /// + public IUserLogin? UserLogin { get; set; } + + } +} diff --git a/Falcon.SugarApi/JWT/JwtTokenBuilder.cs b/Falcon.SugarApi/JWT/JwtTokenBuilder.cs new file mode 100644 index 0000000..8016c48 --- /dev/null +++ b/Falcon.SugarApi/JWT/JwtTokenBuilder.cs @@ -0,0 +1,64 @@ +using JWT; +using JWT.Algorithms; +using JWT.Serializers; +using Microsoft.AspNetCore.Server.IIS.Core; +using System; +using System.Text; + +namespace Falcon.SugarApi.JWT +{ + /// + /// jwt token 生成器 + /// + public class JwtTokenBuilder : IJwtTokenBuilder + { + /// + /// 使用默认配置实例化 + /// + public JwtTokenBuilder() : this(new JwtContext()) { } + + /// + /// 实例化 + /// + /// JWT认证配置 + public JwtTokenBuilder(JwtContext options) { + Options = options; + } + + /// + /// JWT认证配置 + /// + public JwtContext Options { get; } + + /// + /// 获取Token + /// + /// 负载 + /// Token对象 + public string GetToken(LoginUserInfo playload) { + playload = playload ?? throw new ArgumentNullException(nameof(playload)); + var key = Encoding.UTF8.GetBytes(this.Options.SecKey); + var algorithm = new HMACSHA256Algorithm(); + var serializer = new JsonNetSerializer(); + var urlEncoder = new JwtBase64UrlEncoder(); + var encoder = new JwtEncoder(algorithm, serializer, urlEncoder); + return encoder.Encode(playload, key); + } + + /// + /// 获取Token负载 + /// + /// 登录Token + /// 负载信息 + public LoginUserInfo GetPlayload(string token) { + var key = Encoding.UTF8.GetBytes(this.Options.SecKey); + var serializer = new JsonNetSerializer(); + var dtProvider = new UtcDateTimeProvider(); + var validator = new JwtValidator(serializer, dtProvider); + var urlEncoder = new JwtBase64UrlEncoder(); + var algorithm = new HMACSHA256Algorithm(); + var decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); + return decoder.DecodeToObject(token, key, true); + } + } +} diff --git a/Falcon.SugarApi/JWT/JwtTokenBuilderExtend.cs b/Falcon.SugarApi/JWT/JwtTokenBuilderExtend.cs new file mode 100644 index 0000000..b16b5da --- /dev/null +++ b/Falcon.SugarApi/JWT/JwtTokenBuilderExtend.cs @@ -0,0 +1,52 @@ +using System; + +namespace Falcon.SugarApi.JWT +{ + /// + /// JwtTokenBuilder扩展 + /// + public static class JwtTokenBuilderExtend + { + /// + /// 尝试获取Token + /// + /// 生成器 + /// 用户信息 + /// 生成的token + /// 失败异常 + /// 成功True,失败False + public static bool TryGetToken(this JwtTokenBuilder builder, LoginUserInfo userInfo, out string? token, out Exception? exception) { + try { + token = builder.GetToken(userInfo); + exception = null; + return true; + } + catch (Exception ex) { + token = null; + exception = ex; + return false; + } + } + + /// + /// 尝试获取用户信息 + /// + /// jwt创建器 + /// token + /// 用户信息 + /// 异常 + /// True成功,False失败 + public static bool TryGetPlayload(this JwtTokenBuilder builder, string token, out LoginUserInfo? userInfo, out Exception? exception) { + try { + userInfo = builder.GetPlayload(token); + exception = null; + return true; + } + catch (Exception ex) { + userInfo = null; + exception = ex; + return false; + } + } + } +} diff --git a/Falcon.SugarApi/JWT/LoginDto.cs b/Falcon.SugarApi/JWT/LoginDto.cs new file mode 100644 index 0000000..4a7d67e --- /dev/null +++ b/Falcon.SugarApi/JWT/LoginDto.cs @@ -0,0 +1,17 @@ +namespace Falcon.SugarApi.JWT +{ + /// + /// 登录信息 + /// + public class LoginDto + { + /// + /// 用户名 + /// + public string UserName { get; set; } + /// + /// 密码 + /// + public string Password { get; set; } + } +} diff --git a/Falcon.SugarApi/JWT/LoginUserInfo.cs b/Falcon.SugarApi/JWT/LoginUserInfo.cs new file mode 100644 index 0000000..9ea2c65 --- /dev/null +++ b/Falcon.SugarApi/JWT/LoginUserInfo.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace Falcon.SugarApi.JWT +{ + /// + /// JWT载荷信息 + /// + public class LoginUserInfo + + { + /// + /// 登录用户名 + /// + public string? UserName { get; set; } + /// + /// 用户角色 + /// + public List Roles { get; set; } = new List(); + /// + /// 其他扩展用户信息 + /// + public Dictionary UserInfoExtend { get; set; } = new Dictionary(); + /// + /// 登录时间 + /// + public DateTime? LoginTime { get; set; } + } +} diff --git a/Falcon.SugarApi/JWT/TokenDto.cs b/Falcon.SugarApi/JWT/TokenDto.cs new file mode 100644 index 0000000..0871d21 --- /dev/null +++ b/Falcon.SugarApi/JWT/TokenDto.cs @@ -0,0 +1,21 @@ +namespace Falcon.SugarApi.JWT +{ + /// + /// 登录凭据 + /// + public class TokenDto + { + /// + /// 登录结果 + /// + public bool Success { get; set; } + /// + /// 信息 + /// + public string Message { get; set; } + /// + /// 凭据 + /// + public string Token { get; set; } + } +} diff --git a/Falcon.SugarApi/JWT/readme.md b/Falcon.SugarApi/JWT/readme.md new file mode 100644 index 0000000..ecc3437 --- /dev/null +++ b/Falcon.SugarApi/JWT/readme.md @@ -0,0 +1,7 @@ +初始化过程: +1. 初始化相关表: +2. 初始化用户系统:检查是否存在用户,如果没有用户插入默认管理员用户,后期可以删除。 +2. 初始化角色系统: +> 1. 检查系统所有控制器,查询是否标注ApiExplorerSettings特性,如果标注查询是否定义GroupName,不为空则列为角色之一。 +> 2. 检查角色表,如果发现缺少某角色则插入。检查是否存在超管角色,如果不存在则插入 +