This commit is contained in:
cjd
2025-11-04 21:09:16 +08:00
parent 8260e293c7
commit bb90a020dc
592 changed files with 61749 additions and 27 deletions

View File

@@ -0,0 +1,16 @@
using System.Threading.Tasks;
namespace DouyinApi.Extensions.Authorizations.Behaviors
{
public interface IUserBehaviorService
{
Task<bool> CreateOrUpdateUserAccessByUid();
Task<bool> RemoveAllUserAccessByUid();
Task<bool> CheckUserIsNormal();
Task<bool> CheckTokenIsNormal();
}
}

View File

@@ -0,0 +1,48 @@
using DouyinApi.Common.HttpContextUser;
using DouyinApi.IServices;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
namespace DouyinApi.Extensions.Authorizations.Behaviors
{
public class UserBehaviorService : IUserBehaviorService
{
private readonly IUser _user;
private readonly ISysUserInfoServices _sysUserInfoServices;
private readonly ILogger<UserBehaviorService> _logger;
private readonly string _uid;
private readonly string _token;
public UserBehaviorService(IUser user
, ISysUserInfoServices sysUserInfoServices
, ILogger<UserBehaviorService> logger)
{
_user = user;
_sysUserInfoServices = sysUserInfoServices;
_logger = logger;
_uid = _user.ID.ObjToString();
_token = _user.GetToken();
}
public Task<bool> CheckTokenIsNormal()
{
throw new System.NotImplementedException();
}
public Task<bool> CheckUserIsNormal()
{
throw new System.NotImplementedException();
}
public Task<bool> CreateOrUpdateUserAccessByUid()
{
throw new System.NotImplementedException();
}
public Task<bool> RemoveAllUserAccessByUid()
{
throw new System.NotImplementedException();
}
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using DouyinApi.Common;
using DouyinApi.Common.AppConfig;
using Microsoft.IdentityModel.Tokens;
namespace DouyinApi.AuthHelper.OverWrite
{
public class JwtHelper
{
/// <summary>
/// 颁发JWT字符串
/// </summary>
/// <param name="tokenModel"></param>
/// <returns></returns>
public static string IssueJwt(TokenModelJwt tokenModel)
{
string iss = AppSettings.app(new string[] { "Audience", "Issuer" });
string aud = AppSettings.app(new string[] { "Audience", "Audience" });
string secret = AppSecretConfig.Audience_Secret_String;
//var claims = new Claim[] //old
var claims = new List<Claim>
{
/*
* 特别重要:
1、这里将用户的部分信息比如 uid 存到了Claim 中,如果你想知道如何在其他地方将这个 uid从 Token 中取出来请看下边的SerializeJwt() 方法,或者在整个解决方案,搜索这个方法,看哪里使用了!
2、你也可以研究下 HttpContext.User.Claims ,具体的你可以看看 Policys/PermissionHandler.cs 类中是如何使用的。
*/
new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ToString()),
new Claim(JwtRegisteredClaimNames.Iat, $"{DateTime.Now.DateToTimeStamp()}"),
new Claim(JwtRegisteredClaimNames.Nbf,$"{DateTime.Now.DateToTimeStamp()}") ,
//这个就是过期时间目前是过期1000秒可自定义注意JWT有自己的缓冲过期时间
new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds()}"),
new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(1000).ToString()),
new Claim(JwtRegisteredClaimNames.Iss,iss),
new Claim(JwtRegisteredClaimNames.Aud,aud),
//new Claim(ClaimTypes.Role,tokenModel.Role),//为了解决一个用户多个角色(比如Admin,System),用下边的方法
};
// 可以将一个用户的多个角色全部赋予;
// 作者DX 提供技术支持;
claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s)));
//秘钥 (SymmetricSecurityKey 对安全性的要求,密钥的长度太短会报出异常)
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwt = new JwtSecurityToken(
issuer: iss,
claims: claims,
signingCredentials: creds);
var jwtHandler = new JwtSecurityTokenHandler();
var encodedJwt = jwtHandler.WriteToken(jwt);
return encodedJwt;
}
/// <summary>
/// 解析
/// </summary>
/// <param name="jwtStr"></param>
/// <returns></returns>
public static TokenModelJwt SerializeJwt(string jwtStr)
{
var jwtHandler = new JwtSecurityTokenHandler();
TokenModelJwt tokenModelJwt = new TokenModelJwt();
// token校验
if (jwtStr.IsNotEmptyOrNull() && jwtHandler.CanReadToken(jwtStr))
{
JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
object role;
jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role);
tokenModelJwt = new TokenModelJwt
{
Uid = (jwtToken.Id).ObjToLong(),
Role = role != null ? role.ObjToString() : "",
};
}
return tokenModelJwt;
}
public static bool customSafeVerify(string token)
{
var jwtHandler = new JwtSecurityTokenHandler();
var symmetricKeyAsBase64 = AppSecretConfig.Audience_Secret_String;
var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
var signingKey = new SymmetricSecurityKey(keyByteArray);
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
var jwt = jwtHandler.ReadJwtToken(token);
return jwt.RawSignature == Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.CreateEncodedSignature(jwt.RawHeader + "." + jwt.RawPayload, signingCredentials);
}
}
/// <summary>
/// 令牌
/// </summary>
public class TokenModelJwt
{
/// <summary>
/// Id
/// </summary>
public long Uid { get; set; }
/// <summary>
/// 角色
/// </summary>
public string Role { get; set; }
/// <summary>
/// 职能
/// </summary>
public string Work { get; set; }
}
}

