滑动续期与自动刷新:

Web:访问令牌 10 分钟;活跃时自动轮换刷新令牌;连续无操作 30 分钟后清除登录并跳转登录页。
App:会话窗口 3 天;打开 App、恢复前台或请求接口时自动刷新并重新顺延 3 天。
普通登录和 SSO 使用同一策略。
刷新令牌只以 SHA-256 摘要入库,每次刷新都会轮换,旧令牌无法再次使用;退出登录会吊销刷新令牌。
网络临时故障不会误清登录状态,多标签页同时刷新也做了竞争处理。
This commit is contained in:
2026-08-03 19:20:39 +08:00 Unverified
parent 6bee29a351
commit 5ec62a03ca
24 changed files with 6986 additions and 75 deletions
+71 -13
View File
@@ -18,7 +18,7 @@ namespace Jiaowu.Api.Controllers;
public sealed class AuthController(
AppDbContext db,
UserManager<ApplicationUser> userManager,
ITokenService tokenService,
IAuthSessionService authSessionService,
IAppCache cache) : ControllerBase
{
[AllowAnonymous]
@@ -135,8 +135,11 @@ public sealed class AuthController(
}
[AllowAnonymous]
[EnableRateLimiting("public-auth")]
[HttpPost("login")]
public async Task<ActionResult<LoginResponse>> Login(LoginRequest request)
public async Task<ActionResult<LoginResponse>> Login(
LoginRequest request,
CancellationToken cancellationToken)
{
var user = await userManager.FindByNameAsync(request.UserName);
if (user is null || !user.IsEnabled)
@@ -166,15 +169,47 @@ public sealed class AuthController(
await userManager.UpdateAsync(user);
var roles = await userManager.GetRolesAsync(user);
return new LoginResponse(
tokenService.Create(user, roles),
new CurrentUserResponse(
user.Id,
user.UserName!,
user.DisplayName,
roles,
user.CollegeId,
EffectiveDataScopeResolver.Resolve(roles).ToString()));
var session = await authSessionService.CreateAsync(
user,
roles,
request.IsNativeApp
? AuthenticationClientType.App
: AuthenticationClientType.Web,
cancellationToken);
return CreateLoginResponse(session);
}
[AllowAnonymous]
[EnableRateLimiting("token-refresh")]
[HttpPost("refresh")]
public async Task<ActionResult<LoginResponse>> Refresh(
RefreshTokenRequest request,
CancellationToken cancellationToken)
{
var session = await authSessionService.RefreshAsync(
request.RefreshToken,
cancellationToken);
if (session is null)
{
return Unauthorized(new ProblemDetails
{
Title = "登录已过期",
Detail = "登录已过期或刷新令牌已失效,请重新登录。",
Status = StatusCodes.Status401Unauthorized
});
}
return CreateLoginResponse(session);
}
[AllowAnonymous]
[HttpPost("logout")]
public async Task<IActionResult> Logout(
RefreshTokenRequest request,
CancellationToken cancellationToken)
{
await authSessionService.RevokeAsync(request.RefreshToken, cancellationToken);
return NoContent();
}
[Authorize]
@@ -212,11 +247,29 @@ public sealed class AuthController(
Detail = detail,
Status = status
});
internal static LoginResponse CreateLoginResponse(AuthSessionResult session) =>
new(
session.AccessToken,
session.AccessTokenExpiresAt,
session.RefreshToken,
session.SessionExpiresAt,
new CurrentUserResponse(
session.User.Id,
session.User.UserName!,
session.User.DisplayName,
session.Roles,
session.User.CollegeId,
EffectiveDataScopeResolver.Resolve(session.Roles).ToString()));
}
public sealed record LoginRequest(
[Required, MaxLength(100)] string UserName,
[Required, MaxLength(100)] string Password);
[Required, MaxLength(100)] string Password,
bool IsNativeApp = false);
public sealed record RefreshTokenRequest(
[Required, MinLength(40), MaxLength(200)] string RefreshToken);
public sealed record StudentActivationRequest(
[Required, MaxLength(50)] string Name,
@@ -227,7 +280,12 @@ public sealed record StudentActivationRequest(
Guid AdministrativeClassId,
[Required, MinLength(8), MaxLength(100)] string Password);
public sealed record LoginResponse(string Token, CurrentUserResponse User);
public sealed record LoginResponse(
string Token,
DateTime AccessTokenExpiresAt,
string RefreshToken,
DateTime SessionExpiresAt,
CurrentUserResponse User);
public sealed record CurrentUserResponse(
Guid Id,