🔐 认证系统
Ape-Volo-Admin 采用 JWT (JSON Web Token) 作为主要的身份认证机制,提供无状态、安全、高效的用户认证解决方案。
🎯 认证机制概述
JWT 认证流程(说明)
用户登录
- 客户端向服务器发送登录请求,包含用户名和密码。
凭据校验
- 服务器接收到请求后,将用户名和密码发送至数据库进行校验。
- 数据库校验后返回对应的用户信息(如校验失败则返回错误信息)。
生成 JWT Token
- 服务器根据用户信息创建 JWT(JSON Web Token),通常包含用户 ID、角色、过期时间等数据,并使用服务器密钥签名。
Token 返回与存储
- 服务器将生成的 Token 返回给客户端。
- 客户端将 Token 存储在本地(如 LocalStorage、SessionStorage 或 Cookie),用于后续的接口调用。
携带 Token 的 API 请求
- 客户端在后续的 API 请求中,将 Token 放入 HTTP 请求的 Authorization Header(格式为
Bearer <token>)。
- 客户端在后续的 API 请求中,将 Token 放入 HTTP 请求的 Authorization Header(格式为
Token 校验
- 服务器收到 API 请求后,首先从 Header 中获取 Token。
- 服务器验证 Token 的合法性(签名、是否过期等)。
- 若 Token 校验通过,则进行后续业务处理;否则拒绝访问并返回错误(如 401 未授权)。
返回数据或拒绝访问
- 如果校验成功,服务器返回请求数据。
- 如果校验失败,服务器返回拒绝访问提示(如 Token 失效、签名不正确等)。
🔧 JWT 配置
配置文件设置
在 appsettings.json 中配置 JWT 相关参数:
yaml
JwtAuth:
Audience: "http://localhost" # JWT 接收方
Issuer: "http://localhost" # JWT 签发方
SecurityKey: 5ixKD0BkJxYYroZTvdPs3w9NWRoiUacN # JWT 密钥
Expires: "12" # Token 有效期(小时)
RefreshTokenExpires: "168" # 刷新 Token 缓冲有效期(小时)
LoginPath: "/auth/login" # 登录 API 路径🛠️ 核心组件
JWT 认证与授权中间件注册
位置:Ape.Volo.Infrastructure/Extensions/AuthorizationSetup.cs
csharp
public static class AuthorizationSetup
{
public static void AddAuthorizationSetup(this IServiceCollection services)
{
if (services.IsNull()) throw new ArgumentNullException(nameof(services));
services.AddScoped<IHttpUser, HttpUser>();
services.AddScoped<ITokenService, TokenService>();
var jwtAuthOptions = App.GetOptions<JwtAuthOptions>();
var permissionRequirement = new PermissionRequirement();
// 自定义策略授权
services.AddAuthorization(options =>
{
options.AddPolicy(AuthConstants.AuthPolicyName,
policy => policy.Requirements.Add(permissionRequirement));
});
// 开启Bearer认证
services.AddAuthentication(o =>
{
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = nameof(ApiResponseHandler);
o.DefaultForbidScheme = nameof(ApiResponseHandler);
})
// 添加JwtBearer服务
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAuthOptions.Issuer,
ValidateAudience = true,
ValidAudience = jwtAuthOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAuthOptions.SecurityKey)),
LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken,
TokenValidationParameters validationParameters) =>
{
if (expires == null)
{
return true;
}
return expires.Value > DateTime.UtcNow;
},
ValidateLifetime = true
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
// 如果过期,把过期信息添加到头部
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Append("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
})
.AddScheme<AuthenticationSchemeOptions, ApiResponseHandler>(nameof(ApiResponseHandler), _ => { });
services.AddScoped<IAuthorizationHandler, PermissionHandler>();
services.AddSingleton(permissionRequirement);
}
}JWT 自定义策略
csharp
// 自定义策略授权
services.AddAuthorization(options =>
{
options.AddPolicy(AuthConstants.AuthPolicyName,
policy => policy.Requirements.Add(permissionRequirement));
});JWT 自定义策略
csharp
// 自定义策略授权
services.AddAuthorization(options =>
{
options.AddPolicy(AuthConstants.AuthPolicyName,
policy => policy.Requirements.Add(permissionRequirement));
});JWT 自定义授权处理器
位置:Ape.Volo.Infrastructure/Authentication/PermissionHandler.cs
csharp
// 自定义授权处理器
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
/// <summary>
/// 验证方案提供对象
/// </summary>
public IAuthenticationSchemeProvider Schemes { get; set; }
private readonly IPermissionService _permissionService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUserService _userService;
private readonly ISettingService _settingService;
private readonly IBrowserDetector _browserDetector;
private readonly ISearcher _ipSearcher;
private readonly ITokenBlacklistService _tokenBlacklistService;
private readonly ILogger<PermissionHandler> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="schemes"></param>
/// <param name="httpContextAccessor"></param>
/// <param name="permissionService"></param>
/// <param name="userService"></param>
/// <param name="settingService"></param>
/// <param name="browserDetector"></param>
/// <param name="searcher"></param>
/// <param name="tokenBlacklistService"></param>
/// <param name="logger"></param>
public PermissionHandler(IAuthenticationSchemeProvider schemes, IHttpContextAccessor httpContextAccessor,
IPermissionService permissionService, IUserService userService, ISettingService settingService,
IBrowserDetector browserDetector, ISearcher searcher, ITokenBlacklistService tokenBlacklistService,
ILogger<PermissionHandler> logger)
{
_httpContextAccessor = httpContextAccessor;
Schemes = schemes;
_permissionService = permissionService;
_settingService = settingService;
_userService = userService;
_browserDetector = browserDetector;
_ipSearcher = searcher;
_tokenBlacklistService = tokenBlacklistService;
_logger = logger;
}
// 重写异步处理程序
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var isMatchRole = false;
var httpContext = _httpContextAccessor?.HttpContext;
//请求Url
if (httpContext != null)
{
var requestPath = httpContext.Request.Path.Value?.ToLower();
var requestMethod = httpContext.Request.Method.ToLower();
if (requestPath == "/api/test/SearchOrder")
{
context.Succeed(requirement);
return;
}
//判断请求是否停止
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);
//result.Principal不为空代表http状态存在
if (result is { Principal: not null })
{
httpContext.User = result.Principal;
#region 判断jwt令牌是否已过期
//判断jwt令牌是否已过期
var expirationClaim =
httpContext.User.Claims.FirstOrDefault(s => s.Type == AuthConstants.JwtClaimTypes.Exp);
if (expirationClaim != null)
{
var expTime = Convert.ToInt64(expirationClaim.Value).TicksToDateTime();
var nowTime = DateTime.Now.ToLocalTime();
if (expTime < nowTime)
{
await App.Cache.RemoveAsync(GlobalConstants.CachePrefix.OnlineKey +
App.HttpUser.JwtToken.ToMd5String16());
context.Fail();
return;
}
}
#endregion
#region 用户缓存信息是否已过期
var loginUserInfo = await App.Cache.GetAsync<LoginUserInfo>(
GlobalConstants.CachePrefix.OnlineKey +
App.HttpUser.JwtToken.ToMd5String16());
if (loginUserInfo == null)
{
var tokenMd5 = App.HttpUser.JwtToken.ToMd5String16();
var tokenBlacklist = await _tokenBlacklistService.TableWhere(x => x.AccessToken == tokenMd5)
.FirstAsync();
if (tokenBlacklist.IsNotNull())
{
context.Fail();
return;
}
var netUser = await _userService.QueryByIdAsync(App.HttpUser.Id);
if (netUser.IsNull())
{
context.Fail();
return;
}
var remoteIp = httpContext.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0";
var onlineUser = new LoginUserInfo
{
UserId = netUser.Id,
Account = netUser.UserName,
NickName = netUser.NickName,
DeptId = netUser.DeptId,
DeptName = netUser.Dept.Name,
Ip = remoteIp,
Address = _ipSearcher.Search(remoteIp),
OperatingSystem = _browserDetector.Browser?.OS,
DeviceType = _browserDetector.Browser?.DeviceType,
BrowserName = _browserDetector.Browser?.Name,
Version = _browserDetector.Browser?.Version,
LoginTime = DateTime.Now,
AccessToken = App.HttpUser.JwtToken
};
var onlineKey = onlineUser.AccessToken.ToMd5String16();
var isTrue = await App.Cache.SetAsync(
GlobalConstants.CachePrefix.OnlineKey + onlineKey, onlineUser, TimeSpan.FromHours(2),
null);
if (!isTrue)
{
context.Fail();
return;
}
}
#endregion
#region 开发环境下免鉴权
var noAuthenticationRequired =
await _settingService.GetSettingValue<bool>("NoAuthenticationRequired");
if (App.GetOptions<SystemOptions>().IsQuickDebug && noAuthenticationRequired)
{
context.Succeed(requirement);
return;
}
#endregion
#region 验证IP是否发生变化
var ipClaim =
httpContext.User.Claims.FirstOrDefault(s => s.Type == AuthConstants.JwtClaimTypes.Ip);
if (ipClaim != null)
{
var remoteIp = httpContext.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0";
if (!remoteIp.Equals(ipClaim.Value))
{
//IP已发生变化,执行系统处理逻辑
}
}
#endregion
#region 验证用户Url访问权限
var urlAccessControlList = await _permissionService.GetUrlAccessControlAsync(App.HttpUser.Id);
if (urlAccessControlList.Any() && !requestPath.IsNullOrEmpty())
{
isMatchRole = urlAccessControlList.Any(x =>
x.Url.Equals(requestPath, StringComparison.CurrentCultureIgnoreCase) &&
x.Method.Equals(requestMethod, StringComparison.CurrentCultureIgnoreCase));
}
#endregion
#region 验证角色权限
if (!isMatchRole)
{
try
{
var netUser = await _userService.QueryByIdAsync(App.HttpUser.Id);
var roleCodes = netUser.Roles.Select(x => x.AuthCode).ToList();
if (context.Resource.IsNotNull())
{
var endpointFeature = (IEndpointFeature)((DefaultHttpContext)context.Resource)
.Features.FirstOrDefault(x =>
x.Key.FullName == typeof(IEndpointFeature).FullName).Value;
if (endpointFeature.Endpoint?.Metadata.FirstOrDefault(x =>
x.GetType() == typeof(HasRoleAttribute)) is
HasRoleAttribute
apeVoloAuthorize && apeVoloAuthorize.AuthCodes.Any(roleCodes.Contains))
{
isMatchRole = true;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "验证角色权限时发生错误");
}
}
#endregion
#region 验证按钮权限
if (!isMatchRole)
{
try
{
var authCodes =
await _permissionService.GetAuthCodeAsync(App.HttpUser.Id);
if (context.Resource.IsNotNull())
{
var endpointFeature = (IEndpointFeature)((DefaultHttpContext)context.Resource)
.Features.FirstOrDefault(x =>
x.Key.FullName == typeof(IEndpointFeature).FullName).Value;
if (endpointFeature.Endpoint?.Metadata.FirstOrDefault(x =>
x.GetType() == typeof(HasPermAttribute)) is
HasPermAttribute
apeVoloAuthorize && apeVoloAuthorize.AuthCodes.Any(authCodes.Contains))
{
isMatchRole = true;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "验证按钮权限时发生错误");
}
}
#endregion
#region 用户在线则放行
if (!isMatchRole)
{
try
{
//在线特性,接口拥有ApeVoloOnlineAttribute 直接放行
if (context.Resource.IsNotNull())
{
var endpointFeature = (IEndpointFeature)((DefaultHttpContext)context.Resource)
?.Features.FirstOrDefault(x =>
x.Key.FullName == typeof(IEndpointFeature).FullName).Value;
if (endpointFeature != null)
{
var apeVoloOnline =
endpointFeature.Endpoint?.Metadata.FirstOrDefault(x =>
x.GetType() == typeof(ApeVoloOnlineAttribute)) as
ApeVoloOnlineAttribute;
if (apeVoloOnline.IsNotNull())
{
context.Succeed(requirement);
return;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "验证在线标识权限时发生错误");
}
}
#endregion
if (!isMatchRole)
{
context.Fail();
return;
}
context.Succeed(requirement);
return;
}
}
context.Fail();
return;
}
context.Fail();
}
}JWT 自定义认证响应处理
位置:Ape.Volo.Infrastructure/Authentication/PermissionHandler.cs
csharp
// 自定义认证响应处理
public class ApiResponseHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public ApiResponseHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger,
UrlEncoder encoder) :
base(options, logger, encoder)
{
}
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(new ActionResultVm
{
Status = StatusCodes.Status401Unauthorized,
ActionError = new ActionError(),
Message = App.L.R("Sys.HttpUnauthorized"),
Path = App.HttpContext?.Request.Path.Value?.ToLower()
}.ToJson());
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
var loginUserInfo = await App.Cache.GetAsync<LoginUserInfo>(
GlobalConstants.CachePrefix.OnlineKey +
App.HttpUser.JwtToken.ToMd5String16());
if (loginUserInfo.IsNull())
{
Response.ContentType = "application/json";
Response.StatusCode = StatusCodes.Status401Unauthorized;
await Response.WriteAsync(new ActionResultVm
{
Status = StatusCodes.Status401Unauthorized,
ActionError = new ActionError(),
Message = App.L.R("Sys.HttpUnauthorized"),
Path = App.HttpContext?.Request.Path.Value?.ToLower()
}.ToJson());
}
else
{
Response.ContentType = "application/json";
Response.StatusCode = StatusCodes.Status403Forbidden;
await Response.WriteAsync(new ActionResultVm
{
Status = StatusCodes.Status403Forbidden,
ActionError = new ActionError(),
Message = App.L.R("Sys.HttpForbidden"),
Path = App.HttpContext?.Request.Path.Value?.ToLower()
}.ToJson());
}
}
}JWT Token 接口实现
位置:Ape.Volo.Infrastructure/Authentication/TokenService.cs
csharp
public class TokenService : ITokenService
{
private readonly ILogger<TokenService> _logger;
public TokenService(ILogger<TokenService> logger)
{
_logger = logger;
}
/// <summary>
/// 颁发Token
/// </summary>
/// <param name="loginUserInfo"></param>
/// <param name="refresh"></param>
/// <param name="refreshTime"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public async Task<TokenVo> IssueTokenAsync(LoginUserInfo loginUserInfo, bool refresh = false, long refreshTime = 0)
{
if (loginUserInfo == null)
throw new ArgumentNullException(nameof(loginUserInfo));
var jwtAuthOptions = App.GetOptions<JwtAuthOptions>();
var signinCredentials =
new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAuthOptions.SecurityKey)),
SecurityAlgorithms.HmacSha256);
var nowTime = DateTime.Now;
if (refreshTime == 0)
{
refreshTime = nowTime.AddMinutes(jwtAuthOptions.RefreshTokenExpires).ToUnixTimeStampMillisecond();
}
var cls = new List<Claim>
{
new(AuthConstants.JwtClaimTypes.Jti, loginUserInfo.UserId.ToString()),
new(AuthConstants.JwtClaimTypes.Name, loginUserInfo.Account),
new(AuthConstants.JwtClaimTypes.TenantId, loginUserInfo.TenantId.ToString()),
new(AuthConstants.JwtClaimTypes.DeptId, loginUserInfo.DeptId.ToString()),
new(AuthConstants.JwtClaimTypes.Iat, nowTime.ToUnixTimeStampMillisecond().ToString()),
new(AuthConstants.JwtClaimTypes.Ip, loginUserInfo.Ip),
new(AuthConstants.JwtClaimTypes.RefreshTime,refreshTime.toString())
};
var identity = new ClaimsIdentity(AuthConstants.JwtTokenType);
identity.AddClaims(cls);
var tokeOptions = new JwtSecurityToken(
issuer: jwtAuthOptions.Issuer,
audience: jwtAuthOptions.Audience,
claims: cls,
notBefore: nowTime,
expires: nowTime.AddMinutes(jwtAuthOptions.Expires),
signingCredentials: signinCredentials
);
var expires = nowTime.AddMinutes(jwtAuthOptions.Expires).ToUnixTimeStampMillisecond();
var token = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
if (refresh)
{
return await Task.FromResult(new TokenVo
{
Expires = expires,
TokenType = AuthConstants.JwtTokenType,
RefreshToken = token,
RefreshTokenExpires = refreshTime
});
}
return await Task.FromResult(new TokenVo
{
AccessToken = token,
Expires = expires,
TokenType = AuthConstants.JwtTokenType,
RefreshToken = "",
RefreshTokenExpires = refreshTime
});
}
/// <summary>
/// 读取Token
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task<JwtSecurityToken> ReadJwtToken(string token)
{
try
{
token = token.Replace(AuthConstants.JwtTokenType, "").Trim();
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
if (jwtSecurityTokenHandler.CanReadToken(token))
{
var jwtAuthOptions = App.GetOptions<JwtAuthOptions>();
var signinCredentials =
new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAuthOptions.SecurityKey)),
SecurityAlgorithms.HmacSha256);
JwtSecurityToken jwtSecurityToken = jwtSecurityTokenHandler.ReadJwtToken(token);
var rawSignature = JwtTokenUtilities.CreateEncodedSignature(
jwtSecurityToken.RawHeader + "." + jwtSecurityToken.RawPayload,
signinCredentials);
if (jwtSecurityToken.RawSignature == rawSignature)
{
return await Task.FromResult(jwtSecurityToken);
}
}
}
catch (Exception e)
{
_logger.LogError("Error reading JWT token: {Message}", e.Message);
}
return null;
}
}🎯 鉴权方式
开发环境免鉴权
- 配置文件 IsQuickDebug 为
true并且全局设置 NoAuthenticationRequired 为true时,免鉴权。
API 路由权限验证
- 获取 httpContext.Request.Path.Value?.ToLower()。
- 与用户角色所拥有的 API 路由进行匹配。
用户角色权限验证
csharp
[AttributeUsage(AttributeTargets.Method)]
public class HasRoleAttribute : Attribute
{
public HasRoleAttribute(string[] authCodes)
{
AuthCodes = authCodes;
}
/// <summary>
/// 权限代码
/// </summary>
public string[] AuthCodes { get; }
}
[HasRole(["admin"])]
public Task<ActionResult> GetApiGroup()
{
//略...
}- 获取 接口方法的 HasRoleAttribute 自定义特性。
- 与用户角色权限标识符进行匹配。
按钮权限验证
csharp
[AttributeUsage(AttributeTargets.Method)]
public class HasPermAttribute : Attribute
{
public HasPermAttribute(string[] authCodes)
{
AuthCodes = authCodes;
}
/// <summary>
/// 权限代码
/// </summary>
public string[] AuthCodes { get; }
}
[HasPerm(["sys:api:add"])]
public Task<ActionResult> AddApiGroup()
{
//略...
}- 获取 接口方法的 HasPermAttribute 自定义特性。
- 与用户菜单按钮权限标识符进行匹配。
在线放行验证
csharp
/// <summary>
/// 自定义鉴权特性,在线则可通行
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ApeVoloOnlineAttribute : Attribute
{
}
[ApeVoloOnline]
public Task<ActionResult> QueryByName()
{
//略...
}- 获取 接口方法的 ApeVoloOnlineAttribute 自定义特性。
- 能到这一步说明 Token 还在有效期,所以存在则放行。
🔑 登录实现
登录 API
位置:Ape.Volo.Api/Controllers/Auth/AuthorizationController.cs
csharp
/// <summary>
/// 用户登录
/// </summary>
/// <param name="authUser"></param>
/// <returns></returns>
[HttpPost]
[Route("login")]
[Description("Action.UserLogin")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(LoginResultVo))]
public async Task<ActionResult> Login([FromBody] LoginAuthUser authUser)
{
if (!ModelState.IsValid)
{
var actionError = ModelState.GetErrors();
return Error(actionError);
}
var loginFailedLimitOptions = App.GetOptions<LoginFailedLimitOptions>();
var attempsCacheKey = GlobalConstants.CachePrefix.Attempts + App.HttpContext.Connection.RemoteIpAddress +
authUser.UserName;
LoginAttempt loginAttempt = null;
if (loginFailedLimitOptions.Enabled)
{
loginAttempt = await App.Cache.GetAsync<LoginAttempt>(attempsCacheKey);
if (loginAttempt.IsNull())
{
loginAttempt = new LoginAttempt { Count = 0, IsLocked = false, LockUntil = DateTime.MinValue };
await App.Cache.SetAsync(attempsCacheKey, loginAttempt,
TimeSpan.FromSeconds(loginFailedLimitOptions.Lockout), null);
}
if (loginAttempt.IsLocked && DateTime.Now < loginAttempt.LockUntil)
{
// 可以实施账户锁定时,通过邮件或短信通知用户。
// 可以实施账户锁定后要求管理员手动解锁
return Error(App.L.R("Error.AccountLockedWithUnlockTime{0}",
loginAttempt.LockUntil.ToString("yyyy-MM-dd HH:mm:ss")));
}
}
var captchaOptions = App.GetOptions<CaptchaOptions>();
var showCaptcha = true; //是否显示验证码
var thresholdCacheKey = GlobalConstants.CachePrefix.Threshold + App.HttpContext.Connection.RemoteIpAddress;
var failedThreshold = 0;
if (captchaOptions.Threshold > 0)
{
failedThreshold = await App.Cache.GetAsync<int>(thresholdCacheKey);
if (failedThreshold <= 0)
{
failedThreshold = 1;
await App.Cache.SetAsync(thresholdCacheKey, failedThreshold,
TimeSpan.FromSeconds(captchaOptions.TimeOut), null);
}
showCaptcha = failedThreshold > captchaOptions.Threshold;
}
if (!App.GetOptions<SystemOptions>().IsQuickDebug && showCaptcha)
{
if (authUser.Captcha.IsNullOrEmpty())
{
return Error(ValidationError.Required(authUser, nameof(authUser.Captcha)));
}
if (authUser.CaptchaId.IsNullOrEmpty())
{
return Error(ValidationError.Required(authUser, nameof(authUser.CaptchaId)));
}
var code = await App.Cache.GetAsync<string>(authUser.CaptchaId);
if (code.IsNullOrEmpty())
{
return Error(App.L.R("Error.VerificationCodeExpired"));
}
if (!code.Equals(authUser.Captcha))
{
if (captchaOptions.Threshold > 0)
{
failedThreshold++;
await App.Cache.SetAsync(thresholdCacheKey, failedThreshold,
TimeSpan.FromSeconds(captchaOptions.TimeOut),
null);
}
return Error(App.L.R("Error.InvalidVerificationCode"));
}
}
var userDto = await _userService.QueryByNameAsync(authUser.UserName);
if (userDto == null)
{
if (captchaOptions.Threshold > 0)
{
failedThreshold++;
await App.Cache.SetAsync(thresholdCacheKey, failedThreshold,
TimeSpan.FromSeconds(captchaOptions.TimeOut),
null);
}
return Error(App.L.R("Error.UserNotFound"));
}
var rsaOptions = App.GetOptions<RsaOptions>();
var password = new RsaHelper(rsaOptions.PrivateKey, rsaOptions.PublicKey).Decrypt(authUser.Password);
if (!BCryptHelper.Verify(password, userDto.Password))
{
if (captchaOptions.Threshold > 0)
{
failedThreshold++;
await App.Cache.SetAsync(thresholdCacheKey, failedThreshold,
TimeSpan.FromSeconds(captchaOptions.TimeOut),
null);
}
if (loginFailedLimitOptions.Enabled && loginAttempt != null)
{
loginAttempt.Count++;
if (loginAttempt.Count >= loginFailedLimitOptions.MaxAttempts)
{
loginAttempt.IsLocked = true;
loginAttempt.LockUntil = DateTime.Now.AddSeconds(loginFailedLimitOptions.Lockout);
}
await App.Cache.SetAsync(attempsCacheKey, loginAttempt,
TimeSpan.FromSeconds(loginFailedLimitOptions.Lockout), null);
}
return loginFailedLimitOptions.Enabled
? Error(App.L.R("Error.InvalidPasswordWithLockWarning"))
: Error(App.L.R("Error.InvalidPassword"));
}
if (!userDto.Enabled)
{
if (captchaOptions.Threshold > 0)
{
failedThreshold++;
await App.Cache.SetAsync(thresholdCacheKey, failedThreshold,
TimeSpan.FromSeconds(captchaOptions.TimeOut),
null);
}
return Error(App.L.R("Error.UserNotActivated"));
}
await App.Cache.RemoveAsync(authUser.CaptchaId);
await App.Cache.RemoveAsync(thresholdCacheKey);
await App.Cache.RemoveAsync(attempsCacheKey);
var netUser = await _userService.QueryByIdAsync(userDto.Id);
return await LoginResult(netUser, "login");
}🔄 Token 刷新
自动刷新
当 Token 过期时(在 RefreshTokenExpires 时间内),系统实现 Token 以旧换新返回客户端:
csharp
/// <summary>
/// 刷新Token
/// </summary>
/// <param name="refreshToken"></param>
/// <returns></returns>
[HttpPost]
[Route("refreshToken")]
[Description("Action.RefreshToken")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TokenVo))]
public async Task<ActionResult> RefreshToken([FromBody] RefreshToken refreshToken)
{
if (!ModelState.IsValid)
{
var actionError = ModelState.GetErrors();
return Error(actionError);
}
var token = refreshToken.Token;
var tokenMd5 = token.ToMd5String16();
var tokenBlacklist = await _tokenBlacklistService
.TableWhere(x => x.AccessToken == tokenMd5, null, null, null, true)
.FirstAsync();
if (!tokenBlacklist.IsNull())
{
return Error(App.L.R("Error.TokenRevoked"));
}
var jwtSecurityToken = await _tokenService.ReadJwtToken(token);
if (jwtSecurityToken != null)
{
var userId = Convert.ToInt64(jwtSecurityToken.Claims
.FirstOrDefault(s => s.Type == AuthConstants.JwtClaimTypes.Jti)?.Value);
var loginTime = Convert.ToInt64(jwtSecurityToken.Claims
.FirstOrDefault(s => s.Type == AuthConstants.JwtClaimTypes.Iat)?.Value).TimeMillisecondToDateTime();
var nowTime = DateTime.Now.ToLocalTime();
var refreshTimeClaims = Convert.ToInt64(jwtSecurityToken.Claims
.FirstOrDefault(s => s.Type == AuthConstants.JwtClaimTypes.RefreshTime)?.Value);
var refreshTime = refreshTimeClaims.TimeMillisecondToDateTime();
// 允许token刷新时间内
if (nowTime < refreshTime)
{
var netUser = await _userService.QueryByIdAsync(userId);
if (netUser.IsNotNull())
if (netUser.UpdateTime == null || netUser.UpdateTime < loginTime)
return await LoginResult(netUser, "refresh", refreshTimeClaims);
}
return Error(App.L.R("Error.TokenExpired"));
}
return Error(App.L.R("Error.TokenParseFailed"));
}🚫 Token 黑名单
黑名单机制
为了支持用户登出和 Token 撤销,系统实现了 JWT 黑名单机制:
csharp
/// <summary>
/// Token黑名单
/// </summary>
[SugarTable("sys_token_blacklist")]
public class TokenBlacklist : BaseEntity
{
/// <summary>
/// 令牌 登录token的MD5值
/// </summary>
public string AccessToken { get; set; } = string.Empty;
}
var token = refreshToken.Token;
var tokenMd5 = token.ToMd5String16();
var tokenBlacklist = await _tokenBlacklistService
.TableWhere(x => x.AccessToken == tokenMd5, null, null, null, true)
.FirstAsync();
if (!tokenBlacklist.IsNull())
{
return Error(App.L.R("Error.TokenRevoked"));
}🔒 安全最佳实践
1. 密钥管理
- 使用强随机密钥作为签名密钥
- 定期轮换签名密钥
- 将密钥存储在安全的配置文件中
2. Token 生命周期
- 设置合理的过期时间(建议不超过 24 小时)
- 实现 Token 刷新机制
- 支持主动撤销 Token
3. 传输安全
- 始终使用 HTTPS 传输 Token
- 在请求头中传递 Token,避免在 URL 中暴露
- 客户端安全存储 Token

