Add XCode skills for entity caching, ORM, and sharding ETL
|
---
name: cube-membership
description: >
使用 NewLife.Cube 的用户认è¯ä¸Žæƒé™ç®¡ç†ä½“系,涵盖 ManageProvider 用户上下文管ç†ã€
UserService 登录/注册/验è¯ç (密ç /çŸä¿¡/邮件三模å¼ï¼‰ã€PasswordService 密ç 强度验è¯ã€
TokenService JWT é¢å‘与验è¯ï¼ˆHS256/RS256 ç®—æ³•æ ¼å¼ï¼‰ã€AccessService 访问日志,
ä»¥åŠ User/Role/UserToken/UserOnline/UserConnect æ ¸å¿ƒå®žä½“æ“作。
é€‚ç”¨äºŽç”¨æˆ·ç™»å½•ã€æ³¨å†Œã€å¯†ç ç–ç•¥ã€ä»¤ç‰Œé¢å‘ã€åœ¨çº¿ä¼šè¯ç»Ÿè®¡ã€æƒé™æ£€æŸ¥ç‰åœºæ™¯ã€‚
argument-hint: >
说明场景:是登录鉴æƒï¼ˆå¯†ç /çŸä¿¡/é‚®ä»¶ï¼‰ã€æ³¨å†Œæµç¨‹ã€å¯†ç ç–ç•¥ã€
还是 JWT é¢å‘与验è¯ï¼Ÿæ˜¯å¦éœ€è¦åœ¨çº¿ä¼šè¯æˆ–访问日志?
---
# Cube Membership 用户认è¯ä¸Žæƒé™
## 适用场景
- 实现用户密ç 登录ã€çŸä¿¡éªŒè¯ç 登录ã€é‚®ä»¶éªŒè¯ç 登录。
- å‘é€çŸä¿¡/邮件验è¯ç (å«é˜²åˆ·é™æµæœºåˆ¶ï¼‰ã€‚
- é…置密ç 强度æ£åˆ™ç–略,强制密ç 夿‚åº¦è¦æ±‚。
- é¢å‘å’ŒéªŒè¯ JWT 令牌(应用级 Token)。
- 获å–当å‰ç™»å½•用户ã€å½“å‰ç§Ÿæˆ·ï¼Œç®¡ç†ä¼šè¯ç”Ÿå‘½å‘¨æœŸã€‚
- 记录和查询用户访问日志与在线状æ€ç»Ÿè®¡ã€‚
---
## ManageProvider — 用户上下文
### 获å–当å‰ç”¨æˆ·
```csharp
// æ–¹å¼1ï¼šé€šè¿‡é™æ€å±žæ€§ï¼ˆæŽ¨è,已绑定到当å‰è¯·æ±‚上下文)
var user = ManageProvider.User as User;
// æ–¹å¼2:从控制器基类属性
// ControllerBaseX ä¸ç›´æŽ¥å¯ç”¨ï¼š
var user = CurrentUser as User;
// æ–¹å¼3:通过æä¾›è€…实例
var user = ManageProvider.Provider.GetCurrent();
```
### 登录与注销
```csharp
// 密ç 登录(基础接å£ï¼ŒæŽ¨è通过 UserService.Login())
var provider = ManageProvider.Provider;
provider.Login(username, password, remember: true);
// æ³¨é”€ï¼ˆæ¸…ç† Session + Cookie)
provider.Logout();
// 手动设置当å‰ç”¨æˆ·ï¼ˆå¦‚ OAuth 回调åŽï¼‰
provider.SetCurrent(user);
```
### 委托代ç†ï¼ˆIdentity Delegation)
```csharp
// æ£€æŸ¥æ˜¯å¦æœ‰å¯ç”¨çš„身份委托(用于代ç†å¦ä¸€ç”¨æˆ·èº«ä»½ï¼‰
// Login() å†…éƒ¨è‡ªåŠ¨è°ƒç”¨ï¼Œæ— éœ€æ‰‹åŠ¨è§¦å‘
provider.CheckAgent(user); // è‹¥å˜åœ¨æœ‰æ•ˆä»£ç†ï¼Œè¿”回被代ç†ç”¨æˆ·
// 查询æŸç”¨æˆ·çš„æ‰€æœ‰æœ‰æ•ˆä»£ç†
var agents = PrincipalAgent.GetAllValidByAgentId(userId);
```
### ManageProvider 关键属性
| 属性 | 说明 |
|------|------|
| `ManageProvider.User` | 当å‰è¯·æ±‚çš„ç™»å½•ç”¨æˆ·ï¼ˆé™æ€ï¼Œç»‘定到 AsyncLocal) |
| `ManageProvider.Provider` | å½“å‰æä¾›è€…å®žä¾‹ï¼ˆæ ¹æ®ç§Ÿæˆ·ä¸Šä¸‹æ–‡åˆ‡æ¢ï¼‰ |
| `ManageProvider.Menu` | èœå•管ç†å™¨ï¼ˆç”¨äºŽèœå•æƒé™æŸ¥æ‰¾ï¼‰ |
| `TenantContext.CurrentId` | 当å‰ç§Ÿæˆ· ID |
---
## UserService — 统一登录æœåŠ¡
### 三ç§ç™»å½•模å¼
```csharp
// 注入方å¼
[Inject] public UserService UserSvc { get; set; }
// 构建登录请求模型
var model = new LoginModel
{
Username = "alice",
Password = "MyPassword123",
LoginCategory = LoginCategory.Password, // 必填:密ç /手机/邮件
Remember = true,
Pkey = "rsa-key-id", // 用于解密å‰ç«¯ RSA åŠ å¯†å¯†ç
};
// 统一登录入å£ï¼ˆè‡ªåŠ¨åˆ†å‘到对应模å¼ï¼‰
ServiceResult<IToken> result = UserSvc.Login(model, HttpContext);
if (result.IsSuccess)
{
var token = result.Data; // IToken: AccessToken + RefreshToken + ExpireIn
return Json(0, "登录æˆåŠŸ", token);
}
else
{
return Json(401, result.Message);
}
```
**LoginCategory 枚举:**
| 值 | 说明 |
|----|------|
| `Password` | è´¦å·å¯†ç 登录(支æŒå‰ç«¯ RSA åŠ å¯†å¯†ç ) |
| `Phone` | 手机验è¯ç 登录(自动注册新用户) |
| `Email` | 邮箱验è¯ç 登录(自动注册新用户) |
### å‘é€éªŒè¯ç
```csharp
// å‘é€çŸä¿¡éªŒè¯ç (自动防刷:60ç§’é—´éš” + IPé™5次/10分)
var record = await UserSvc.SendVerifyCode(new VerifyCodeModel
{
Username = "13800138000", // 手机å·
Channel = "Sms", // "Sms" 或 "Mail"
Action = "login", // "login" / "bind" / "reset" / "notify"
}, ip: UserHost);
// å‘é€é‚®ä»¶éªŒè¯ç
var record = await UserSvc.SendVerifyCode(new VerifyCodeModel
{
Username = "alice@example.com",
Channel = "Mail",
Action = "reset", // é‡ç½®å¯†ç 场景
}, ip: UserHost);
```
**Action 类型说明:**
| Action | 场景 | 缓å˜å‰ç¼€éš”离 |
|--------|------|-------------|
| `login` | 验è¯ç 登录 | 独立计数器 |
| `bind` | 绑定手机/邮件 | 独立计数器 |
| `reset` | é‡ç½®å¯†ç | 独立计数器 |
| `notify` | 通知(默认) | 独立计数器 |
### 在线会è¯ç®¡ç†
```csharp
// 更新用户在线记录(RunTimeMiddleware 自动调用)
var online = UserSvc.SetWebStatus(
online: existing,
sessionId: HttpContext.Session?.Id,
deviceId: Request.Cookies["deviceId"],
page: Request.Path,
status: "ok",
userAgent: new UserAgentParser(Request.Headers["User-Agent"]),
user: ManageProvider.User,
ip: UserHost
);
// 清ç†20åˆ†é’Ÿæ— æ´»åŠ¨çš„è¿‡æœŸä¼šè¯ï¼ˆDataRetentionService 自动调用)
var expired = UserSvc.ClearExpire(secTimeout: 20 * 60);
```
---
## PasswordService — 密ç 强度验è¯
```csharp
// 注入或通过 DI 获å–
[Inject] public PasswordService PwdSvc { get; set; }
// 验è¯å¯†ç 是å¦ç¬¦åˆå¼ºåº¦è¦æ±‚(基于 CubeSetting.PaswordStrength æ£åˆ™ï¼‰
var valid = PwdSvc.Valid("MyPassword123");
if (!valid)
throw new Exception("密ç ä¸ç¬¦åˆè¦æ±‚:需包å«å¤§å°å†™å—æ¯å’Œæ•°å—,至少8ä½");
```
**é…置密ç 强度(appsettings.json):**
```json
{
"Cube": {
"PaswordStrength": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$"
}
}
```
| æ£åˆ™ç¤ºä¾‹ | è¦æ±‚ |
|---------|------|
| `*` 或空 | æ— è¦æ±‚(默认) |
| `^.{6,}$` | 至少6ä½ |
| `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$` | 大å°å†™+æ•°å—+8ä½ |
| `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^\\w]).{10,}$` | 大å°å†™+æ•°å—+特殊å—符+10ä½ |
---
## TokenService — JWT 令牌
### é¢å‘令牌
```csharp
// 注入
[Inject] public TokenService TokenSvc { get; set; }
// é¢å‘(name=ç”¨æˆ·åæˆ–应用å,secret æ ¼å¼ï¼š"算法:密钥")
var token = TokenSvc.IssueToken(
name: user.Name,
secret: CubeSetting.Current.JwtSecret, // "HS256:MySecretKey..."
expire: 7200, // 有效期(秒)
id: Rand.NextString(8) // å¯é€‰ï¼šä»¤ç‰Œ ID
);
// 返回 IToken { AccessToken, TokenType="JWT", ExpireIn=7200, RefreshToken }
```
### 验è¯ä»¤ç‰Œ
```csharp
// 验è¯å¹¶è§£ç (å«å¼‚常)
var (jwt, app) = TokenSvc.DecodeToken(token, CubeSetting.Current.JwtSecret);
var username = jwt.Subject; // 令牌ä¸çš„用户å
var expireAt = jwt.Expire; // 过期时间
// 验è¯å¹¶è¿”å›žå¼‚å¸¸å¯¹è±¡ï¼ˆä¸æŠ›å‡ºï¼‰
var (jwt, ex) = TokenSvc.DecodeTokenWithError(token, jwtSecret);
if (ex != null) return Json(403, "ä»¤ç‰Œæ— æ•ˆï¼š" + ex.Message);
```
### 自动ç»ç¾
```csharp
// è¿‡æœŸå‰ 10 分钟内返回新令牌,其余时间返回 null
var newToken = TokenSvc.ValidAndIssueToken(user.Name, oldToken, jwtSecret, expire: 7200);
if (newToken != null) Response.Headers["X-Token"] = newToken.AccessToken;
```
### JWT å¯†é’¥æ ¼å¼
```
"算法:密钥"
```
| æ ¼å¼ç¤ºä¾‹ | 算法 | 说明 |
|---------|------|------|
| `HS256:MySecretKey123456` | HMAC-SHA256 | 对称密钥,推è长度 ≥ 32 å—符 |
| `RS256:-----BEGIN RSA PRIVATE KEY-----...` | RSA-SHA256 | éžå¯¹ç§°ï¼ŒéªŒè¯æ—¶ç”¨å…¬é’¥ |
---
## 应用授æƒï¼ˆTokenService.Authorize)
ç”¨äºŽéªŒè¯æŽ¥å…¥åº”ç”¨ï¼ˆApp å®žä½“ï¼‰çš„èº«ä»½ï¼Œæ”¯æŒ IP 白åå•。
```csharp
// 验è¯åº”用å‡è¯ï¼ˆä¸å˜åœ¨æ—¶æ ¹æ® autoRegister 决定是å¦è‡ªåŠ¨åˆ›å»ºï¼‰
var app = TokenSvc.Authorize(
username: "OrderWeb", // 应用å
password: "app-secret-key", // 应用密钥
autoRegister: false, // 是å¦å…许自动注册新应用
ip: UserHost // è¯·æ±‚æ¥æº IP(用于白å啿£€æŸ¥ï¼‰
);
```
---
## æ ¸å¿ƒå®žä½“é€ŸæŸ¥
### User(用户)
```csharp
// 常用查找方法
User.FindByName(username)
User.FindByMobile(mobile)
User.FindByMail(mail)
User.FindByKey(id)
User.Find(User._.Code == code)
// 密ç 验è¯ï¼ˆåŸºäºŽ Membership.User 基类)
var user = User.FindByName(username);
if (user.Password != PasswordService.Hash(password, user.Name))
throw new Exception("密ç 错误");
// å¸¸ç”¨å—æ®µ
user.Name // 登录å
user.DisplayName // 显示å/昵称
user.Mail // 邮箱
user.Mobile // 手机å·
user.Avatar // å¤´åƒ URL
user.Enable // 是å¦å¯ç”¨
user.Roles // 角色列表(IRole[])
user.IsAdmin // 是å¦è¶…级管ç†å‘˜
```
### Role(角色)
```csharp
Role.FindByName(roleName)
Role.FindAllByNames(roleNames)
// 判æ–ç”¨æˆ·æ˜¯å¦æœ‰æŸè§’色
user.Roles.Any(r => r.Name == "Admin");
// 检查æƒé™
user.Has(menu, PermissionFlags.Update)
```
### UserToken(用户令牌记录)
```csharp
// 查找有效 Token 记录(å¯ç”¨äºŽå®žçް Token 黑åå•/刷新)
UserToken.FindByToken(accessToken)
UserToken.FindAllByUserId(userId)
// æ ¸å¿ƒå—æ®µ
token.Token // AccessToken å—符串
token.RefreshToken // 刷新令牌
token.Expire // 过期时间
token.DeviceId // 设备 ID
token.Enable // æ˜¯å¦æœ‰æ•ˆ
```
### UserConnect(第三方账å·ç»‘定)
```csharp
// é€šè¿‡ç¬¬ä¸‰æ–¹è´¦å·æŸ¥æ‰¾binding
UserConnect.FindByProviderAndOpenID("Weixin", openID)
UserConnect.FindAllByUserId(userId)
// æ ¸å¿ƒå—æ®µ
uc.Provider // "Weixin" / "DingTalk" / "QQ" ç‰
uc.OpenID // 第三方 OpenID
uc.UnionID // 第三方 UnionID(跨应用)
uc.UserID // 本地用户 ID
uc.NickName // 第三方昵称
uc.Avatar // ç¬¬ä¸‰æ–¹å¤´åƒ URL
```
### UserOnline(在线会è¯ï¼‰
```csharp
UserOnline.FindBySessionID(sessionId)
UserOnline.FindAllByUserId(userId)
// æ ¸å¿ƒå—æ®µ
online.SessionID // Session æ ‡è¯†
online.DeviceId // 设备 ID
online.Page // 当å‰é¡µé¢è·¯å¾„
online.Platform // æ“作系统平å°
online.Brower // æµè§ˆå™¨
online.NetType // 网络类型(Wifi/4G ç‰ï¼‰
online.OnlineTime // 在线时长(秒)
online.Address // IP 归属地
```
---
## AccessService — 访问日志
```csharp
// AccessService ç”± RunTimeMiddleware è‡ªåŠ¨è°ƒç”¨ï¼Œæ— éœ€æ‰‹åŠ¨ä½¿ç”¨ã€‚
// 管ç†åŽå°æŸ¥çœ‹æ—¥å¿—ï¼šç³»ç»Ÿç®¡ç† â†’ 访问日志
// 若需手动记录
var access = new UserVisit
{
UserId = ManageProvider.User?.ID ?? 0,
Page = Request.Path,
Action = "API调用",
Ip = UserHost,
TraceId = DefaultSpan.Current?.TraceId,
};
access.Insert();
```
---
## 陿µä¸Žå®‰å…¨é…置(CubeSetting)
```csharp
var set = CubeSetting.Current;
// 登录安全
set.MaxLoginError // 最大错误次数(默认5),超出åŽå°ç¦
set.LoginForbiddenTime // å°ç¦æ—¶é•¿ï¼ˆç§’,默认300)
// 密ç ç–ç•¥
set.PaswordStrength // æ£åˆ™è¡¨è¾¾å¼ï¼Œç©ºæˆ–"*"=ä¸é™åˆ¶
// 令牌é…ç½®
set.JwtSecret // "算法:密钥" æ ¼å¼ï¼ˆå¿…é¡»é…ç½®ï¼ï¼‰
set.TokenExpire // Token 有效期(秒,默认7200)
// 注册ç–ç•¥
set.AllowRegister // 是å¦å…许新用户注册
set.AutoRegister // OAuth ç™»å½•åŽæ˜¯å¦è‡ªåŠ¨æ³¨å†Œ
set.DefaultRole // 新注册用户的默认角色å(默认"普通用户")
// 会è¯ç–ç•¥
set.SessionTimeout // 会è¯è¶…时(秒,0=æµè§ˆå™¨å…³é—时过期)
set.RefreshUserPeriod // 刷新用户信æ¯å‘¨æœŸï¼ˆç§’,默认600)
```
---
## 常è§ä¾‹å¤–与注æ„事项
- `CubeSetting.JwtSecret` **ä¸èƒ½ä¸ºç©º**,å¦åˆ™ `TokenService.IssueToken()` 会抛出解æžå¼‚常;生产环境应通过 Secret 管ç†å·¥å…·æ³¨å…¥ï¼Œä¸è¦å†™å…¥ç‰ˆæœ¬æŽ§åˆ¶ã€‚
- `ManageProvider.User` 基于 `AsyncLocal<T>` å®žçŽ°ï¼Œè·¨çº¿ç¨‹ä¼ é€’æ—¶éœ€æ³¨æ„ `AsyncLocal` 的值æ•获行为。
- `UserService.Login()` 会自动调用 `ManageProvider.SaveCookie()`ï¼Œæ— éœ€åœ¨æŽ§åˆ¶å™¨å±‚å†æ¬¡å†™å…¥ Cookie。
- `MaxLoginError` 的错误计数å˜å‚¨åœ¨ **缓å˜ï¼ˆICache)** ä¸ï¼ŒæœåŠ¡é‡å¯åŽä¼šé‡ç½®ï¼›è‹¥éœ€æŒä¹…化å¯è‡ªå®šä¹‰ `ICacheProvider` 使用 Redis。
- `SendVerifyCode()` ä¸çš„ IP 陿µï¼ˆ5次/10分钟)基于请求 IP,内容分å‘网络(CDN)场景需确ä¿ä¼ 入真实客户端 IP(`X-Forwarded-For`)。
## æŽ¨èæ£€æŸ¥é¡¹
- [ ] `CubeSetting.JwtSecret` 是å¦å·²é…置(éžç©ºã€éžé»˜è®¤å€¼ï¼‰
- [ ] çŸä¿¡/邮件验è¯ç 功能是å¦å·²åœ¨ `CubeSetting` ä¸å¯ç”¨ï¼Œå¯¹åº”æœåС商é…置是å¦å®Œæ•´
- [ ] `MaxLoginError` å’Œ `LoginForbiddenTime` æ˜¯å¦æ ¹æ®ä¸šåŠ¡å®‰å…¨è¦æ±‚调整
- [ ] `PasswordService.Valid()` 在用户注册/é‡ç½®å¯†ç 时是å¦å·²è°ƒç”¨
- [ ] Token å“应ä¸çš„ `ExpireIn` 是å¦å·²å‘ŠçŸ¥å‰ç«¯ç”¨äºŽå®žçŽ°è‡ªåŠ¨ç»ç¾é€»è¾‘
- [ ] `UserConnect` 表是å¦ä¸ºç¬¬ä¸‰æ–¹ç™»å½•æä¾›äº†å”¯ä¸€ç´¢å¼•(Provider + OpenID)
|