## 后端安全加固 - 新增 UserUploadPathResolver: 用户上传文件路径安全解析, 防目录穿越 - LocalReportFileStorage: 文件存储路径安全加固 - local_account_file_cleanup: 账号删除时文件清理逻辑增强 - AuthService: 认证逻辑增强 - file_endpoints / report_endpoints: 文件访问接口安全加固 - ai_chat_endpoints / doctor_endpoints: 接口安全调整 - Program.cs: 服务注册调整 ## 前端认证与媒体 - 新增 authenticated_network_image.dart: 带认证的图片加载组件 - auth_provider: 认证状态管理大幅增强(+173) - api_client: 网络客户端增强(+124) - chat_provider: 聊天 provider 重构(+76) - omron_device_provider: 蓝牙设备 provider 增强(+53) - sse_handler: SSE 处理增强(+35) - consultation_provider / data_providers / conversation_history_provider: 调整 ## 页面调整 - remaining_pages: 健康档案/饮食记录等页面增强(+115) - home_page / chat_messages_view: 主页微调 - doctor 端多页微调(consultations/dashboard/followups/patient_detail/profile/report_detail/reports) - report_pages / settings_pages / notification_prefs_page: 微调 - device_scan_page / diet_capture_page / admin_home_page: 微调 ## 测试 - 新增 file_path_security_tests: 文件路径安全测试 - 新增 protected_media_url_test: 媒体URL保护测试 - 新增 user_session_identity_test: 用户会话身份测试 - account_deletion_tests / application_service_tests / auth_tests: 更新
202 lines
8.7 KiB
C#
202 lines
8.7 KiB
C#
using Health.Application.Auth;
|
|
using Health.Infrastructure.Services;
|
|
|
|
namespace Health.Infrastructure.Auth;
|
|
|
|
public sealed class AuthService(
|
|
AppDbContext db,
|
|
JwtProvider jwt,
|
|
SmsService sms,
|
|
AppleTokenValidator appleValidator // Apple Sign-In JWT 验证
|
|
) : IAuthService
|
|
{
|
|
private const string AdminPhone = "12345678910";
|
|
private const string AdminSmsCode = "000000";
|
|
private static readonly Guid AdminId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
|
private readonly AppDbContext _db = db;
|
|
private readonly JwtProvider _jwt = jwt;
|
|
private readonly SmsService _sms = sms;
|
|
private readonly AppleTokenValidator _appleValidator = appleValidator;
|
|
|
|
public async Task<AuthResult> SendCodeAsync(string phone, bool revealDevelopmentCode, CancellationToken ct)
|
|
{
|
|
var code = phone == AdminPhone ? AdminSmsCode : _sms.GenerateCode();
|
|
await _db.VerificationCodes.AddAsync(new VerificationCode
|
|
{
|
|
Id = Guid.NewGuid(), Phone = phone, Code = code,
|
|
ExpiresAt = DateTime.UtcNow.AddMinutes(5),
|
|
}, ct);
|
|
await _db.SaveChangesAsync(ct);
|
|
if (phone != AdminPhone) await _sms.SendCodeAsync(phone, code);
|
|
return new AuthResult(0, new { success = true, devCode = revealDevelopmentCode ? code : null });
|
|
}
|
|
|
|
public async Task<AuthResult> RegisterAsync(RegisterCommand command, CancellationToken ct)
|
|
{
|
|
var code = await TakeCodeAsync(command.Phone, command.SmsCode, ct);
|
|
if (code == null) return Error(40001, "验证码错误或已过期");
|
|
if (await _db.Users.AnyAsync(x => x.Phone == command.Phone, ct)) return Error(40002, "该手机号已注册,请直接登录");
|
|
if (string.IsNullOrWhiteSpace(command.Name)) return Error(40003, "请输入姓名");
|
|
if (!await _db.Doctors.AnyAsync(x => x.Id == command.DoctorId && x.IsActive, ct)) return Error(40004, "所选医生不存在或已停诊");
|
|
|
|
code.IsUsed = true;
|
|
var user = new User
|
|
{
|
|
Id = Guid.NewGuid(), Phone = command.Phone, Role = "User", Name = command.Name,
|
|
DoctorId = command.DoctorId, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow,
|
|
};
|
|
_db.Users.Add(user);
|
|
_db.NotificationPreferences.Add(new NotificationPreference { Id = Guid.NewGuid(), UserId = user.Id });
|
|
_db.HealthArchives.Add(new HealthArchive { Id = Guid.NewGuid(), UserId = user.Id });
|
|
var tokens = AddTokens(user.Id, user.Phone, user.Role);
|
|
await _db.SaveChangesAsync(ct);
|
|
return new AuthResult(0, new { tokens.accessToken, tokens.refreshToken, user = new { user.Id, user.Phone, user.Role, isNew = true } });
|
|
}
|
|
|
|
public async Task<AuthResult> LoginAsync(string phone, string smsCode, CancellationToken ct)
|
|
{
|
|
var code = await TakeCodeAsync(phone, smsCode, ct);
|
|
if (code == null) return Error(40001, "验证码错误或已过期");
|
|
code.IsUsed = true;
|
|
|
|
if (phone == AdminPhone)
|
|
{
|
|
var tokens = AddTokens(AdminId, AdminPhone, "Admin");
|
|
await _db.SaveChangesAsync(ct);
|
|
return new AuthResult(0, new { tokens.accessToken, tokens.refreshToken, user = new { id = AdminId, phone = AdminPhone, role = "Admin", name = "管理员", isNew = false } });
|
|
}
|
|
|
|
var user = await _db.Users.FirstOrDefaultAsync(x => x.Phone == phone, ct);
|
|
if (user == null) return Error(40004, "该手机号未注册,请先注册");
|
|
user.UpdatedAt = DateTime.UtcNow;
|
|
var userTokens = AddTokens(user.Id, user.Phone, user.Role);
|
|
await _db.SaveChangesAsync(ct);
|
|
return new AuthResult(0, new
|
|
{
|
|
userTokens.accessToken, userTokens.refreshToken,
|
|
user = new { user.Id, user.Phone, user.Role, user.Name, user.Gender, user.AvatarUrl, BirthDate = user.BirthDate?.ToString("yyyy-MM-dd"), isNew = false }
|
|
});
|
|
}
|
|
|
|
public async Task<AuthResult> RefreshAsync(string refreshToken, CancellationToken ct)
|
|
{
|
|
var oldToken = await _db.RefreshTokens.FirstOrDefaultAsync(x => x.Token == refreshToken && !x.IsRevoked, ct);
|
|
if (oldToken == null || oldToken.ExpiresAt < DateTime.UtcNow) return Error(40002, "登录已过期,请重新登录");
|
|
oldToken.IsRevoked = true;
|
|
|
|
if (oldToken.UserId == AdminId)
|
|
{
|
|
var tokens = AddTokens(AdminId, AdminPhone, "Admin");
|
|
await _db.SaveChangesAsync(ct);
|
|
return new AuthResult(0, new
|
|
{
|
|
tokens.accessToken,
|
|
tokens.refreshToken,
|
|
user = new
|
|
{
|
|
id = AdminId,
|
|
phone = AdminPhone,
|
|
role = "Admin",
|
|
name = "管理员"
|
|
}
|
|
});
|
|
}
|
|
|
|
var user = await _db.Users.FindAsync([oldToken.UserId], ct);
|
|
if (user == null) return Error(40002, "用户不存在");
|
|
var userTokens = AddTokens(user.Id, user.Phone, user.Role);
|
|
await _db.SaveChangesAsync(ct);
|
|
return new AuthResult(0, new
|
|
{
|
|
userTokens.accessToken,
|
|
userTokens.refreshToken,
|
|
user = new
|
|
{
|
|
user.Id,
|
|
user.Phone,
|
|
user.Role,
|
|
user.Name,
|
|
user.Gender,
|
|
user.AvatarUrl,
|
|
BirthDate = user.BirthDate?.ToString("yyyy-MM-dd")
|
|
}
|
|
});
|
|
}
|
|
|
|
public async Task LogoutAsync(string refreshToken, CancellationToken ct)
|
|
{
|
|
var token = await _db.RefreshTokens.FirstOrDefaultAsync(x => x.Token == refreshToken, ct);
|
|
if (token != null) token.IsRevoked = true;
|
|
await _db.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task<AuthResult> AppleLoginAsync(AppleLoginCommand command, CancellationToken ct)
|
|
{
|
|
// 1. 验证 Apple identityToken
|
|
string appleUserId;
|
|
try
|
|
{
|
|
appleUserId = await _appleValidator.ValidateAsync(command.IdentityToken, ct);
|
|
}
|
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return Error(40005, "Apple 登录验证失败,请重试");
|
|
}
|
|
|
|
// 2. 查找已有用户
|
|
var existingUser = await _db.Users.FirstOrDefaultAsync(x => x.AppleUserId == appleUserId, ct);
|
|
if (existingUser != null)
|
|
{
|
|
existingUser.UpdatedAt = DateTime.UtcNow;
|
|
if (!string.IsNullOrWhiteSpace(command.Name)) existingUser.Name ??= command.Name;
|
|
var tokens = AddTokens(existingUser.Id, existingUser.Phone, existingUser.Role);
|
|
await _db.SaveChangesAsync(ct);
|
|
return new AuthResult(0, new
|
|
{
|
|
tokens.accessToken, tokens.refreshToken,
|
|
user = new { existingUser.Id, existingUser.Phone, existingUser.Role, existingUser.Name, existingUser.Gender, existingUser.AvatarUrl, isNew = false }
|
|
});
|
|
}
|
|
|
|
// 3. 新用户 → 创建账户(无需手机号、无需选医生)
|
|
var newUser = new User
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Phone = null,
|
|
AppleUserId = appleUserId,
|
|
Role = "User",
|
|
Name = command.Name,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
};
|
|
_db.Users.Add(newUser);
|
|
_db.NotificationPreferences.Add(new NotificationPreference { Id = Guid.NewGuid(), UserId = newUser.Id });
|
|
_db.HealthArchives.Add(new HealthArchive { Id = Guid.NewGuid(), UserId = newUser.Id });
|
|
var newTokens = AddTokens(newUser.Id, null, newUser.Role);
|
|
await _db.SaveChangesAsync(ct);
|
|
return new AuthResult(0, new
|
|
{
|
|
newTokens.accessToken, newTokens.refreshToken,
|
|
user = new { newUser.Id, Phone = (string?)null, newUser.Role, newUser.Name, isNew = true }
|
|
});
|
|
}
|
|
|
|
private Task<VerificationCode?> TakeCodeAsync(string phone, string code, CancellationToken ct) =>
|
|
_db.VerificationCodes.Where(x => x.Phone == phone && x.Code == code && x.ExpiresAt > DateTime.UtcNow && !x.IsUsed)
|
|
.OrderByDescending(x => x.CreatedAt).FirstOrDefaultAsync(ct);
|
|
|
|
private (string accessToken, string refreshToken) AddTokens(Guid userId, string? phone, string role)
|
|
{
|
|
var access = _jwt.GenerateAccessToken(userId, phone ?? "", role);
|
|
var refresh = _jwt.GenerateRefreshToken();
|
|
_db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), UserId = userId, Token = refresh, ExpiresAt = DateTime.UtcNow.AddDays(30) });
|
|
return (access, refresh);
|
|
}
|
|
|
|
private static AuthResult Error(int code, string message) => new(code, null, message);
|
|
}
|