Files
Atomx/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs
2025-12-04 12:12:46 +08:00

205 lines
8.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Atomx.Admin.Client.Models;
using Atomx.Admin.Client.Validators;
using Atomx.Admin.Services;
using Atomx.Common.Constants;
using Atomx.Common.Models;
using Atomx.Data;
using Atomx.Data.CacheServices;
using Atomx.Data.Services;
using Atomx.Utils.Extension;
using MapsterMapper;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
namespace Atomx.Admin.Controllers
{
/// <summary>
/// 登录/刷新/登出控制器(重构)
/// 说明:
/// - 返回标准的 AuthResponseaccess + refresh + expiry便于 WASM 客户端保存到 localStorage
/// - 仍保留 Cookie 登录SignInAsync以兼容 Blazor Server 场景
/// - 使用注入的 ITokenService 负责 token 的生成、刷新与撤销(数据库保存 refresh token 哈希)
/// - 提供 /api/sign/in (POST), /api/sign/refresh (POST), /api/sign/out (POST)
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class SignController : ControllerBase
{
private readonly ILogger<SignController> _logger;
private readonly IIdentityService _identityService;
private readonly IIdCreatorService _idCreator;
private readonly IMapper _mapper;
private readonly DataContext _dbContext;
private readonly JwtSetting _jwtSetting;
private readonly ICacheService _cacheService;
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly ITokenService _tokenService;
public SignController(
ILogger<SignController> logger,
IIdentityService identityService,
IIdCreatorService idCreator,
IMapper mapper,
DataContext dbContext,
JwtSetting jwtSetting,
ICacheService cacheService,
AuthenticationStateProvider authenticationStateProvider,
ITokenService tokenService)
{
_logger = logger;
_identityService = identityService;
_idCreator = idCreator;
_mapper = mapper;
_dbContext = dbContext;
_jwtSetting = jwtSetting;
_cacheService = cacheService;
_authenticationStateProvider = authenticationStateProvider;
_tokenService = tokenService;
}
/// <summary>
/// 登录:支持邮箱或用户名登录
/// - 返回 AuthResponse 给 WASMaccess + refresh
/// - 在 Server 场景同时创建 Cookie兼容 Blazor Server
/// </summary>
[HttpPost("in")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var validator = new LoginModelValidator();
var validation = validator.Validate(model);
if (!validation.IsValid)
{
var message = validation.Errors.FirstOrDefault()?.ErrorMessage ?? string.Empty;
return new JsonResult(new ApiResult<AuthResponse>().IsFail(message, null));
}
// 查询用户(支持 email / username
Atomx.Common.Entities.Admin? user = null;
if (model.Account.Contains("@"))
{
user = _dbContext.Admins.SingleOrDefault(p => p.Email == model.Account);
}
else
{
user = _dbContext.Admins.SingleOrDefault(p => p.Username == model.Account);
}
if (user == null)
{
return new JsonResult(new ApiResult<AuthResponse>().IsFail("用户不存在", null));
}
// 简单密码校验(项目 uses MD5 存储示例)
if (user.Password != model.Password.ToMd5Password())
{
return new JsonResult(new ApiResult<AuthResponse>().IsFail("账号密码不正确", null));
}
// 生成 access + refreshTokenService 会把 refresh 的哈希保存到数据库)
var ip = _identityService.GetClientIp();
var userAgent = Request.Headers["User-Agent"].FirstOrDefault();
var authResponse = await _tokenService.GenerateTokenAsync(user, ip, userAgent);
// 更新用户登录统计信息
user.LastLogin = DateTime.UtcNow;
user.LastIp = ip;
user.LoginCount++;
_dbContext.Admins.Update(user);
await _dbContext.SaveChangesAsync();
// 为 Blazor Server 场景创建 CookieClaims 中包含必要角色/权限)
var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId);
var claims = new List<Claim>
{
new Claim(ClaimKeys.Id, user.Id.ToString()),
new Claim(ClaimKeys.Email, user.Email ?? string.Empty),
new Claim(ClaimKeys.Name, user.Username ?? string.Empty),
new Claim(ClaimKeys.Role, user.RoleId.ToString()),
new Claim(ClaimKeys.Permission, role?.Permission ?? string.Empty)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
// SignInAsync 创建 HttpOnly Cookie便于 Server-side 认证
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
new AuthenticationProperties
{
IsPersistent = model.RememberMe,
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes)
});
return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(authResponse));
}
/// <summary>
/// 刷新客户端传入可能已过期的access token 与 refresh token明文
/// - TokenService.RefreshTokenAsync 会验证并一次性撤销旧的 refresh token生成新的对
/// </summary>
[HttpPost("refresh")]
[AllowAnonymous]
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
{
if (request == null || string.IsNullOrWhiteSpace(request.Token) || string.IsNullOrWhiteSpace(request.RefreshToken))
{
return BadRequest(new ApiResult<AuthResponse>().IsFail("无效的刷新请求", null));
}
try
{
var ip = _identityService.GetClientIp();
var newTokens = await _tokenService.RefreshTokenAsync(request.Token, request.RefreshToken, ip);
return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(newTokens));
}
catch (SecurityTokenException ex)
{
_logger.LogWarning(ex, "刷新失败:刷新令牌无效或 access token 无效");
return Unauthorized(new ApiResult<AuthResponse>().IsFail("刷新令牌无效或已过期", null));
}
catch (Exception ex)
{
_logger.LogError(ex, "刷新令牌时发生内部错误");
return StatusCode(500, new ApiResult<AuthResponse>().IsFail("服务器内部错误", null));
}
}
/// <summary>
/// 登出:可传入 refresh token 以撤销WASM 前端应同时清除 localStorage
/// - 本接口会 SignOut CookieServer 场景)并尝试撤销 refresh token
/// </summary>
[HttpPost("out")]
[AllowAnonymous]
public async Task<IActionResult> LogoutAsync([FromBody] RevokeRequest? revokeRequest = null)
{
if (revokeRequest != null && !string.IsNullOrWhiteSpace(revokeRequest.RefreshToken))
{
try
{
var ip = _identityService.GetClientIp();
await _tokenService.RevokeTokenAsync(revokeRequest.RefreshToken, ip);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "撤销 refresh token 失败(允许)");
}
}
// 清理 Cookie
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
Response.Cookies.Delete("access_token");
Response.Cookies.Delete("refresh_token");
return new JsonResult(new ApiResult<string>().IsSuccess("已退出"));
}
}
}