Add XCode skills for entity caching, ORM, and sharding ETL
|
---
name: cube-oauth-sso
description: >
使用 NewLife.Cube çš„ OAuth/SSO 体系实现第三方登录和å•点登录,
涵盖 OAuthClient 基类(14+ 内置æä¾›è€…)ã€OAuthConfig æ•°æ®åº“é…ç½®ã€
SsoController 回调处ç†ã€OAuthServer 作为 SSO æœåŠ¡ç«¯ï¼ˆAuthorize/Token/UserInfo 端点),
ä»¥åŠ SsoClient 作为 SSO å®¢æˆ·ç«¯ï¼ˆå¯†ç æ¨¡å¼/客户端å‡è¯æ¨¡å¼ï¼‰ã€‚
适用于微信/钉钉/ä¼ä¸šå¾®ä¿¡ç‰ç¬¬ä¸‰æ–¹ç™»å½•接入ã€è‡ªå»º SSO ä¸å¿ƒã€è·¨ç³»ç»Ÿç»Ÿä¸€è®¤è¯ç‰åœºæ™¯ã€‚
argument-hint: >
说明场景:是接入第三方平å°ï¼ˆå¾®ä¿¡/钉钉ç‰ï¼‰ã€ä½¿ç”¨é”方作为 SSO æœåŠ¡ç«¯ï¼Œ
还是作为 SSO 客户端接入外部 SSO?需è¦çš„æŽˆæƒæ¨¡å¼ï¼ˆæŽˆæƒç /密ç /客户端å‡è¯ï¼‰ï¼Ÿ
---
# Cube OAuth / SSO å•点登录
## 适用场景
- ä¸ºåº”ç”¨æ·»åŠ å¾®ä¿¡å…¬ä¼—å·ã€å¾®ä¿¡å°ç¨‹åºã€ä¼ä¸šå¾®ä¿¡ã€é’‰é’‰ã€QQã€GitHub ç‰ç¬¬ä¸‰æ–¹ç™»å½•。
- 将锿–¹ä½œä¸º OAuth2 SSO æœåŠ¡ç«¯ï¼Œå‘å系统统一æä¾›ç”¨æˆ·è®¤è¯ã€‚
- 通过 `SsoClient` 在æœåŠ¡é—´ä»¥å¯†ç æ¨¡å¼æˆ–客户端å‡è¯æ¨¡å¼è¿›è¡Œç”¨æˆ·èº«ä»½éªŒè¯ã€‚
- 管ç†åŽå°é…置第三方 OAuth åº”ç”¨ï¼Œæ— éœ€ä¿®æ”¹ä»£ç 。
---
## 内置 OAuth æä¾›è€…
| å称(Name) | ç±» | 说明 |
|----|----|----|
| `Weixin` | `WeixinClient` | 微信公众å·ç½‘é¡µæŽˆæƒ |
| `WxApp` | `WxAppClient` | 微信å°ç¨‹åº code æ¢ openid |
| `WxOpen` | `WxOpenClient` | å¾®ä¿¡å¼€æ”¾å¹³å° |
| `QyWeixin` / `QyWeiXin` | `QyWeiXin` | ä¼ä¸šå¾®ä¿¡åº”ç”¨æŽˆæƒ |
| `DingTalk` | `DingTalkClient` | 钉钉ä¼ä¸šåº”用å…ç™» |
| `Alipay` | `AlipayClient` | æ”¯ä»˜å®æŽˆæƒ |
| `QQ` | `QQClient` | QQ äº’è” |
| `Github` | `GithubClient` | GitHub Developer OAuth |
| `Weibo` | `WeiboClient` | å¾®åšç™»å½• |
| `Baidu` | `BaiduClient` | ç™¾åº¦è´¦å· |
| `Microsoft` | `MicrosoftClient` | å¾®è½¯è´¦å· |
| `Id4` | `Id4Client` | IdentityServer4 自建 OAuth |
| `Taobao` | `TaobaoClient` | æ·˜å®å¼€æ”¾å¹³å° |
| `NewLife` | `OAuthClient`(基类) | 锿–¹è‡ªèº« SSO æœåŠ¡ç«¯ |
---
## OAuthConfig — æ•°æ®åº“é…ç½®
所有 OAuth é…ç½®å˜å‚¨åœ¨ `OAuthConfig` 表,通过管ç†åŽå°ï¼ˆ**ç³»ç»Ÿç®¡ç† â†’ OAuthé…ç½®**ï¼‰ç»´æŠ¤ï¼Œæ— éœ€ç¡¬ç¼–ç 。
### æ ¸å¿ƒå—æ®µ
```csharp
public class OAuthConfig
{
public String Name { get; set; } // æä¾›è€…唯一å,与 OAuthClient å类绑定
public String NickName { get; set; } // 登录页显示å(如"微信公众å·")
public String Logo { get; set; } // ç™»å½•æŒ‰é’®å›¾æ ‡ URL
// 应用å‡è¯
public String AppId { get; set; } // 第三方 AppId / ClientId
public String Secret { get; set; } // 第三方 AppSecret / ClientSecret
public String Scope { get; set; } // 授æƒèŒƒå›´ï¼ˆsnsapi_userinfo/user_info ç‰ï¼‰
// æœåŠ¡ç«¯ç‚¹ï¼ˆä½¿ç”¨å†…ç½®å®¢æˆ·ç«¯æ—¶æ— éœ€å¡«å†™ï¼Œå·²å†…ç½®ï¼‰
public String Server { get; set; } // OAuth æœåŠ¡åŸºåœ°å€
public String AuthUrl { get; set; } // 授æƒç«¯ç‚¹ URL
public String AccessUrl { get; set; } // 令牌端点 URL
public String UserUrl { get; set; } // 用户信æ¯ç«¯ç‚¹ URL
public String AppUrl { get; set; } // 本应用外部地å€ï¼ˆåå‘ä»£ç†æ—¶ä½¿ç”¨ï¼‰
// 授æƒç±»åž‹
public GrantTypes GrantType { get; set; } // AuthorizationCode/Password/ClientCredentials
public String FieldMap { get; set; } // å—æ®µæ˜ å°„ JSON:{"openid":"user_id"}
// 功能开关
public Boolean Enable { get; set; } // 是å¦å¯ç”¨
public Boolean Visible { get; set; } // 登录页是å¦å±•ç¤ºæ¤æ–¹å¼
public Boolean AutoRegister { get; set; } // 是å¦è‡ªåŠ¨æ³¨å†Œæ–°ç”¨æˆ·
public Boolean FetchAvatar { get; set; } // 是å¦ä¸‹è½½ç¬¬ä¸‰æ–¹å¤´åƒåˆ°æœ¬åœ°
public Boolean Debug { get; set; } // 输出调试日志
}
```
### 枚举值
```csharp
public enum GrantTypes
{
AuthorizationCode = 0, // 授æƒç 模å¼ï¼ˆç½‘页登录,最常用)
Implicit, // éšå¼æ¨¡å¼ï¼ˆå·²å¼ƒç”¨ï¼‰
Password, // å¯†ç æ¨¡å¼ï¼ˆæœåŠ¡ç«¯ç›´æŽ¥éªŒè¯ï¼‰
ClientCredentials, // 客户端å‡è¯ï¼ˆæœºå™¨å¯¹æœºå™¨ï¼‰
}
```
---
## 第三方登录æµç¨‹ï¼ˆä»¥å¾®ä¿¡ä¸ºä¾‹ï¼‰
### 1. é…ç½® OAuthConfig(管ç†åŽå°ä¸€æ¬¡æ€§æ“作)
```
ç³»ç»Ÿç®¡ç† â†’ OAuthé…ç½® → æ·»åŠ
Name: Weixin
NickName: 微信公众å·
AppId: wx1234567890abcdef
Secret: ä½ çš„-app-secret
Scope: snsapi_userinfo
Enable: ✓
Visible: ✓
AutoRegister:✓
```
### 2. 用户点击"微信登录"
æµè§ˆå™¨è®¿é—® `/Sso/Login?name=Weixin&r=/dashboard`ï¼ˆå…¶ä¸ `r` 是登录æˆåŠŸåŽè·³è½¬åœ°å€ï¼‰ã€‚
### 3. SsoController 处ç†å›žè°ƒ
锿–¹å†…ç½® `SsoController` è‡ªåŠ¨å¤„ç†æ•´ä¸ª OAuth æµç¨‹ï¼š
```
1. GET /Sso/Login?name=Weixin&r=/dashboard
→ 创建 WeixinClient,Authorize() æž„å»ºæŽˆæƒ URL
→ é‡å®šå‘到微信授æƒé¡µ
2. 微信回调 GET /Sso/LoginInfo/Weixin?code=xxx&state=yyy
→ GetAccessToken(code) æ¢ access_token
→ GetUserInfo() èŽ·å– openid/nickname/avatar
→ 查找或自动注册本地 User(通过 UserConnect 绑定)
→ ManageProvider.Login(userId) 写入 Session + Cookie
→ é‡å®šå‘到 /dashboard
```
### 4. OAuthClient API(自定义æµç¨‹æ—¶ä½¿ç”¨ï¼‰
```csharp
// åœ¨è‡ªå®šä¹‰æŽ§åˆ¶å™¨ä¸æ‰‹åŠ¨æŽ§åˆ¶ OAuth æµç¨‹
public class CustomSsoController : ControllerBaseX
{
[AllowAnonymous]
public IActionResult LoginByWeixin()
{
// 1. 创建客户端(自动从 OAuthConfig åŠ è½½é…置)
var client = OAuthHelper.Create(TenantContext.CurrentId, "Weixin");
// 2. æž„å»ºæŽˆæƒ URL(state å˜åˆ° Session 用于防 CSRF)
var redirectUri = $"{Request.Scheme}://{Request.Host}/callback/Weixin";
var authUrl = client.Authorize(redirectUri, state: "random-state", Request.GetUri()....)ï¼›
return Redirect(authUrl);
}
[AllowAnonymous]
public async Task<IActionResult> CallbackWeixin(String code, String state)
{
var client = OAuthHelper.Create(TenantContext.CurrentId, "Weixin");
// 3. 用 code æ¢ access_token
await client.GetAccessToken(code); // å¡«å…… AccessToken
// 4. 用 access_token 获å–用户信æ¯
await client.GetUserInfo(); // å¡«å…… OpenID/NickName/Avatar/Mail ç‰
// 5. 通过 OpenID 查找绑定关系
var uc = UserConnect.FindByProviderAndOpenID("Weixin", client.OpenID);
if (uc == null)
{
// 首次登录:自动注册本地用户
uc = new UserConnect { Provider = "Weixin", OpenID = client.OpenID };
var user = new User { Name = client.NickName, DisplayName = client.NickName };
user.Insert();
uc.UserID = user.ID;
uc.Insert();
}
// 6. 登录
ManageProvider.Provider.SetCurrent(User.FindByKey(uc.UserID));
return Redirect("/");
}
}
```
---
## 锿–¹ä½œä¸º SSO æœåŠ¡ç«¯ï¼ˆOAuthServer)
锿–¹å¯å¯¹å¤–æä¾›æ ‡å‡† OAuth2 授æƒç 模å¼ï¼Œè®©åç³»ç»Ÿé€šè¿‡é”æ–¹ç»Ÿä¸€ç™»å½•。
### 端点列表(SsoController æä¾›ï¼‰
| 端点 | 方法 | 说明 |
|------|------|------|
| `/Sso/Authorize` | GET | å系统é‡å®šå‘到æ¤å¤„ï¼Œé”æ–¹æ˜¾ç¤ºç™»å½•ç•Œé¢ |
| `/Sso/Auth2` | GET | 用户登录åŽå†…éƒ¨è·³è½¬ï¼Œç”Ÿæˆ code |
| `/Sso/Access_Token` | GET/POST | å系统用 code æ¢ access_token |
| `/Sso/Token` | GET/POST | å¯†ç æ¨¡å¼/客户端å‡è¯æ¨¡å¼ |
| `/Sso/UserInfo` | GET/POST | å系统用 token 获å–ç”¨æˆ·ä¿¡æ¯ |
| `/Sso/Logout` | GET | 注销(å¯ä¼ `redirect_uri` 傿•°ï¼‰ |
### å系统接入æ¥éª¤
**第一æ¥ï¼šåœ¨é”方管ç†åŽå°æ³¨å†Œå系统应用**
```
ç³»ç»Ÿç®¡ç† â†’ 应用系统(App)→ æ·»åŠ
Name: OrderWeb
Secret: app-secret-key
Enable: ✓
å…许IP: (留空=ä¸é™ï¼Œæˆ–填写å系统 IP)
```
**第二æ¥ï¼šå系统é…ç½® SsoClient**
```csharp
// å系统 appsettings.json(或 Cube é…置文件)
{
"SsoServer": "https://sso.company.com",
"AppId": "OrderWeb",
"AppSecret": "app-secret-key",
"JwtKey": "main$-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----"
}
// å系统代ç ä¸åˆ›å»º SsoClient
var sso = SsoClient.Create("NewLife"); // Name 对应 OAuthConfig ä¸ NewLife æä¾›è€…
// 也å¯ä»¥ç›´æŽ¥å®žä¾‹åŒ–
var sso = new SsoClient
{
Server = "https://sso.company.com",
AppId = "OrderWeb",
Secret = "app-secret-key",
SecurityKey = "main$-----BEGIN PUBLIC KEY-----\nMII..."
};
```
**第三æ¥ï¼šåç³»ç»Ÿå¯†ç æ¨¡å¼ç™»å½•(æœåŠ¡ç«¯ç›´è¿žï¼‰**
```csharp
// å¯†ç æ¨¡å¼ï¼ˆå¯†ç 会被 RSA å…¬é’¥åŠ å¯†åŽä¼ 输)
var token = await sso.GetToken("alice@company.com", "user-password");
var user = await sso.GetUser(token.AccessToken);
Console.WriteLine($"用户 {user.Name},角色:{user.Roles}");
// 刷新令牌
var newToken = await sso.RefreshToken(token.AccessToken);
// 一体化验è¯ï¼ˆç›´æŽ¥è¿”回用户信æ¯ï¼‰
var userInfo = await sso.UserAuth("alice@company.com", "user-password");
```
**第四æ¥ï¼šå®¢æˆ·ç«¯å‡è¯æ¨¡å¼ï¼ˆåº”用间 API 调用)**
```csharp
// 用应用å‡è¯æ¢å–应用级令牌
var appToken = await sso.GetToken(deviceId: "server-app-001");
// 用于机器间 API 调用,ä¸å…³è”最终用户
```
---
## SsoClient API 速查
| 方法 | 说明 |
|------|------|
| `GetToken(username, password)` | å¯†ç æ¨¡å¼ï¼šç”¨æˆ·åå¯†ç æ¢ä»¤ç‰Œï¼ˆå¯†ç RSA å…¬é’¥åŠ å¯†ï¼‰ |
| `GetToken(deviceId)` | 客户端å‡è¯æ¨¡å¼ï¼šè®¾å¤‡ ID æ¢ä»¤ç‰Œ |
| `RefreshToken(accessToken)` | åˆ·æ–°å·²æœ‰ä»¤ç‰Œï¼ŒèŽ·å–æ–° AccessToken |
| `GetUserInfo(accessToken)` | 用令牌获å–用户信æ¯ï¼ˆåŽŸå§‹å—典) |
| `GetUser(accessToken)` | 用令牌获å–强类型用户对象 |
| `UserAuth(username, password)` | 一体化:验è¯å¹¶ç›´æŽ¥è¿”å›žç”¨æˆ·ä¿¡æ¯ |
| `GetKey(client_id, client_secret)` | èŽ·å– JWT 验è¯å…¬é’¥ |
---
## OAuthClient 基类 — 坿‰©å±•属性
自定义 OAuth æä¾›è€…时继承 `OAuthClient`:
```csharp
public class MyOAuthClient : OAuthClient
{
public MyOAuthClient()
{
Name = "MyProvider";
Server = "https://auth.example.com";
AuthUrl = "/oauth/authorize";
AccessUrl = "/oauth/token";
UserUrl = "/api/userinfo";
Scope = "read:user";
// å—æ®µæ˜ å°„ï¼ˆç¬¬ä¸‰æ–¹å—æ®µå → 锿–¹æ ‡å‡†å—段å)
FieldMap = new Dictionary<String, Object>
{
["login"] = "UserName", // GitHub çš„ login æ˜ å°„åˆ° UserName
["avatar_url"] = "Avatar",
["email"] = "Mail",
};
}
}
```
---
## 常è§ä¾‹å¤–与注æ„事项
- 微信公众å·åœ¨**éžå¾®ä¿¡å†…ç½®æµè§ˆå™¨**䏿— 法使用 `snsapi_userinfo` scope,需改为 `snsapi_base`ï¼ˆä»…èŽ·å– OpenID)。
- `OAuthConfig.AppUrl` 在åå‘代ç†ï¼ˆNginx/网关)场景下必须填写应用的**外网地å€**,å¦åˆ™ `redirect_uri` 会被构建为内网地å€å¯¼è‡´å›žè°ƒå¤±è´¥ã€‚
- `SsoClient.SecurityKey` æ ¼å¼ä¸º `"keyName$PEM-PUBLIC-KEY"`ï¼Œå…¶ä¸ `keyName` 为公钥å(固定 `"main"`),`$` åŽä¸º PEM æ ¼å¼ RSA 公钥(Base64 encoded)。
- `OAuthConfig.FieldMap` 是 JSON å—ç¬¦ä¸²ï¼Œæ ¼å¼ä¸º `{"ç¬¬ä¸‰æ–¹å—æ®µå":"æ ‡å‡†å—æ®µå"}`,用于适é…éžæ ‡å‡†ç¬¬ä¸‰æ–¹æŽ¥å£çš„å—æ®µå‘½å。
- åŒä¸€ `Provider`(如 `"Weixin"`)在åŒä¸€ç§Ÿæˆ·å†…åªèƒ½æœ‰ä¸€æ¡ `OAuthConfig`,多租户按 `TenantId` 隔离。
## æŽ¨èæ£€æŸ¥é¡¹
- [ ] `OAuthConfig.Enable` å’Œ `OAuthConfig.Visible` 是å¦å‡å·²å¯ç”¨ï¼ˆäºŒè€…独立控制)
- [ ] 第三方平å°ä¸Š OAuth åº”ç”¨å›žè°ƒåœ°å€æ˜¯å¦ä¸Žåº”用实际地å€ä¸€è‡´ï¼ˆ`AppUrl` é…ç½®æ£ç¡®ï¼‰
- [ ] `SsoClient.SecurityKey` 是å¦é…置了 RSA å…¬é’¥ï¼ˆå¯†ç æ¨¡å¼å¿…须,é¿å…æ˜Žæ–‡ä¼ è¾“å¯†ç )
- [ ] å系统(App 实体)是å¦åœ¨é”方管ç†åŽå°æ³¨å†Œå¹¶å¯ç”¨
- [ ] `AutoRegister = true` 时是å¦å·²è€ƒè™‘自动注册用户的默认角色(由 `CubeSetting.DefaultRole` 决定)
|