View File

@@ -0,0 +1,50 @@
using DouyinApi.Model;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using DouyinApi.Common.HttpContextUser;
namespace DouyinApi.AuthHelper
{
public class ApiResponseHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IUser _user;
public ApiResponseHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUser user) : base(options, logger, encoder, clock)
{
_user = user;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
throw new NotImplementedException();
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.ContentType = "application/json";
Response.StatusCode = StatusCodes.Status401Unauthorized;
await Response.WriteAsync(JsonConvert.SerializeObject((new ApiResponse(StatusCode.CODE401)).MessageModel));
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
Response.ContentType = "application/json";
if (_user.MessageModel != null)
{
Response.StatusCode = _user.MessageModel.status;
await Response.WriteAsync(JsonConvert.SerializeObject(_user.MessageModel));
}
else
{
Response.StatusCode = StatusCodes.Status403Forbidden;
await Response.WriteAsync(JsonConvert.SerializeObject((new ApiResponse(StatusCode.CODE403)).MessageModel));
}
}
}
}

View File

@@ -0,0 +1,45 @@
using DouyinApi.Model.ViewModels;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace DouyinApi.AuthHelper
{
/// <summary>
/// JWTToken生成类
/// </summary>
public class JwtToken
{
/// <summary>
/// 获取基于JWT的Token
/// </summary>
/// <param name="claims">需要在登录的时候配置</param>
/// <param name="permissionRequirement">在startup中定义的参数</param>
/// <returns></returns>
public static TokenInfoViewModel BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
{
var now = DateTime.Now;
// 实例化JwtSecurityToken
var jwt = new JwtSecurityToken(
issuer: permissionRequirement.Issuer,
audience: permissionRequirement.Audience,
claims: claims,
notBefore: now,
expires: now.Add(permissionRequirement.Expiration),
signingCredentials: permissionRequirement.SigningCredentials
);
// 生成 Token
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
//打包返回前台
var responseJson = new TokenInfoViewModel
{
success = true,
token = encodedJwt,
expires_in = permissionRequirement.Expiration.TotalSeconds,
token_type = "Bearer"
};
return responseJson;
}
}
}

View File

