增加安全组件JwtContext,通过IServiceCollection.AddFalconJWT注册。

This commit is contained in:
falcon 2022-11-29 14:32:17 +08:00
parent 37a6942020
commit 7157dac6bf
15 changed files with 497 additions and 2 deletions

View File

@ -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
{
/// <summary>
/// JwtTokenBuilderTest
/// </summary>
[TestClass]
public class JwtTokenBuilderTest
{
/// <summary>
/// Token获取测试
/// </summary>
[TestMethod]
public void GetTokenTest() {
var loginTime = DateTime.Now;
var playload = new LoginUserInfo {
UserName = "abdc",
LoginTime = loginTime,
Roles = new List<string> { "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"));
}
}
}

View File

@ -1,10 +1,15 @@
namespace Falcon.SugarApi.Encryption using Microsoft.Extensions.Options;
namespace Falcon.SugarApi.Encryption
{ {
/// <summary> /// <summary>
/// AES加密算法配置 /// AES加密算法配置
/// </summary> /// </summary>
public class AESConfig public class AESConfig : IOptions<AESConfig>
{ {
/// <inheritdoc />
public AESConfig Value => this;
private int keyLength = 32; private int keyLength = 32;
/// <summary> /// <summary>
@ -16,5 +21,6 @@
/// 秘钥字符表 /// 秘钥字符表
/// </summary> /// </summary>
public string KeyChars { get; set; } = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789,.!/*\"; public string KeyChars { get; set; } = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789,.!/*\";
} }
} }

View File

@ -12,6 +12,7 @@
<ItemGroup> <ItemGroup>
<!--<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />--> <!--<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />-->
<PackageReference Include="JWT" Version="9.0.3" />
<PackageReference Include="SqlSugarCore" Version="5.1.3.27" /> <PackageReference Include="SqlSugarCore" Version="5.1.3.27" />
<PackageReference Include="SugarRedis" Version="1.3.0" /> <PackageReference Include="SugarRedis" Version="1.3.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />

View File

@ -0,0 +1,42 @@
using Falcon.SugarApi.Encryption;
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// 使用AES算法生成Token
/// </summary>
public class AESTokenbuilder : IJwtTokenBuilder
{
/// <summary>
/// AES加密算法
/// </summary>
public IAESEncryption AES { get; set; }
/// <summary>
/// jwt生成参数
/// </summary>
public JwtContext Jwt { get; }
/// <summary>
/// 构造AES的Token
/// </summary>
/// <param name="config">AES参数</param>
/// <param name="jwt">jwt生成参数</param>
public AESTokenbuilder(AESConfig config, JwtContext jwt) {
Jwt = jwt;
AES = new AESProvider(config);
}
/// <inheritdoc>/
public LoginUserInfo GetPlayload(string token) {
token.ThrowNullExceptionWhenNull();
return this.AES.Decrypt<LoginUserInfo>(Jwt.SecKey, token);
}
/// <inheritdoc>/
public string GetToken(LoginUserInfo playload) {
playload.ThrowNullExceptionWhenNull();
return this.AES.Encrypt(Jwt.SecKey, playload);
}
}
}

View File

@ -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
{
/// <summary>
/// 验证
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiAuthorizationAttribute : Attribute, IAuthorizationFilter
{
/// <summary>
/// 用户需要具有的角色
/// </summary>
public List<string> Roles { get; set; } = new List<string>();
/// <inheritdoc/>
public void OnAuthorization(AuthorizationFilterContext context) {
if (context.Filters.Any(f => f is IAllowAnonymousFilter)) {
return;
}
var option = context.HttpContext.RequestServices.GetRequiredService<JwtContext>();
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;
}
/// <summary>
/// 返回授权失败
/// </summary>
/// <param name="context">上下文</param>
private static void Unauthorized(AuthorizationFilterContext context) {
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
}
}

View File

@ -0,0 +1,22 @@
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// jwt token 生成器
/// </summary>
public interface IJwtTokenBuilder
{
/// <summary>
/// 获取Token负载
/// </summary>
/// <param name="token">登录Token</param>
/// <returns>负载信息</returns>
LoginUserInfo GetPlayload(string token);
/// <summary>
/// 获取Token
/// </summary>
/// <param name="playload">负载</param>
/// <returns>Token对象</returns>
string GetToken(LoginUserInfo playload);
}
}

View File

@ -0,0 +1,56 @@
using Falcon.SugarApi.Encryption;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// 服务扩展
/// </summary>
public static class IServiceCollectionExtend
{
/// <summary>
/// 注册Falcon.SqlSugar.JWT
/// </summary>
/// <param name="services">服务集合</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddFalconJWT(this IServiceCollection services)
=> services.AddFalconJWT(null);
/// <summary>
/// 注册Falcon.SqlSugar.JWT.JwtContext并配置相关参数
/// <para>消费端通过JwtContext注入</para>
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="OptionBuilder">参数创建器</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddFalconJWT(this IServiceCollection services, Action<JwtContext>? OptionBuilder) {
services.AddSingleton<JwtContext>(sp => {
var option = new JwtContext();
var jtb = sp.GetService<IJwtTokenBuilder>();
if (jtb == null) {
var aesc = sp.GetService<AESConfig>() ?? new AESConfig();
jtb = new AESTokenbuilder(aesc, option);
}
option.JwtTokenBuilder = jtb;
var ulj = sp.GetService<IUserLogin>();
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;
}
}
}

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// 用户登录验证接口
/// </summary>
public interface IUserLogin
{
/// <summary>
/// 登录是否成功
/// </summary>
/// <param name="login">登录凭据</param>
/// <returns>成功返回用户信息否则返回null</returns>
LoginUserInfo? Login(LoginDto login);
/// <summary>
/// 用户登出
/// </summary>
/// <param name="userInfo">用户名</param>
/// <returns>True成功False失败</returns>
bool LogOut(LoginUserInfo userInfo);
/// <summary>
/// 登录用户验证。检查客户端提供的已登录用户是否仍然处于登录中。
/// </summary>
/// <param name="userInfo">登录凭据</param>
/// <returns>登录中返回True否则返回False</returns>
bool CheckUserLogin(LoginUserInfo userInfo);
/// <summary>
/// 用户是否具有某个角色
/// </summary>
/// <param name="userInfo">用户信息</param>
/// <param name="roles">需要具有的角色组</param>
/// <returns>True具有,False不具有</returns>
bool UserInRoles(LoginUserInfo userInfo, List<string> roles);
}
}

View File

@ -0,0 +1,29 @@
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// JWT认证上下文
/// </summary>
public class JwtContext
{
/// <summary>
/// 验证头的键值
/// </summary>
public string AuthHeaderKey { get; set; } = "auth";
/// <summary>
/// 用户Token加密的秘钥
/// </summary>
public string SecKey { get; set; } = "fefafwefwf464664f64e64f63";
/// <summary>
/// jwtToken生成器
/// </summary>
public IJwtTokenBuilder? JwtTokenBuilder { get; set; }
/// <summary>
/// 用户登录管理
/// </summary>
public IUserLogin? UserLogin { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// jwt token 生成器
/// </summary>
public class JwtTokenBuilder : IJwtTokenBuilder
{
/// <summary>
/// 使用默认配置实例化
/// </summary>
public JwtTokenBuilder() : this(new JwtContext()) { }
/// <summary>
/// 实例化
/// </summary>
/// <param name="options">JWT认证配置</param>
public JwtTokenBuilder(JwtContext options) {
Options = options;
}
/// <summary>
/// JWT认证配置
/// </summary>
public JwtContext Options { get; }
/// <summary>
/// 获取Token
/// </summary>
/// <param name="playload">负载</param>
/// <returns>Token对象</returns>
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);
}
/// <summary>
/// 获取Token负载
/// </summary>
/// <param name="token">登录Token</param>
/// <returns>负载信息</returns>
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<LoginUserInfo>(token, key, true);
}
}
}

View File

@ -0,0 +1,52 @@
using System;
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// JwtTokenBuilder扩展
/// </summary>
public static class JwtTokenBuilderExtend
{
/// <summary>
/// 尝试获取Token
/// </summary>
/// <param name="builder">生成器</param>
/// <param name="userInfo">用户信息</param>
/// <param name="token">生成的token</param>
/// <param name="exception">失败异常</param>
/// <returns>成功True失败False</returns>
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;
}
}
/// <summary>
/// 尝试获取用户信息
/// </summary>
/// <param name="builder">jwt创建器</param>
/// <param name="token">token</param>
/// <param name="userInfo">用户信息</param>
/// <param name="exception">异常</param>
/// <returns>True成功False失败</returns>
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;
}
}
}
}

View File

@ -0,0 +1,17 @@
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// 登录信息
/// </summary>
public class LoginDto
{
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; }
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// JWT载荷信息
/// </summary>
public class LoginUserInfo
{
/// <summary>
/// 登录用户名
/// </summary>
public string? UserName { get; set; }
/// <summary>
/// 用户角色
/// </summary>
public List<string> Roles { get; set; } = new List<string>();
/// <summary>
/// 其他扩展用户信息
/// </summary>
public Dictionary<string, object> UserInfoExtend { get; set; } = new Dictionary<string, object>();
/// <summary>
/// 登录时间
/// </summary>
public DateTime? LoginTime { get; set; }
}
}

View File

@ -0,0 +1,21 @@
namespace Falcon.SugarApi.JWT
{
/// <summary>
/// 登录凭据
/// </summary>
public class TokenDto
{
/// <summary>
/// 登录结果
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 信息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 凭据
/// </summary>
public string Token { get; set; }
}
}

View File

@ -0,0 +1,7 @@
初始化过程:
1. 初始化相关表:
2. 初始化用户系统:检查是否存在用户,如果没有用户插入默认管理员用户,后期可以删除。
2. 初始化角色系统:
> 1. 检查系统所有控制器查询是否标注ApiExplorerSettings特性如果标注查询是否定义GroupName不为空则列为角色之一。
> 2. 检查角色表,如果发现缺少某角色则插入。检查是否存在超管角色,如果不存在则插入