Files
TechHelper/TechHelper.Server/Services/User/AuthenticationService.cs
SpecialX ac900159ba
Some checks failed
TechAct / explore-gitea-actions (push) Failing after 12s
重构项目结构,移除Assignment相关功能,优化Submission模块
2025-10-09 18:57:28 +08:00

165 lines
5.1 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 Entities.Configuration;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Entities.DTO;
namespace TechHelper.Services.Beta
{
/// <summary>
/// 用户认证服务,实现 JWT 令牌生成、刷新令牌生成、过期令牌解析等功能。
/// </summary>
public class AuthenticationService : IAuthenticationService
{
private readonly JwtConfiguration _jwtSettings;
private readonly JwtSecurityTokenHandler _jwtHandler;
private readonly UserManager<Entities.Contracts.User> _userManager;
private readonly IClassService _classService;
/// <summary>
/// 构造函数,注入 JWT 配置、用户管理器和班级服务。
/// </summary>
public AuthenticationService(IOptions<JwtConfiguration> jwtSettings, UserManager<Entities.Contracts.User> userManager, IClassService classService)
{
_jwtSettings = jwtSettings.Value;
_jwtHandler = new JwtSecurityTokenHandler();
_userManager = userManager;
_classService = classService;
}
/// <summary>
/// 生成指定用户的 JWT 访问令牌。
/// </summary>
/// <param name="user">用户实体</param>
/// <returns>JWT 字符串</returns>
public async Task<string> GetToken(Entities.Contracts.User user)
{
var signingCredentials = GetSigningCredentials();
var claims = await GetClaims(user);
var tokenOptions = GenerateTokenOptions(signingCredentials, claims);
return _jwtHandler.WriteToken(tokenOptions);
}
/// <summary>
/// 获取 JWT 签名凭证。
/// </summary>
/// <returns>签名凭证</returns>
private SigningCredentials GetSigningCredentials()
{
var key = Encoding.UTF8.GetBytes(_jwtSettings.SecurityKey);
var secret = new SymmetricSecurityKey(key);
return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256);
}
/// <summary>
/// 获取用户的声明信息,包括角色和班级信息。
/// </summary>
/// <param name="user">用户实体</param>
/// <returns>声明集合</returns>
private async Task<IEnumerable<Claim>> GetClaims(Entities.Contracts.User user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Email)
};
// 添加用户角色声明
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
#region ClassInfo
// 添加用户班级信息声明
var classInfo = await _classService.GetUserInjoinedClasses(user.Id);
if (classInfo.Status)
{
var classs = classInfo.Result as UserClassDetailInfoDto;
if (classs == null) return claims;
foreach (var c in classs.UserClassInfos)
{
claims.Add(new Claim("Grade", c.Grade.ToString()));
claims.Add(new Claim("Class", c.Class.ToString()));
}
}
#endregion
return claims;
}
/// <summary>
/// 生成 JWT 令牌对象。
/// </summary>
/// <param name="signingCredentials">签名凭证</param>
/// <param name="claims">声明集合</param>
/// <returns>JWT 令牌对象</returns>
private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, IEnumerable<Claim> claims)
{
var tokenOptions = new JwtSecurityToken(
issuer: _jwtSettings.ValidIssuer,
audience: _jwtSettings.ValidAudience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(
_jwtSettings.ExpiryInMinutes)),
signingCredentials: signingCredentials);
return tokenOptions;
}
/// <summary>
/// 生成安全的刷新令牌Base64 字符串)。
/// </summary>
/// <returns>刷新令牌</returns>
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
/// <summary>
/// 从过期的 JWT 令牌中获取声明主体(不验证过期时间)。
/// </summary>
/// <param name="token">过期的 JWT 令牌</param>
/// <returns>声明主体</returns>
/// <exception cref="SecurityTokenException">令牌无效时抛出</exception>
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_jwtSettings.SecurityKey)),
ValidateLifetime = false, // 不验证过期时间
ValidIssuer = _jwtSettings.ValidIssuer,
ValidAudience = _jwtSettings.ValidAudience,
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token,
tokenValidationParameters, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null ||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
StringComparison.InvariantCultureIgnoreCase))
{
throw new SecurityTokenException("Invalid token");
}
return principal;
}
}
}