@@ -0,0 +1,269 @@
using DouyinApi.Common;
using DouyinApi.Common.Helper;
using DouyinApi.Common.HttpContextUser;
using DouyinApi.IServices;
using DouyinApi.Model;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DouyinApi.Common.Swagger;
using DouyinApi.Model.Models;
namespace DouyinApi.AuthHelper
{
/// <summary>
/// 权限授权处理器
/// </summary>
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
/// <summary>
/// 验证方案提供对象
/// </summary>
public IAuthenticationSchemeProvider Schemes { get; set; }
private readonly IRoleModulePermissionServices _roleModulePermissionServices;
private readonly IHttpContextAccessor _accessor;
private readonly ISysUserInfoServices _userServices;
private readonly IUser _user;
/// <summary>
/// 构造函数注入
/// </summary>
/// <param name="schemes"></param>
/// <param name="roleModulePermissionServices"></param>
/// <param name="accessor"></param>
/// <param name="userServices"></param>
/// <param name="user"></param>
public PermissionHandler(IAuthenticationSchemeProvider schemes,
IRoleModulePermissionServices roleModulePermissionServices, IHttpContextAccessor accessor,
ISysUserInfoServices userServices, IUser user)
{
_accessor = accessor;
_userServices = userServices;
_user = user;
Schemes = schemes;
_roleModulePermissionServices = roleModulePermissionServices;
}
// 重写异步处理程序
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var httpContext = _accessor.HttpContext;
// 获取系统中所有的角色和菜单的关系集合
if (!requirement.Permissions.Any())
{
var data = await _roleModulePermissionServices.RoleModuleMaps();
var list = new List<PermissionItem>();
// ids4和jwt切换
// ids4
if (Permissions.IsUseIds4)
{
list = (from item in data
where item.IsDeleted == false
orderby item.Id
select new PermissionItem
{
Url = item.Module?.LinkUrl,
Role = item.Role?.Id.ObjToString(),
}).ToList();
}
// jwt
else
{
list = (from item in data
where item.IsDeleted == false
orderby item.Id
select new PermissionItem
{
Url = item.Module?.LinkUrl,
Role = item.Role?.Name.ObjToString(),
}).ToList();
}
requirement.Permissions = list;
}
if (httpContext != null)
{
var questUrl = httpContext.Request.Path.Value.ToLower();
// 整体结构类似认证中间件UseAuthentication的逻辑具体查看开源地址
// https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Core/src/AuthenticationMiddleware.cs
httpContext.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = httpContext.Request.Path,
OriginalPathBase = httpContext.Request.PathBase
});
// Give any IAuthenticationRequestHandler schemes a chance to handle the request
// 主要作用是: 判断当前是否需要进行远程验证,如果是就进行远程验证
var handlers = httpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
if (await handlers.GetHandlerAsync(httpContext, scheme.Name) is IAuthenticationRequestHandler
handler && await handler.HandleRequestAsync())
{
context.Fail();
return;
}
}
//判断请求是否拥有凭据,即有没有登录
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await httpContext.AuthenticateAsync(defaultAuthenticate.Name);
// 是否开启测试环境
var isTestCurrent = AppSettings.app(new string[] { "AppSettings", "UseLoadTest" }).ObjToBool();
//result?.Principal不为空即登录成功
if (result?.Principal != null || isTestCurrent || httpContext.IsSuccessSwagger())
{
if (!isTestCurrent) httpContext.User = result.Principal;
//应该要先校验用户的信息 再校验菜单权限相关的
// JWT模式下校验当前用户状态
// IDS4也可以校验可以通过服务或者接口形式
SysUserInfo user = new();
if (!Permissions.IsUseIds4)
{
//校验用户
user = await _userServices.QueryById(_user.ID, true);
if (user == null)
{
_user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户不存在或已被删除").MessageModel;
context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg));
return;
}
if (user.IsDeleted)
{
_user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户已被删除,禁止登录!").MessageModel;
context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg));
return;
}
if (!user.Enable)
{
_user.MessageModel = new ApiResponse(StatusCode.CODE401, "用户已被禁用!禁止登录!").MessageModel;
context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg));
return;
}
}
// 判断token是否过期过期则重新登录
var isExp = false;
// ids4和jwt切换
// ids4
if (Permissions.IsUseIds4)
{
isExp = (httpContext.User.Claims.FirstOrDefault(s => s.Type == "exp")?.Value) != null &&
DateHelper.StampToDateTime(httpContext.User.Claims
.FirstOrDefault(s => s.Type == "exp")?.Value) >= DateTime.Now;
}
else
{
// jwt
isExp =
(httpContext.User.Claims.FirstOrDefault(s => s.Type == ClaimTypes.Expiration)
?.Value) != null &&
DateTime.Parse(httpContext.User.Claims
.FirstOrDefault(s => s.Type == ClaimTypes.Expiration)?.Value) >= DateTime.Now;
}
if (!isExp)
{
context.Fail(new AuthorizationFailureReason(this, "授权已过期,请重新授权"));
return;
}
//校验签发时间
if (!Permissions.IsUseIds4)
{
var value = httpContext.User.Claims
.FirstOrDefault(s => s.Type == JwtRegisteredClaimNames.Iat)?.Value;
if (value != null)
{
if (user.CriticalModifyTime > value.ObjToDate())
{
_user.MessageModel = new ApiResponse(StatusCode.CODE401, "很抱歉,授权已失效,请重新授权")
.MessageModel;
context.Fail(new AuthorizationFailureReason(this, _user.MessageModel.msg));
return;
}
}
}
// 获取当前用户的角色信息
var currentUserRoles = new List<string>();
currentUserRoles = (from item in httpContext.User.Claims
where item.Type == ClaimTypes.Role
select item.Value).ToList();
if (!currentUserRoles.Any())
{
currentUserRoles = (from item in httpContext.User.Claims
where item.Type == "role"
select item.Value).ToList();
}
//超级管理员 默认拥有所有权限
if (currentUserRoles.All(s => s != "SuperAdmin"))
{
var isMatchRole = false;
var permisssionRoles =
requirement.Permissions.Where(w => currentUserRoles.Contains(w.Role));
foreach (var item in permisssionRoles)
{
try
{
if (Regex.Match(questUrl, item.Url?.ObjToString().ToLower())?.Value == questUrl)
{
isMatchRole = true;
break;
}
}
catch (Exception)
{
// ignored
}
}
//验证权限
if (currentUserRoles.Count <= 0 || !isMatchRole)
{
context.Fail();
return;
}
}
context.Succeed(requirement);
return;
}
}
//判断没有登录时是否访问登录的url,并且是Post请求并且是form表单提交类型否则为失败
if (!(questUrl.Equals(requirement.LoginPath.ToLower(), StringComparison.Ordinal) &&
(!httpContext.Request.Method.Equals("POST") || !httpContext.Request.HasFormContentType)))
{
context.Fail();
return;
}
}
//context.Succeed(requirement);
}
}
}

