添加 JWT Token 吊销机制

This commit is contained in:
milimoe 2025-03-12 21:54:29 +08:00
parent b7b0ac1f37
commit fc5fb61ae8
Signed by: milimoe
GPG Key ID: 05D280912DA6C69E
8 changed files with 122 additions and 17 deletions

View File

@ -569,6 +569,7 @@ namespace Milimoe.FunGame.Server.Controller
// 验证登录 // 验证登录
if (username != null && password != null) if (username != null && password != null)
{ {
password = password.Encrypt(username);
ServerHelper.WriteLine("[" + DataRequestSet.GetTypeString(DataRequestType.Login_Login) + "] Username: " + username); ServerHelper.WriteLine("[" + DataRequestSet.GetTypeString(DataRequestType.Login_Login) + "] Username: " + username);
if (SQLHelper != null) if (SQLHelper != null)
{ {
@ -769,6 +770,7 @@ namespace Milimoe.FunGame.Server.Controller
string password = DataRequest.GetDictionaryJsonObject<string>(requestData, UserQuery.Column_Password) ?? ""; string password = DataRequest.GetDictionaryJsonObject<string>(requestData, UserQuery.Column_Password) ?? "";
if (username.Trim() != "" && password.Trim() != "") if (username.Trim() != "" && password.Trim() != "")
{ {
password = password.Encrypt(username);
SQLHelper?.Execute(UserQuery.Update_Password(SQLHelper, username, password)); SQLHelper?.Execute(UserQuery.Update_Password(SQLHelper, username, password));
if (SQLHelper?.Success ?? false) if (SQLHelper?.Success ?? false)
{ {

View File

@ -0,0 +1,26 @@
using Milimoe.FunGame.WebAPI.Services;
namespace Milimoe.FunGame.WebAPI.Architecture
{
public class JwtAuthenticationMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
JWTService jwtService = context.RequestServices.GetRequiredService<JWTService>();
// 获取 JWT Token
string token = context.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
// 检查 JWT 是否被吊销
if (jwtService.IsTokenRevoked(token))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("{\"message\":\"此 Token 已吊销,请重新登录以获取 Token。\"}");
return;
}
await next(context);
}
}
}

View File

@ -54,6 +54,11 @@ namespace Milimoe.FunGame.WebAPI.Controllers
} }
return BadRequest(new SocketObject(SocketMessageType.System, model.Token, "请求未执行完毕,请等待!")); return BadRequest(new SocketObject(SocketMessageType.System, model.Token, "请求未执行完毕,请等待!"));
} }
catch (TimeoutException)
{
_logger.LogWarning("请求超时。");
return StatusCode(408);
}
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("Error: {e}", e); _logger.LogError("Error: {e}", e);

View File

@ -21,6 +21,15 @@ namespace Milimoe.FunGame.WebAPI.Controllers
{ {
RequestType = payload.RequestType RequestType = payload.RequestType
}; };
if (payload.RequestType == DataRequestType.RunTime_Logout || payload.RequestType == DataRequestType.Reg_Reg ||
payload.RequestType == DataRequestType.Login_Login || payload.RequestType == DataRequestType.Login_GetFindPasswordVerifyCode)
{
response.StatusCode = 400;
response.Message = $"请求类型 {DataRequestSet.GetTypeString(payload.RequestType)} 不允许通过此接口处理!";
return StatusCode(400, response);
}
if (model.RequestID == Guid.Empty) if (model.RequestID == Guid.Empty)
{ {
Guid uid = Guid.NewGuid(); Guid uid = Guid.NewGuid();
@ -58,6 +67,11 @@ namespace Milimoe.FunGame.WebAPI.Controllers
return BadRequest(response); return BadRequest(response);
} }
} }
catch (TimeoutException)
{
_logger.LogWarning("请求超时。");
return StatusCode(408);
}
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("Error: {e}", e); _logger.LogError("Error: {e}", e);

View File

