Files
AI-Health/backend/src/Health.Infrastructure/Auth/AuthService.cs
MingNian 9cea41705e feat: 文件存储安全加固 + 认证增强 + 媒体URL保护 + provider 重构
## 后端安全加固
- 新增 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: 更新
2026-07-20 10:19:01 +08:00

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);
}