20 KiB
Design Document: 用户认证系统迁移
Overview
本设计文档描述了将PHP用户认证系统迁移到.NET 8的技术方案。系统采用分层架构,包括Controller层、Service层和Repository层。迁移过程中保持与现有UniApp前端的100%兼容性,使用相同的请求/响应格式。
Architecture
┌─────────────────────────────────────────────────────────────┐
│ UniApp 前端 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ API Gateway / Nginx │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ .NET 8 API 服务 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Controllers │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │AuthController│ │UserController│ │HealthCheck │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Services │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ AuthService │ │ UserService │ │ JwtService │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │WechatService│ │ SmsService │ │IpLocationSvc│ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Infrastructure │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ EF Core │ │ Redis │ │ COS Upload │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ MySQL │ │ Redis │
└───────────┘ └───────────┘
Components and Interfaces
1. AuthController
负责处理所有认证相关的HTTP请求。
[ApiController]
[Route("api")]
public class AuthController : ControllerBase
{
// POST /login - 微信小程序登录
[HttpPost("login")]
public async Task<ApiResponse<string>> WechatMiniProgramLogin(WechatLoginRequest request);
// POST /mobileLogin - 手机号验证码登录
[HttpPost("mobileLogin")]
public async Task<ApiResponse<string>> MobileLogin(MobileLoginRequest request);
// POST /login_bind_mobile - 微信授权绑定手机号
[HttpPost("login_bind_mobile")]
public async Task<ApiResponse<BindMobileResponse>> LoginBindMobile(BindMobileRequest request);
// POST /bindMobile - 验证码绑定手机号
[HttpPost("bindMobile")]
public async Task<ApiResponse<BindMobileResponse>> BindMobile(BindMobileWithCodeRequest request);
// GET|POST /login_record - 记录用户登录
[HttpPost("login_record")]
[HttpGet("login_record")]
public async Task<ApiResponse<RecordLoginResponse>> RecordLogin(RecordLoginRequest request);
}
2. UserController
负责处理用户信息相关的HTTP请求。
[ApiController]
[Route("api")]
public class UserController : ControllerBase
{
// POST /user - 获取用户信息
[HttpPost("user")]
public async Task<ApiResponse<UserInfoResponse>> GetUserInfo();
// POST /update_userinfo - 更新用户信息
[HttpPost("update_userinfo")]
public async Task<ApiResponse> UpdateUserInfo(UpdateUserInfoRequest request);
// POST /user_log_off - 注销账号
[HttpPost("user_log_off")]
public async Task<ApiResponse> LogOff(LogOffRequest request);
}
3. IAuthService
认证服务接口。
public interface IAuthService
{
Task<LoginResult> WechatMiniProgramLoginAsync(string code, int? pid, string clickId);
Task<LoginResult> MobileLoginAsync(string mobile, string code, int? pid, string clickId);
Task<BindMobileResult> BindMobileAsync(int userId, string mobile, string code);
Task<BindMobileResult> WechatBindMobileAsync(int userId, string wechatCode);
Task RecordLoginAsync(int userId, string device, string deviceInfo);
Task LogOffAsync(int userId, int type);
}
4. IWechatService
微信服务接口。
public interface IWechatService
{
Task<WechatAuthResult> GetOpenIdAsync(string code);
Task<WechatMobileResult> GetMobileAsync(string code);
}
5. IJwtService
JWT服务接口。
public interface IJwtService
{
string GenerateToken(User user);
ClaimsPrincipal ValidateToken(string token);
int? GetUserIdFromToken(string token);
}
6. IUserService
用户服务接口。
public interface IUserService
{
Task<User> GetUserByIdAsync(int userId);
Task<User> GetUserByOpenIdAsync(string openId);
Task<User> GetUserByUnionIdAsync(string unionId);
Task<User> GetUserByMobileAsync(string mobile);
Task<User> CreateUserAsync(CreateUserDto dto);
Task UpdateUserAsync(int userId, UpdateUserDto dto);
Task<UserInfoDto> GetUserInfoAsync(int userId);
Task<int> CalculateVipLevelAsync(int userId, int currentVip);
}
7. IIpLocationService
IP地理位置服务接口。
public interface IIpLocationService
{
Task<IpLocationResult> GetLocationAsync(string ip);
}
Data Models
Request Models
public class WechatLoginRequest
{
public string Code { get; set; }
public int? Pid { get; set; }
}
public class MobileLoginRequest
{
public string Mobile { get; set; }
public string Code { get; set; }
public int? Pid { get; set; }
}
public class BindMobileRequest
{
public string Code { get; set; } // 微信授权code
}
public class BindMobileWithCodeRequest
{
public string Mobile { get; set; }
public string Code { get; set; } // 短信验证码
}
public class UpdateUserInfoRequest
{
public string Nickname { get; set; }
public string Headimg { get; set; }
public string Imagebase { get; set; } // Base64图片
}
public class RecordLoginRequest
{
public string Device { get; set; }
public string DeviceInfo { get; set; }
}
public class LogOffRequest
{
public int Type { get; set; } // 0-注销 1-取消注销
}
Response Models
public class ApiResponse<T>
{
public int Status { get; set; } // 1成功 0失败 -1未登录
public string Msg { get; set; }
public T Data { get; set; }
}
public class UserInfoResponse
{
public UserInfoDto Userinfo { get; set; }
public OtherConfigDto Other { get; set; }
public List<TaskDto> TaskList { get; set; }
}
public class UserInfoDto
{
public int Id { get; set; }
public string Uid { get; set; }
public string Nickname { get; set; }
public string Headimg { get; set; }
public string Mobile { get; set; } // 脱敏后的手机号
public int MobileIs { get; set; } // 是否绑定手机
public decimal Money { get; set; }
public decimal Money2 { get; set; }
public decimal Integral { get; set; }
public decimal Score { get; set; }
public int Vip { get; set; }
public string VipImgurl { get; set; }
public int Coupon { get; set; }
public int Day { get; set; } // 注册天数
}
public class BindMobileResponse
{
public string Token { get; set; } // 账户合并时返回新token
}
public class RecordLoginResponse
{
public string Uid { get; set; }
public string Nickname { get; set; }
public string Headimg { get; set; }
}
Internal DTOs
public class LoginResult
{
public bool Success { get; set; }
public string Token { get; set; }
public int UserId { get; set; }
public string ErrorMessage { get; set; }
}
public class WechatAuthResult
{
public bool Success { get; set; }
public string OpenId { get; set; }
public string UnionId { get; set; }
public string ErrorMessage { get; set; }
}
public class IpLocationResult
{
public bool Success { get; set; }
public string Province { get; set; }
public string City { get; set; }
public string Adcode { get; set; }
}
public class CreateUserDto
{
public string OpenId { get; set; }
public string UnionId { get; set; }
public string Mobile { get; set; }
public string Nickname { get; set; }
public string Headimg { get; set; }
public int Pid { get; set; }
public int? ClickId { get; set; }
}
Key Implementation Details
1. 防抖机制实现
使用Redis实现登录防抖,防止用户频繁请求:
public async Task<bool> TryAcquireLoginLockAsync(string key, int expireSeconds = 3)
{
var lockKey = $"login:debounce:{key}";
return await _redis.StringSetAsync(lockKey, "1", TimeSpan.FromSeconds(expireSeconds), When.NotExists);
}
2. 用户UID生成策略
根据配置支持三种UID生成方式:
- 类型0: 使用真实用户ID
- 类型1: 生成指定长度的纯数字ID
- 类型2: 生成字母数字混合ID
public string GenerateUid(int userId, UidConfig config)
{
return config.UidType switch
{
0 => userId.ToString(),
1 => GenerateNumericUid(config.UidLength),
2 => GenerateAlphaNumericUid(config.UidLength),
_ => userId.ToString()
};
}
3. 账户合并逻辑
当用户绑定的手机号已被其他账户使用时,需要合并账户:
public async Task<BindMobileResult> MergeAccountsAsync(int currentUserId, int mobileUserId)
{
// 1. 将当前用户的openid迁移到手机号用户
// 2. 生成新的token
// 3. 删除当前用户和账户记录
// 4. 返回新token
}
4. Token兼容策略
为保持与旧系统兼容,同时使用JWT和数据库token:
public async Task<string> CreateAccountTokenAsync(int userId)
{
var tokenNum = GenerateRandomString(10);
var time = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var accountToken = ComputeMd5($"{userId}{tokenNum}{time}");
// 存储到UserAccount表
await UpdateUserAccountAsync(userId, accountToken, tokenNum, time);
return accountToken;
}
5. 默认头像生成
使用Identicon库生成用户默认头像:
public async Task<(string nickname, string headimg)> CreateDefaultAvatarAsync(string seed, string prefix)
{
var nickname = $"{prefix}{Random.Shared.Next(1000, 9999)}";
var identicon = new Identicon(seed + nickname);
var imageData = identicon.GetImageData();
var url = await _cosUploader.UploadAsync(imageData, $"storage/users/icon/default/{Guid.NewGuid()}.png");
return (nickname, url);
}
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: 微信登录用户查找优先级
For any 用户登录请求,如果提供了unionid,系统应优先通过unionid查找用户;只有当unionid查找失败时,才通过openid查找。
Validates: Requirements 1.2
Property 2: 新用户创建完整性
For any 不存在的用户(无论是微信登录还是手机号登录),系统创建的新用户记录应包含:有效的uid、非空的昵称、有效的头像URL、正确的pid(如果提供)。
Validates: Requirements 1.3, 2.3
Property 3: 登录成功Token生成
For any 成功的登录请求(微信或手机号),系统应返回包含用户ID的有效JWT Token,且该Token应使用配置的密钥正确签名。
Validates: Requirements 1.5, 2.4, 3.1, 3.2
Property 4: 登录防抖机制
For any 用户在3秒内发起的重复登录请求,第二次及之后的请求应被拒绝并返回"请勿频繁登录"错误。
Validates: Requirements 1.6, 2.6
Property 5: 推荐关系记录
For any 新用户注册时提供的有效推荐人ID(pid),系统应将该pid正确保存到用户记录中。
Validates: Requirements 1.8, 2.7
Property 6: 验证码验证与清理
For any 手机号登录请求,如果验证码正确,系统应验证通过并删除Redis中的验证码;如果验证码错误或过期,系统应返回"验证码错误"。
Validates: Requirements 2.1, 2.2
Property 7: Token验证与授权
For any 携带Token的API请求,如果Token有效,系统应允许访问;如果Token无效或过期,系统应返回状态码-1(未登录)。
Validates: Requirements 3.3, 3.4
Property 8: 数据库Token兼容存储
For any 成功的登录请求,系统应同时在UserAccount表中存储account_token,确保与旧系统兼容。
Validates: Requirements 3.6
Property 9: 用户信息完整性
For any 用户信息查询请求,返回的数据应包含所有必要字段(id、uid、nickname、headimg、mobile、money、integral、vip等),且手机号应进行脱敏处理(格式:138****8000)。
Validates: Requirements 4.1, 4.4
Property 10: 昵称更新验证
For any 昵称更新请求,如果昵称为空,系统应拒绝更新;如果昵称有效,系统应保存更新。
Validates: Requirements 4.2
Property 11: VIP等级计算
For any 用户信息查询,系统应根据用户的累计消费金额正确计算并返回VIP等级。
Validates: Requirements 4.5
Property 12: 手机号绑定验证码验证
For any 手机号绑定请求,系统应验证短信验证码;验证码错误时应返回"验证码错误"。
Validates: Requirements 5.1, 5.5
Property 13: 账户合并正确性
For any 手机号已被其他用户绑定的情况,系统应正确合并账户:将当前用户的openid迁移到手机号用户,删除当前用户记录,并返回新的Token。
Validates: Requirements 5.2, 5.3
Property 14: 直接绑定手机号
For any 手机号未被其他用户绑定的情况,系统应直接更新当前用户的手机号。
Validates: Requirements 5.4
Property 15: 登录日志记录
For any 成功的登录请求,系统应在UserLoginLog表中记录登录日志,并更新UserAccount表中的最后登录时间和IP信息。
Validates: Requirements 6.1, 6.3
Property 16: recordLogin接口返回值
For any recordLogin接口调用,系统应返回用户的uid、昵称和头像。
Validates: Requirements 6.4
Property 17: 账号注销类型处理
For any 账号注销请求,type=0时表示注销账号,type=1时表示取消注销,系统应记录相应的日志并返回成功消息。
Validates: Requirements 7.1, 7.2, 7.3
Error Handling
1. 微信API错误
- 当微信API返回错误时,记录详细错误日志
- 返回用户友好的错误信息:"登录失败,请稍后重试"
2. 验证码错误
- 验证码不存在或已过期:返回"验证码错误"
- 验证码不匹配:返回"验证码错误"
3. 防抖限制
- 用户频繁请求:返回"请勿频繁登录,请稍后再试"
4. Token错误
- Token无效:返回status=-1,msg="未登录"
- Token过期:返回status=-1,msg="登录已过期"
5. 数据库错误
- 事务失败时回滚
- 记录详细错误日志
- 返回"网络故障,请稍后再试"
Testing Strategy
单元测试
使用xUnit框架编写单元测试,覆盖以下场景:
-
AuthService测试
- 微信登录成功/失败场景
- 手机号登录成功/失败场景
- 防抖机制测试
- 账户合并测试
-
JwtService测试
- Token生成测试
- Token验证测试
- Token过期测试
-
UserService测试
- 用户查找测试
- 用户创建测试
- 用户信息更新测试
- VIP等级计算测试
属性测试
使用FsCheck库进行属性测试,每个属性测试运行至少100次迭代:
- Property 1-5: 登录相关属性测试
- Property 6-8: Token相关属性测试
- Property 9-11: 用户信息相关属性测试
- Property 12-14: 手机号绑定相关属性测试
- Property 15-17: 日志和注销相关属性测试
集成测试
-
API端到端测试
- 完整的微信登录流程
- 完整的手机号登录流程
- 用户信息获取和更新流程
-
数据库集成测试
- 用户创建和查询
- 账户合并
- 登录日志记录
测试配置
// 属性测试配置示例
[Property(MaxTest = 100)]
public Property LoginShouldGenerateValidToken()
{
return Prop.ForAll(
Arb.From<ValidLoginRequest>(),
request => {
var result = _authService.LoginAsync(request).Result;
return result.Success && !string.IsNullOrEmpty(result.Token);
});
}