epic-1-auth
执行 Epic 1 —— 用户认证与账号体系。实现 JWT 双 Token 认证、自定义角色权限、账号初始化。依赖 Epic 0 已完成。
SKILL.md.claude/skills/epic-1-auth/SKILL.md
GitHubEpic 1 — 用户认证与账号体系
前置条件
- Epic 0 已完成(解决方案可编译)
- 已读取
shareflow-context/SKILL.md
任务清单
后端
Domain 层
User 实体 (src/Domain/Entities/User.cs)
public class User : Entity<Guid>
{
public string Email { get; private set; } = string.Empty;
public string PasswordHash { get; private set; } = string.Empty;
public string DisplayName { get; private set; } = string.Empty;
public UserRole Role { get; private set; }
public UserStatus Status { get; private set; }
public Guid? BackendRoleId { get; private set; } // 自定义后台角色
public bool VerifyPassword(string password) => BCrypt.Net.BCrypt.Verify(password, PasswordHash);
public void SetPassword(string password) { PasswordHash = BCrypt.Net.BCrypt.HashPassword(password); SetUpdatedAt(); }
public void Activate() { Status = UserStatus.Active; SetUpdatedAt(); }
public void Disable() { Status = UserStatus.Disabled; SetUpdatedAt(); }
public static User Create(string email, string password, string displayName, UserRole role) =>
new() { Id = Guid.NewGuid(), Email = email.ToLower(), DisplayName = displayName, Role = role,
Status = UserStatus.Active, PasswordHash = BCrypt.Net.BCrypt.HashPassword(password) };
}
枚举 (src/Domain/Enums/UserRole.cs / UserStatus.cs)
public enum UserRole { SuperAdmin, BackendCustom, Sales, Client }
public enum UserStatus { Active, Disabled, PendingSetup }
BackendRole 实体 (src/Domain/Entities/BackendRole.cs)
public class BackendRole : Entity<Guid>
{
public string Name { get; private set; } = string.Empty;
public List<string> Permissions { get; private set; } = [];
}
RefreshToken 实体 (src/Domain/Entities/RefreshToken.cs)
public class RefreshToken : Entity<Guid>
{
public Guid UserId { get; private set; }
public string Token { get; private set; } = string.Empty;
public DateTime ExpiresAt { get; private set; }
public bool IsRevoked { get; private set; }
public bool IsActive => !IsRevoked && ExpiresAt > DateTime.UtcNow;
public void Revoke() { IsRevoked = true; SetUpdatedAt(); }
}
Repository 接口 (src/Domain/Interfaces/IUserRepository.cs)
public interface IUserRepository
{
Task<User?> GetByIdAsync(Guid id);
Task<User?> GetByEmailAsync(string email);
Task<bool> ExistsByEmailAsync(string email);
Task AddAsync(User user);
Task<IReadOnlyList<string>> GetPermissionsAsync(Guid userId); // 查 BackendRole.Permissions
}
public interface IRefreshTokenRepository
{
Task<RefreshToken?> GetByTokenAsync(string token);
Task AddAsync(RefreshToken token);
Task RevokeAllByUserIdAsync(Guid userId);
}
public interface IBackendRoleRepository
{
Task<BackendRole?> GetByIdAsync(Guid id);
Task<IReadOnlyList<BackendRole>> GetAllAsync();
Task AddAsync(BackendRole role);
Task UpdateAsync(BackendRole role);
Task DeleteAsync(Guid id);
}
Infrastructure 层
- EF Core Configuration(Fluent API)
- Repository 实现(3个)
AppDbContext添加 DbSet
Application 层
Mapster 映射 (src/Application/Auth/MappingConfig.cs)
public class AuthMappingConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<User, UserDto>()
.Map(dest => dest.RoleName, src => src.Role.ToString());
}
}
DTO (src/Application/Auth/DTOs/)
LoginRequest{ Email, Password }TokenResponse{ AccessToken, RefreshToken, ExpiresAt, User: UserDto }UserDto{ Id, Email, DisplayName, Role, Permissions[] }
IJwtService (src/Application/Auth/Interfaces/IJwtService.cs)
public interface IJwtService
{
string GenerateAccessToken(User user, IReadOnlyList<string> permissions);
string GenerateRefreshToken();
ClaimsPrincipal? ValidateToken(string token);
}
IAuthService
public interface IAuthService
{
Task<TokenResponse> LoginAsync(LoginRequest request);
Task<TokenResponse> RefreshTokenAsync(string refreshToken);
Task LogoutAsync(string refreshToken);
}
AuthService 核心逻辑:
- 查用户 → 验密码 → 检查状态
- 查 BackendRole.Permissions(Client/Sales 直接用 Role 判断)
- 生成 JWT AccessToken(Claims: userId, role, permissions[])
- 生成 RefreshToken → 存 DB
- Logout:Token 加入 Redis 黑名单(key:
token:blacklist:{jti},TTL = AccessToken 剩余时效)
IAccountProvisioningService
public interface IAccountProvisioningService
{
/// <summary>合同签约完成后自动创建客户账号</summary>
Task<(string email, string initialPassword)> ProvisionClientAccountAsync(
string clientName, string clientEmail, Guid contractId);
}
PermissionAttribute + PermissionFilter (src/Api/Filters/)
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class PermissionAttribute(string permission) : Attribute
{
public string Permission { get; } = permission;
}
// ActionFilter: 从 JWT Claims 读取 permissions[],校验 Permission 常量
权限常量 (src/Domain/Constants/Permissions.cs)
public static class Permissions
{
public const string ProjectWrite = "project.write";
public const string ProjectRead = "project.read";
public const string RevenueWrite = "revenue.write";
public const string RevenueVerify = "revenue.verify";
public const string DividendWrite = "dividend.write";
public const string DividendRead = "dividend.read";
public const string ContractRead = "contract.read";
public const string ContractRenew = "contract.renew";
public const string WithdrawalApprove = "withdrawal.approve";
public const string UserManage = "user.manage";
public const string RoleManage = "role.manage";
public const string ReportRead = "report.read";
}
API Controller
POST /v1/auth/login → AuthController.LoginAsync()
POST /v1/auth/refresh → AuthController.RefreshAsync()
POST /v1/auth/logout → [Authorize] AuthController.LogoutAsync()
角色管理(仅 SuperAdmin):
GET/POST /v1/admin/roles
PUT /v1/admin/roles/{id}/permissions
DELETE /v1/admin/roles/{id}
POST /v1/admin/users/{id}/role
前端
stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
const accessToken = ref<string | null>(null)
const user = ref<UserInfo | null>(null)
const permissions = ref<string[]>([])
function hasPermission(key: string): boolean {
if (user.value?.role === 'SuperAdmin') return true
return permissions.value.includes(key)
}
async function login(email: string, password: string) { ... }
async function refreshToken() { ... }
function logout() { ... }
return { accessToken, user, permissions, hasPermission, login, refreshToken, logout }
}, { persist: true })
路由守卫 router/guards.ts
- 未登录 →
/auth/login - 已登录按 Role 跳转:SuperAdmin/BackendCustom →
/admin,Sales →/sales,Client →/client
完成标准
-
POST /v1/auth/login返回 Token 对 -
[Permission("report.read")]无权限时返回 403 - SuperAdmin 绕过所有权限校验
- 前端登录后按角色自动跳转
- RefreshToken 流程正常(旧 Token 失效后自动续期)