@ -67,6 +67,11 @@ namespace Milimoe.FunGame.WebAPI.Controllers
return BadRequest(response); return BadRequest(response);
} }
} }
catch (TimeoutException)
{
_logger.LogWarning("请求超时。");
return StatusCode(408);
}
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("Error: {e}", e); _logger.LogError("Error: {e}", e);

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization; using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.Constant;
@ -13,10 +14,8 @@ namespace Milimoe.FunGame.WebAPI.Controllers
{ {
[ApiController] [ApiController]
[Route("[controller]")] [Route("[controller]")]
public class UserController(JWTService jwtTokenService, ILogger<AdapterController> logger) : ControllerBase public class UserController(RESTfulAPIListener apiListener, JWTService jwtTokenService, ILogger<AdapterController> logger) : ControllerBase
{ {
private readonly ILogger<AdapterController> _logger = logger;
[HttpPost("reg")] [HttpPost("reg")]
public IActionResult Reg([FromBody] RegDTO dto) public IActionResult Reg([FromBody] RegDTO dto)
{ {
@ -46,7 +45,7 @@ namespace Milimoe.FunGame.WebAPI.Controllers
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("Error: {e}", e); logger.LogError("Error: {e}", e);
} }
return BadRequest("服务器暂时无法处理注册请求。"); return BadRequest("服务器暂时无法处理注册请求。");
} }
@ -56,12 +55,17 @@ namespace Milimoe.FunGame.WebAPI.Controllers
{ {
try try
{ {
PayloadModel<DataRequestType> response = new()
{
RequestType = DataRequestType.Login_Login
};
string msg = "用户名或密码不正确。"; string msg = "用户名或密码不正确。";
string clientIP = HttpContext.Connection.RemoteIpAddress?.ToString() + ":" + HttpContext.Connection.RemotePort; string clientIP = HttpContext.Connection.RemoteIpAddress?.ToString() + ":" + HttpContext.Connection.RemotePort;
ServerHelper.WriteLine(ServerHelper.MakeClientName(clientIP) + " 通过 RESTful API 连接至服务器,正在登录 . . .", InvokeMessageType.Core); ServerHelper.WriteLine(ServerHelper.MakeClientName(clientIP) + " 通过 RESTful API 连接至服务器,正在登录 . . .", InvokeMessageType.Core);
string username = dto.Username; string username = dto.Username;
string password = dto.Password; string password = dto.Password;
RESTfulAPIListener? apiListener = RESTfulAPIListener.Instance;
if (apiListener != null) if (apiListener != null)
{ {
// 移除旧模型 // 移除旧模型
@ -90,7 +94,14 @@ namespace Milimoe.FunGame.WebAPI.Controllers
model.GetUsersCount(); model.GetUsersCount();
string token = jwtTokenService.GenerateToken(username); string token = jwtTokenService.GenerateToken(username);
Config.ConnectingPlayerCount--; Config.ConnectingPlayerCount--;
return Ok(new { BearerToken = token, OpenToken = model.Token }); response.StatusCode = 200;
response.Message = "登录成功!";
response.Data = new()
{
{ "bearerToken", token },
{ "openToken", model.Token }
};
return Ok(response);
} }
} }
else msg = "服务器暂时无法处理登录请求。"; else msg = "服务器暂时无法处理登录请求。";
@ -100,26 +111,37 @@ namespace Milimoe.FunGame.WebAPI.Controllers
Config.ConnectingPlayerCount--; Config.ConnectingPlayerCount--;
ServerHelper.WriteLine(msg, InvokeMessageType.Core); ServerHelper.WriteLine(msg, InvokeMessageType.Core);
return Unauthorized(msg); response.Message = msg;
response.StatusCode = 401;
return Unauthorized(response);
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("Error: {e}", e); logger.LogError("Error: {e}", e);
} }
return BadRequest("服务器暂时无法处理登录请求。"); return BadRequest();
} }
[HttpPost("refresh")] [HttpPost("refresh")]
[Authorize] [Authorize]
public IActionResult Refresh([FromBody] LoginDTO dto) public IActionResult Refresh()
{ {
try try
{ {
return Ok(jwtTokenService.GenerateToken(dto.Username)); string oldToken = HttpContext.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
// 吊销
jwtTokenService.RevokeToken(oldToken);
// 生成
string username = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
string newToken = jwtTokenService.GenerateToken(username);
return Ok(newToken);
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError("Error: {e}", e); logger.LogError("Error: {e}", e);
} }
return BadRequest(); return BadRequest();
} }

View File

@ -1,5 +1,6 @@
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Reflection; using System.Reflection;
using System.Security.Claims;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -146,7 +147,8 @@ try
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"], ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? "undefined")) IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? "undefined")),
NameClaimType = ClaimTypes.NameIdentifier
}; };
}).AddScheme<AuthenticationSchemeOptions, CustomBearerAuthenticationHandler>("CustomBearer", options => { }); }).AddScheme<AuthenticationSchemeOptions, CustomBearerAuthenticationHandler>("CustomBearer", options => { });
builder.Logging.AddConsole(options => builder.Logging.AddConsole(options =>
@ -155,6 +157,8 @@ try
}); });
builder.Services.AddSingleton<ConsoleFormatter, CustomConsoleFormatter>(); builder.Services.AddSingleton<ConsoleFormatter, CustomConsoleFormatter>();
// 其他依赖注入 // 其他依赖注入
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IUserContext, HttpUserContext>(); builder.Services.AddScoped<IUserContext, HttpUserContext>();
builder.Services.AddSingleton(listener); builder.Services.AddSingleton(listener);
@ -174,8 +178,6 @@ try
throw new NoUserLogonException(); throw new NoUserLogonException();
}); });
builder.Services.AddHttpClient();
WebApplication app = builder.Build(); WebApplication app = builder.Build();
// 启用 CORS // 启用 CORS
@ -194,6 +196,8 @@ try
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseMiddleware<JwtAuthenticationMiddleware>();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();

View File

@ -1,11 +1,12 @@
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.Constant;
namespace Milimoe.FunGame.WebAPI.Services namespace Milimoe.FunGame.WebAPI.Services
{ {
public class JWTService(IConfiguration configuration) public class JWTService(IConfiguration configuration, IMemoryCache memoryCache)
{ {
public string GenerateToken(string username) public string GenerateToken(string username)
{ {
@ -29,5 +30,31 @@ namespace Milimoe.FunGame.WebAPI.Services
return new JwtSecurityTokenHandler().WriteToken(token); return new JwtSecurityTokenHandler().WriteToken(token);
} }
public void RevokeToken(string token)
{
// 从 Token 中提取过期时间
JwtSecurityToken jwtSecurityToken = new JwtSecurityTokenHandler().ReadJwtToken(token);
string? expiryClaim = jwtSecurityToken.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Exp)?.Value;
if (expiryClaim != null && long.TryParse(expiryClaim, out long expiryUnixTimestamp))
{
DateTime expiryDateTime = DateTimeOffset.FromUnixTimeSeconds(expiryUnixTimestamp).LocalDateTime;
TimeSpan remainingTime = expiryDateTime - DateTime.Now;
// 将 Token 存储到 MemoryCache 中,过期时间为 Token 的剩余有效期
memoryCache.Set(token, true, remainingTime);
}
else
{
memoryCache.Set(token, true, TimeSpan.FromMinutes(30));
}
}
public bool IsTokenRevoked(string token)
{
// 检查 Token 是否被吊销
return memoryCache.TryGetValue(token, out _);
}
} }
} }