View File

@@ -0,0 +1,19 @@
namespace DouyinApi.AuthHelper
{
/// <summary>
/// 用户或角色或其他凭据实体,就像是订单详情一样
/// 之前的名字是 Permission
/// </summary>
public class PermissionItem
{
/// <summary>
/// 用户或角色或其他凭据名称
/// </summary>
public virtual string Role { get; set; }
/// <summary>
/// 请求Url
/// </summary>
public virtual string Url { get; set; }
}
}

View File

@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
namespace DouyinApi.AuthHelper
{
/// <summary>
/// 必要参数类,类似一个订单信息
/// 继承 IAuthorizationRequirement用于设计自定义权限处理器PermissionHandler
/// 因为AuthorizationHandler 中的泛型参数 TRequirement 必须继承 IAuthorizationRequirement
/// </summary>
public class PermissionRequirement : IAuthorizationRequirement
{
/// <summary>
/// 用户权限集合,一个订单包含了很多详情,
/// 同理,一个网站的认证发行中,也有很多权限详情(这里是Role和URL的关系)
/// </summary>
public List<PermissionItem> Permissions { get; set; }
/// <summary>
/// 无权限action
/// </summary>
public string DeniedAction { get; set; }
/// <summary>
/// 认证授权类型
/// </summary>
public string ClaimType { internal get; set; }
/// <summary>
/// 请求路径
/// </summary>
public string LoginPath { get; set; } = "/Api/Login";
/// <summary>
/// 发行人
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// 订阅人
/// </summary>
public string Audience { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public TimeSpan Expiration { get; set; }
/// <summary>
/// 签名验证
/// </summary>
public SigningCredentials SigningCredentials { get; set; }
/// <summary>
/// 构造
/// </summary>
/// <param name="deniedAction">拒约请求的url</param>
/// <param name="permissions">权限集合</param>
/// <param name="claimType">声明类型</param>
/// <param name="issuer">发行人</param>
/// <param name="audience">订阅人</param>
/// <param name="signingCredentials">签名验证实体</param>
/// <param name="expiration">过期时间</param>
public PermissionRequirement(string deniedAction, List<PermissionItem> permissions, string claimType, string issuer, string audience, SigningCredentials signingCredentials, TimeSpan expiration)
{
ClaimType = claimType;
DeniedAction = deniedAction;
Permissions = permissions;
Issuer = issuer;
Audience = audience;
Expiration = expiration;
SigningCredentials = signingCredentials;
}
}
}