chore
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
|
||||
using Atomx.Admin.Client.Models;
|
||||
using Atomx.Admin.Client.Models;
|
||||
using Atomx.Admin.Client.Validators;
|
||||
using Atomx.Admin.Services;
|
||||
using Atomx.Admin.Utils;
|
||||
@@ -16,27 +15,42 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Atomx.Admin.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录/刷新/登出控制器(重构)
|
||||
/// 说明:
|
||||
/// - 返回标准的 AuthResponse(access + 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
|
||||
{
|
||||
readonly ILogger<SignController> _logger;
|
||||
readonly IIdentityService _identityService;
|
||||
readonly IIdCreatorService _idCreator;
|
||||
readonly IMapper _mapper;
|
||||
readonly DataContext _dbContext;
|
||||
readonly JwtSetting _jwtSetting;
|
||||
readonly ICacheService _cacheService;
|
||||
readonly AuthenticationStateProvider _authenticationStateProvider;
|
||||
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)
|
||||
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;
|
||||
@@ -46,106 +60,201 @@ namespace Atomx.Admin.Controllers
|
||||
_jwtSetting = jwtSetting;
|
||||
_cacheService = cacheService;
|
||||
_authenticationStateProvider = authenticationStateProvider;
|
||||
_tokenService = tokenService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户登录系统
|
||||
/// 登录:支持邮箱或用户名登录
|
||||
/// - 返回 AuthResponse 给 WASM(access + refresh)
|
||||
/// - 在 Server 场景同时创建 Cookie(兼容 Blazor Server)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("in")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(LoginModel model)
|
||||
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;
|
||||
var result = new ApiResult<string>().IsFail(message ?? string.Empty, null);
|
||||
return new JsonResult(result);
|
||||
var message = validation.Errors.FirstOrDefault()?.ErrorMessage ?? string.Empty;
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail(message, null));
|
||||
}
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var issuer = _jwtSetting.Issuer;
|
||||
var audience = _jwtSetting.Audience;
|
||||
var securityKey = _jwtSetting.SecurityKey;
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
Common.Entities.Admin? user = null;
|
||||
// 查询用户(支持 email / username)
|
||||
Atomx.Common.Entities.Admin? user = null;
|
||||
if (model.Account.Contains("@"))
|
||||
{
|
||||
user = _dbContext.Admins.Where(p => p.Email == model.Account).SingleOrDefault();
|
||||
user = _dbContext.Admins.SingleOrDefault(p => p.Email == model.Account);
|
||||
}
|
||||
else
|
||||
{
|
||||
user = _dbContext.Admins.Where(p => p.Username == model.Account).SingleOrDefault();
|
||||
user = _dbContext.Admins.SingleOrDefault(p => p.Username == model.Account);
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
var result = new ApiResult<string>().IsFail("用户不存在", null);
|
||||
return new JsonResult(result);
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail("用户不存在", null));
|
||||
}
|
||||
|
||||
// 简单密码校验(项目 uses MD5 存储示例)
|
||||
if (user.Password != model.Password.ToMd5Password())
|
||||
{
|
||||
var result = new ApiResult<string>().IsFail("账号密码不正确", null);
|
||||
return new JsonResult(result);
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail("账号密码不正确", null));
|
||||
}
|
||||
|
||||
var role = _dbContext.Roles.Where(p => p.Id == user.RoleId).SingleOrDefault();
|
||||
// 生成 access + refresh(TokenService 会把 refresh 的哈希保存到数据库)
|
||||
var ip = _identityService.GetClientIp();
|
||||
var userAgent = Request.Headers["User-Agent"].FirstOrDefault();
|
||||
var authResponse = await _tokenService.GenerateTokenAsync(user, ip, userAgent);
|
||||
|
||||
var claims = new List<Claim>()
|
||||
{
|
||||
new Claim(ClaimKeys.Id, user.Id.ToString()),
|
||||
new Claim(ClaimKeys.Email, user.Email),
|
||||
new Claim(ClaimKeys.Name, user.Username),
|
||||
new Claim(ClaimKeys.Role, user.RoleId.ToString()),
|
||||
new Claim(ClaimKeys.Permission, role?.Permission??string.Empty)
|
||||
};
|
||||
// 更新用户登录统计信息
|
||||
user.LastLogin = DateTime.UtcNow;
|
||||
user.LastIp = ip;
|
||||
user.LoginCount++;
|
||||
_dbContext.Admins.Update(user);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
// 为 Blazor Server 场景创建 Cookie(Claims 中包含必要角色/权限)
|
||||
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)
|
||||
});
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
// 另外将 tokens 写入 HttpOnly Cookie(增强与传统中间件的兼容性)
|
||||
try
|
||||
{
|
||||
Subject = claimsIdentity,
|
||||
var cookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
//Secure = !Request.IsLocal(), // 本地调试时允许 http
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes),
|
||||
Path = "/"
|
||||
};
|
||||
Response.Cookies.Append("access_token", authResponse.Token, cookieOptions);
|
||||
|
||||
Expires = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes),
|
||||
SigningCredentials = credentials,
|
||||
Issuer = issuer,
|
||||
Audience = audience
|
||||
};
|
||||
|
||||
var tokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
|
||||
|
||||
var loginResult = new ApiResult<string>().IsSuccess(tokenString);
|
||||
|
||||
user.LastLogin = DateTime.UtcNow;
|
||||
user.LastIp = _identityService.GetClientIp();
|
||||
user.LoginCount++;
|
||||
_dbContext.Admins.Update(user);
|
||||
_dbContext.SaveChanges();
|
||||
|
||||
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
|
||||
|
||||
return new JsonResult(loginResult);
|
||||
var refreshCookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
//Secure = !Request.IsLocal(),
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes),
|
||||
Path = "/"
|
||||
};
|
||||
Response.Cookies.Append("refresh_token", authResponse.RefreshToken, refreshCookieOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "设置 token cookie 失败(非致命)");
|
||||
}
|
||||
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(authResponse));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户退出系统
|
||||
/// 刷新:客户端传入(可能已过期的)access token 与 refresh token(明文)
|
||||
/// - TokenService.RefreshTokenAsync 会验证并一次性撤销旧的 refresh token,生成新的对
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("out")]
|
||||
[HttpPost("refresh")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LogoutAsync()
|
||||
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
|
||||
{
|
||||
await HttpContext.SignOutAsync();
|
||||
return new JsonResult(new ApiResult<string>());
|
||||
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);
|
||||
|
||||
// 更新 cookie(如存在)
|
||||
try
|
||||
{
|
||||
var cookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
//Secure = !Request.IsLocal(),
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes),
|
||||
Path = "/"
|
||||
};
|
||||
Response.Cookies.Append("access_token", newTokens.Token, cookieOptions);
|
||||
|
||||
var refreshCookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
//Secure = !Request.IsLocal(),
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes),
|
||||
Path = "/"
|
||||
};
|
||||
Response.Cookies.Append("refresh_token", newTokens.RefreshToken, refreshCookieOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "刷新 token 时写 cookie 失败(允许)");
|
||||
}
|
||||
|
||||
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 Cookie(Server 场景)并尝试撤销 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("已退出"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user