589 lines
20 KiB
Markdown
589 lines
20 KiB
Markdown
# 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请求。
|
||
|
||
```csharp
|
||
[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请求。
|
||
|
||
```csharp
|
||
[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
|
||
|
||
认证服务接口。
|
||
|
||
```csharp
|
||
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
|
||
|
||
微信服务接口。
|
||
|
||
```csharp
|
||
public interface IWechatService
|
||
{
|
||
Task<WechatAuthResult> GetOpenIdAsync(string code);
|
||
Task<WechatMobileResult> GetMobileAsync(string code);
|
||
}
|
||
```
|
||
|
||
### 5. IJwtService
|
||
|
||
JWT服务接口。
|
||
|
||
```csharp
|
||
public interface IJwtService
|
||
{
|
||
string GenerateToken(User user);
|
||
ClaimsPrincipal ValidateToken(string token);
|
||
int? GetUserIdFromToken(string token);
|
||
}
|
||
```
|
||
|
||
### 6. IUserService
|
||
|
||
用户服务接口。
|
||
|
||
```csharp
|
||
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地理位置服务接口。
|
||
|
||
```csharp
|
||
public interface IIpLocationService
|
||
{
|
||
Task<IpLocationResult> GetLocationAsync(string ip);
|
||
}
|
||
```
|
||
|
||
## Data Models
|
||
|
||
### Request Models
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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实现登录防抖,防止用户频繁请求:
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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. 账户合并逻辑
|
||
|
||
当用户绑定的手机号已被其他账户使用时,需要合并账户:
|
||
|
||
```csharp
|
||
public async Task<BindMobileResult> MergeAccountsAsync(int currentUserId, int mobileUserId)
|
||
{
|
||
// 1. 将当前用户的openid迁移到手机号用户
|
||
// 2. 生成新的token
|
||
// 3. 删除当前用户和账户记录
|
||
// 4. 返回新token
|
||
}
|
||
```
|
||
|
||
### 4. Token兼容策略
|
||
|
||
为保持与旧系统兼容,同时使用JWT和数据库token:
|
||
|
||
```csharp
|
||
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库生成用户默认头像:
|
||
|
||
```csharp
|
||
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框架编写单元测试,覆盖以下场景:
|
||
|
||
1. **AuthService测试**
|
||
- 微信登录成功/失败场景
|
||
- 手机号登录成功/失败场景
|
||
- 防抖机制测试
|
||
- 账户合并测试
|
||
|
||
2. **JwtService测试**
|
||
- Token生成测试
|
||
- Token验证测试
|
||
- Token过期测试
|
||
|
||
3. **UserService测试**
|
||
- 用户查找测试
|
||
- 用户创建测试
|
||
- 用户信息更新测试
|
||
- VIP等级计算测试
|
||
|
||
### 属性测试
|
||
|
||
使用FsCheck库进行属性测试,每个属性测试运行至少100次迭代:
|
||
|
||
1. **Property 1-5**: 登录相关属性测试
|
||
2. **Property 6-8**: Token相关属性测试
|
||
3. **Property 9-11**: 用户信息相关属性测试
|
||
4. **Property 12-14**: 手机号绑定相关属性测试
|
||
5. **Property 15-17**: 日志和注销相关属性测试
|
||
|
||
### 集成测试
|
||
|
||
1. **API端到端测试**
|
||
- 完整的微信登录流程
|
||
- 完整的手机号登录流程
|
||
- 用户信息获取和更新流程
|
||
|
||
2. **数据库集成测试**
|
||
- 用户创建和查询
|
||
- 账户合并
|
||
- 登录日志记录
|
||
|
||
### 测试配置
|
||
|
||
```csharp
|
||
// 属性测试配置示例
|
||
[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);
|
||
});
|
||
}
|
||
```
|