滑动续期与自动刷新:
Web:访问令牌 10 分钟;活跃时自动轮换刷新令牌;连续无操作 30 分钟后清除登录并跳转登录页。 App:会话窗口 3 天;打开 App、恢复前台或请求接口时自动刷新并重新顺延 3 天。 普通登录和 SSO 使用同一策略。 刷新令牌只以 SHA-256 摘要入库,每次刷新都会轮换,旧令牌无法再次使用;退出登录会吊销刷新令牌。 网络临时故障不会误清登录状态,多标签页同时刷新也做了竞争处理。
This commit is contained in:
1 parent
6bee29a351
commit
5ec62a03ca
24 files changed
+6983
-72
No files matched your search
+3
-1
@@ -67,7 +67,9 @@ Operations__MySqlClientPath=mysql
|
|||||||
Jwt__Issuer=Jiaowu.Api
|
Jwt__Issuer=Jiaowu.Api
|
||||||
Jwt__Audience=Jiaowu.Web
|
Jwt__Audience=Jiaowu.Web
|
||||||
Jwt__Key=REPLACE_WITH_AT_LEAST_32_RANDOM_BYTES
|
Jwt__Key=REPLACE_WITH_AT_LEAST_32_RANDOM_BYTES
|
||||||
Jwt__ExpireMinutes=60
|
Jwt__AccessTokenMinutes=10
|
||||||
|
Jwt__WebIdleMinutes=30
|
||||||
|
Jwt__AppIdleMinutes=4320
|
||||||
|
|
||||||
# Keycloak SSO(可选)。Authority 必须指向 realm,例如:
|
# Keycloak SSO(可选)。Authority 必须指向 realm,例如:
|
||||||
# https://sso.example.edu.cn/realms/mingxu
|
# https://sso.example.edu.cn/realms/mingxu
|
||||||
|
|||||||
+3
-1
@@ -30,7 +30,9 @@ x-jiaowu-environment: &jiaowu-environment
|
|||||||
Jwt__Issuer: Jiaowu.Api
|
Jwt__Issuer: Jiaowu.Api
|
||||||
Jwt__Audience: Jiaowu.Web
|
Jwt__Audience: Jiaowu.Web
|
||||||
Jwt__Key: "${JWT_KEY:?请在 .env.docker 中设置 JWT_KEY}"
|
Jwt__Key: "${JWT_KEY:?请在 .env.docker 中设置 JWT_KEY}"
|
||||||
Jwt__ExpireMinutes: "60"
|
Jwt__AccessTokenMinutes: "10"
|
||||||
|
Jwt__WebIdleMinutes: "30"
|
||||||
|
Jwt__AppIdleMinutes: "4320"
|
||||||
AllowedHosts: "${ALLOWED_HOSTS:-localhost}"
|
AllowedHosts: "${ALLOWED_HOSTS:-localhost}"
|
||||||
Cors__Origins__0: "${CORS_ORIGIN:-http://localhost:8080}"
|
Cors__Origins__0: "${CORS_ORIGIN:-http://localhost:8080}"
|
||||||
OfficialDocuments__PublicBaseUrl: "${OFFICIAL_DOCUMENTS_PUBLIC_BASE_URL:-http://localhost:8080}"
|
OfficialDocuments__PublicBaseUrl: "${OFFICIAL_DOCUMENTS_PUBLIC_BASE_URL:-http://localhost:8080}"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Jiaowu.Api.Controllers;
|
|||||||
public sealed class AuthController(
|
public sealed class AuthController(
|
||||||
AppDbContext db,
|
AppDbContext db,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
ITokenService tokenService,
|
IAuthSessionService authSessionService,
|
||||||
IAppCache cache) : ControllerBase
|
IAppCache cache) : ControllerBase
|
||||||
{
|
{
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
@@ -135,8 +135,11 @@ public sealed class AuthController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
[EnableRateLimiting("public-auth")]
|
||||||
[HttpPost("login")]
|
[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);
|
var user = await userManager.FindByNameAsync(request.UserName);
|
||||||
if (user is null || !user.IsEnabled)
|
if (user is null || !user.IsEnabled)
|
||||||
@@ -166,15 +169,47 @@ public sealed class AuthController(
|
|||||||
await userManager.UpdateAsync(user);
|
await userManager.UpdateAsync(user);
|
||||||
var roles = await userManager.GetRolesAsync(user);
|
var roles = await userManager.GetRolesAsync(user);
|
||||||
|
|
||||||
return new LoginResponse(
|
var session = await authSessionService.CreateAsync(
|
||||||
tokenService.Create(user, roles),
|
user,
|
||||||
new CurrentUserResponse(
|
|
||||||
user.Id,
|
|
||||||
user.UserName!,
|
|
||||||
user.DisplayName,
|
|
||||||
roles,
|
roles,
|
||||||
user.CollegeId,
|
request.IsNativeApp
|
||||||
EffectiveDataScopeResolver.Resolve(roles).ToString()));
|
? 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]
|
[Authorize]
|
||||||
@@ -212,11 +247,29 @@ public sealed class AuthController(
|
|||||||
Detail = detail,
|
Detail = detail,
|
||||||
Status = status
|
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(
|
public sealed record LoginRequest(
|
||||||
[Required, MaxLength(100)] string UserName,
|
[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(
|
public sealed record StudentActivationRequest(
|
||||||
[Required, MaxLength(50)] string Name,
|
[Required, MaxLength(50)] string Name,
|
||||||
@@ -227,7 +280,12 @@ public sealed record StudentActivationRequest(
|
|||||||
Guid AdministrativeClassId,
|
Guid AdministrativeClassId,
|
||||||
[Required, MinLength(8), MaxLength(100)] string Password);
|
[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(
|
public sealed record CurrentUserResponse(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
|
|||||||
Loaded 3 of 24 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user