diff --git a/README.md b/README.md index 4c965ec..b4b5ace 100644 --- a/README.md +++ b/README.md @@ -70,18 +70,59 @@ ### 环境要求 - .NET 9.0 SDK - Node.js 20+ / pnpm 9+ -- PostgreSQL 14+ -- Redis(可选,用于缓存) +- Docker Desktop(用于数据库和缓存容器) -### 后端启动 +### 使用 .NET Aspire 一键启动(推荐) + +.NET Aspire 可以自动编排和管理所有服务,包括数据库、缓存、API服务和前端应用。 ```bash # 克隆仓库 git clone https://github.com/AterDev/IAM.git cd IAM -# 配置数据库连接 -# 编辑 src/Services/ApiService/appsettings.Development.json +# 启动所有服务 +cd src/AppHost +dotnet run +``` + +这将自动启动: +- **PostgreSQL** 数据库容器 (端口 15432) +- **Redis** 缓存容器 (端口 16379) +- **数据库迁移服务** - 自动执行数据库迁移和数据种子 +- **IAM API 服务** - `https://localhost:7070` +- **IAM 管理前端** - `http://localhost:4200` +- **示例 API 服务** - `https://localhost:7000` +- **示例前端应用** - `http://localhost:4201` + +Aspire Dashboard 将自动在浏览器中打开,提供: +- 所有服务的实时状态监控 +- 日志查看和搜索 +- 分布式追踪 +- 指标和性能监控 + +**服务端口说明:** +| 服务 | 端口 | 说明 | +|------|------|------| +| IAM API | 7070 | 身份认证和授权服务 | +| IAM 管理前端 | 4200 | 管理后台界面 | +| 示例 API | 7000 | 示例后端服务 | +| 示例前端 | 4201 | 示例前端应用 | + +**默认配置已自动创建:** +- 管理员账号: `admin` / `MakeDotnetGreatAgain` +- OAuth作用域: openid, profile, email, address, phone, offline_access +- 前端客户端: `FrontClient` (支持授权码+PKCE流程) +- API客户端: `ApiClient` (支持客户端凭证流程) + +### 手动启动(高级用户) + +如果需要单独启动各个服务: + +#### 后端启动 + +```bash +# 确保 PostgreSQL 和 Redis 正在运行 # 运行数据库迁移 cd src/Services/MigrationService @@ -92,9 +133,9 @@ cd ../ApiService dotnet run ``` -API将在 `https://localhost:5001` 启动 +API将在 `https://localhost:7070` 启动 -### 前端启动 +#### 前端启动 ```bash cd src/ClientApp/WebApp @@ -108,10 +149,6 @@ pnpm start 管理门户将在 `http://localhost:4200` 启动 -默认管理员账号: -- 用户名: `admin` -- 密码: `Admin@123` - ### Docker部署 ```bash @@ -127,6 +164,7 @@ docker-compose up -d ``` IAM/ ├── src/ +│ ├── AppHost/ # .NET Aspire 应用编排 │ ├── Ater/ # 基础类库 │ │ ├── Ater.Common/ # 通用帮助类 │ │ ├── Ater.Web.Convention/ # Web约定 @@ -172,9 +210,11 @@ IAM/ ### 后端 - **框架**: ASP.NET Core 9.0 +- **编排**: .NET Aspire (开发环境) - **ORM**: Entity Framework Core - **数据库**: PostgreSQL -- **认证**: JWT Bearer +- **缓存**: Redis +- **认证**: JWT Bearer / OAuth 2.0 / OIDC - **文档**: Swagger/OpenAPI ### 前端 @@ -252,34 +292,71 @@ pnpm test:coverage ## 🤝 示例集成 +IAM 提供了完整的前后端集成示例,展示如何使用 OAuth 2.0 / OIDC 进行身份认证和授权。 + +所有示例项目都已集成到 .NET Aspire 中,可以一键启动所有服务。详见 [samples/README.md](samples/README.md) + ### .NET后端集成 参见 [samples/backend-dotnet/](samples/backend-dotnet/) +演示内容: +- JWT Bearer 令牌验证 +- 使用 IAM 作为授权服务器 +- 受保护的 API 端点 +- CORS 配置 + ```csharp // 配置JWT认证 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { - options.Authority = "https://your-iam-server"; - options.Audience = "your-api"; + options.Authority = "https://localhost:7070"; // IAM 服务器地址 + options.Audience = "ApiClient"; // API 客户端标识 }); ``` +**运行端口**: `https://localhost:7000` + ### Angular前端集成 参见 [samples/frontend-angular/](samples/frontend-angular/) +演示内容: +- OAuth 2.0 授权码流程 + PKCE +- 自动令牌管理和刷新 +- 受保护路由 +- HTTP 拦截器自动添加令牌 + ```typescript // 配置OIDC客户端 export const authConfig: AuthConfig = { - issuer: 'https://your-iam-server', - clientId: 'your-client-id', - redirectUri: window.location.origin + '/callback', + authority: 'https://localhost:7070', // IAM 服务器地址 + clientId: 'FrontClient', // 前端客户端标识 + redirectUri: window.location.origin, scope: 'openid profile email', responseType: 'code', - usePkce: true + usePkce: true // 启用 PKCE }; ``` +**运行端口**: `http://localhost:4201` + +### 快速体验 + +使用 .NET Aspire 一键启动所有服务: + +```bash +cd src/AppHost +dotnet run +``` + +然后访问: +- IAM 管理后台: `http://localhost:4200` +- 示例前端应用: `http://localhost:4201` + +使用默认账号登录: +- 用户名: `admin` +- 密码: `MakeDotnetGreatAgain` + ## 📋 待实现功能 详见 [未实现功能分析](docs/MISSING-FEATURES-ANALYSIS.md) diff --git a/docs/SEED-DATA-GUIDE.md b/docs/SEED-DATA-GUIDE.md new file mode 100644 index 0000000..5aa4944 --- /dev/null +++ b/docs/SEED-DATA-GUIDE.md @@ -0,0 +1,270 @@ +# IAM 初始化数据指南 + +本文档说明IAM系统在首次启动时自动创建的初始化数据。 + +## 概述 + +当运行 `MigrationService` 进行数据库迁移时,系统会自动创建以下初始化数据: +- 默认管理员账号 +- 默认角色 +- OAuth标准作用域 +- 默认OAuth客户端 + +这些数据确保系统可以立即使用,无需手动配置。 + +## 默认管理员账号 + +**账号信息:** +- 用户名:`admin` +- 密码:`MakeDotnetGreatAgain` +- 邮箱:`admin@iam.local` +- 角色:Administrator(超级管理员) +- 邮箱已确认:是 +- 锁定状态:未锁定 + +**代码位置:** `src/Services/MigrationService/Worker.cs` - `SeedInitialDataAsync` 方法 + +## OAuth 标准作用域 + +系统自动创建以下符合OpenID Connect标准的作用域: + +| 作用域名称 | 显示名称 | 描述 | 是否必需 | +|-----------|---------|------|---------| +| openid | OpenID | OpenID Connect身份认证 | ✓ | +| profile | Profile | 用户基本信息 | - | +| email | Email | 用户邮箱地址 | - | +| address | Address | 用户地址信息 | - | +| phone | Phone | 用户电话号码 | - | +| offline_access | Offline Access | 离线访问权限(刷新令牌) | - | + +**代码位置:** `src/Services/MigrationService/Worker.cs` - `SeedOAuthDataAsync` 方法 + +## 默认 OAuth 客户端 + +### FrontClient - 前端应用客户端 + +用于单页应用(SPA)和Web前端。 + +**配置:** +- 客户端ID:`FrontClient` +- 显示名称:前端客户端 +- 客户端类型:公共客户端(public) +- 应用类型:SPA +- 是否需要PKCE:是 +- 同意类型:隐式(implicit) + +**授权流程:** +- 授权码流程(Authorization Code) +- 刷新令牌流程(Refresh Token) + +**允许的作用域:** +- openid +- profile +- email +- offline_access + +**重定向URI:** +- `http://localhost:4200` +- `https://localhost:4200` +- `http://localhost:4201` +- `https://localhost:4201` + +**退出后重定向URI:** +- 同重定向URI + +**使用场景:** +- IAM管理前端(端口4200) +- 示例前端应用(端口4201) +- 其他基于浏览器的前端应用 + +### ApiClient - 后端API客户端 + +用于后端服务和API之间的机器对机器通信。 + +**配置:** +- 客户端ID:`ApiClient` +- 客户端密钥:`ApiClient_Secret_2025`(已哈希存储) +- 显示名称:API客户端 +- 客户端类型:机密客户端(confidential) +- 应用类型:Web +- 是否需要PKCE:否 +- 同意类型:隐式(implicit) + +**授权流程:** +- 客户端凭证流程(Client Credentials) + +**允许的作用域:** +- openid + +**使用场景:** +- 示例后端API(端口7000) +- 其他后端服务 +- 微服务间通信 + +**代码位置:** `src/Services/MigrationService/Worker.cs` - `SeedOAuthDataAsync` 方法 + +## 数据种子执行流程 + +1. **检查数据是否已存在** + - 系统会检查每个数据项是否已存在 + - 如果存在,跳过创建 + - 这确保多次运行迁移服务不会重复创建数据 + +2. **创建顺序** + 1. 管理员角色 + 2. 管理员用户 + 3. 用户角色关联 + 4. OAuth作用域 + 5. OAuth客户端 + 6. 客户端作用域关联 + +3. **密码处理** + - 所有密码使用 `PasswordHasherService` 进行哈希 + - 采用安全的哈希算法 + - 密码不以明文存储 + +## 修改默认配置 + +### 修改管理员密码 + +**方法1:启动后在管理后台修改** +1. 使用默认密码登录 +2. 导航到用户管理 +3. 修改admin用户的密码 + +**方法2:修改种子数据代码** +编辑 `src/Services/MigrationService/Worker.cs`: +```csharp +PasswordHash = passwordHasher.HashPassword("你的新密码"), +``` + +### 修改客户端配置 + +**添加新的重定向URI:** +编辑 `SeedOAuthDataAsync` 方法中的 `RedirectUris` 列表: +```csharp +RedirectUris = new List +{ + "http://localhost:4200", + "https://localhost:4200", + "http://localhost:4201", + "https://localhost:4201", + "https://yourdomain.com", // 添加新URI +} +``` + +**修改客户端密钥:** +```csharp +var apiClientSecret = "你的新密钥"; +``` + +### 添加自定义作用域 + +在 `defaultScopes` 列表中添加: +```csharp +var defaultScopes = new List<(string Name, string DisplayName, string Description, bool Required)> +{ + ("openid", "OpenID", "OpenID Connect身份认证", true), + // ... 其他默认作用域 + ("custom_scope", "自定义作用域", "自定义作用域描述", false), +}; +``` + +### 添加新的默认客户端 + +在 `SeedOAuthDataAsync` 方法末尾添加新客户端的创建代码,参考现有的 `FrontClient` 或 `ApiClient` 实现。 + +## 验证种子数据 + +启动系统后,可以通过以下方式验证: + +### 通过管理后台验证 + +1. 登录管理后台:`http://localhost:4200` +2. 使用默认管理员账号登录 +3. 导航到各个管理页面: + - 用户管理 - 查看admin用户 + - 客户端管理 - 查看FrontClient和ApiClient + - 作用域管理 - 查看默认作用域 + +### 通过数据库验证 + +连接PostgreSQL数据库: +```bash +psql -h localhost -p 15432 -U postgres -d IAM_dev +``` + +查询数据: +```sql +-- 查看用户 +SELECT * FROM "Users" WHERE "UserName" = 'admin'; + +-- 查看客户端 +SELECT * FROM "Clients"; + +-- 查看作用域 +SELECT * FROM "ApiScopes"; + +-- 查看客户端作用域关联 +SELECT * FROM "ClientScopes"; +``` + +## 安全建议 + +1. **立即修改管理员密码** + - 默认密码仅用于开发和测试 + - 生产环境必须使用强密码 + +2. **修改客户端密钥** + - `ApiClient_Secret_2025` 是公开的默认密钥 + - 生产环境必须使用强随机密钥 + +3. **限制重定向URI** + - 仅添加实际使用的URI + - 不要使用通配符 + - 使用HTTPS + +4. **定期轮换密钥** + - 定期更新客户端密钥 + - 保持密钥更新日志 + +5. **启用审计日志** + - 监控管理员操作 + - 跟踪客户端使用情况 + +## 故障排除 + +### 种子数据未创建 + +**可能原因:** +- 数据库连接失败 +- 迁移服务未成功运行 +- 数据已存在(检查逻辑跳过了创建) + +**解决方案:** +1. 查看迁移服务日志 +2. 检查数据库连接字符串 +3. 确认PostgreSQL容器正在运行 +4. 手动清空数据库并重新运行迁移 + +### 无法使用默认账号登录 + +**检查:** +- 密码是否正确(区分大小写) +- 用户名是否为小写 `admin` +- 账号是否被锁定 +- 数据库中是否存在该用户 + +### 客户端配置不正确 + +**解决方案:** +1. 在管理后台查看客户端配置 +2. 验证重定向URI是否正确 +3. 检查允许的授权类型 +4. 确认客户端作用域配置 + +## 相关文档 + +- [快速入门指南](quick-start.md) +- [示例项目文档](../samples/README.md) +- [OAuth实现文档](oauth-implementation.md) diff --git a/docs/quick-start.md b/docs/quick-start.md index 3f7b6a4..0cdbd4f 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -2,37 +2,75 @@ 本指南帮助您快速开始使用IAM系统。 -## 初始管理员账号 +## 使用 .NET Aspire 快速启动(推荐) -系统在首次数据库迁移时会自动创建一个默认管理员账号。 +最简单的方式是使用 .NET Aspire 一键启动所有服务。 + +### 环境要求 + +- .NET 9.0 SDK 或更高版本 +- Docker Desktop(用于数据库和缓存容器) +- Node.js 20+ 和 pnpm(前端开发) + +### 启动所有服务 + +```bash +cd src/AppHost +dotnet run +``` + +Aspire 将自动启动: +- PostgreSQL 数据库容器 +- Redis 缓存容器 +- 数据库迁移服务(自动创建表和种子数据) +- IAM API 服务 (https://localhost:7070) +- IAM 管理前端 (http://localhost:4200) +- 示例 API 服务 (https://localhost:7000) +- 示例前端应用 (http://localhost:4201) + +Aspire Dashboard 将在浏览器中自动打开,显示所有服务的状态。 + +## 初始配置 + +系统在首次启动时会自动创建以下默认配置: + +### 管理员账号 -**管理员凭据:** - 用户名:`admin` - 密码:`MakeDotnetGreatAgain` - 邮箱:`admin@iam.local` - 角色:Administrator -## 启动系统 +### 默认 OAuth 作用域 -### 1. 运行数据库迁移 +- `openid` - OpenID Connect身份认证(必需) +- `profile` - 用户基本信息 +- `email` - 用户邮箱地址 +- `address` - 用户地址信息 +- `phone` - 用户电话号码 +- `offline_access` - 离线访问权限(刷新令牌) -首次启动前,需要运行数据库迁移: +### 默认 OAuth 客户端 -```bash -cd src/AppHost -dotnet run -``` +#### FrontClient(前端应用客户端) +- 客户端ID:`FrontClient` +- 类型:公共客户端(SPA) +- 授权流程:授权码 + PKCE +- 支持的重定向URI:localhost:4200, localhost:4201 + +#### ApiClient(后端API客户端) +- 客户端ID:`ApiClient` +- 客户端密钥:`ApiClient_Secret_2025` +- 类型:机密客户端 +- 授权流程:客户端凭证流程 -这将: -- 创建数据库架构 -- 运行所有迁移 -- 自动创建管理员账号 +## 访问系统 -### 2. 访问管理门户 +### 访问管理门户 -浏览器访问:`https://localhost:7001` +浏览器访问:`http://localhost:4200` -### 3. 登录 +### 登录管理门户 1. 点击"登录"按钮 2. 输入凭据: @@ -40,9 +78,17 @@ dotnet run - 密码:`MakeDotnetGreatAgain` 3. 点击"登录" +登录成功后,您将看到管理仪表板。 + +### 访问示例应用 + +浏览器访问:`http://localhost:4201` + +这是一个完整的示例前端应用,展示如何集成IAM进行身份认证。 + ## 管理功能 -登录后,您可以: +登录后,您可以在管理门户中: ### 用户管理 - 创建新用户 @@ -51,11 +97,18 @@ dotnet run - 锁定/解锁账户 ### 客户端管理 -- 注册OAuth客户端 +- 查看默认客户端(FrontClient, ApiClient) +- 注册新的OAuth客户端 - 配置重定向URI - 设置允许的作用域 - 管理客户端密钥 +### 作用域管理 +- 查看默认作用域 +- 创建自定义作用域 +- 配置作用域声明 +- 设置作用域权限 + ### API资源管理 - 定义API资源 - 配置资源作用域 @@ -66,51 +119,102 @@ dotnet run - 分配权限 - 管理角色成员 -## 集成测试 +## 测试集成 ### 使用示例项目 -系统提供了两个示例项目用于测试IAM集成: +Aspire 已经自动启动了示例项目,您可以直接测试: + +#### 测试前端集成 + +1. 访问示例前端:`http://localhost:4201` +2. 点击"登录"按钮 +3. 您将被重定向到IAM登录页面 +4. 输入管理员凭据登录 +5. 登录成功后返回示例应用 +6. 查看用户信息和受保护页面 + +#### 测试后端API + +示例API已在 `https://localhost:7000` 运行。 -#### 后端示例(ASP.NET Core) +测试端点: +- 公开端点:`GET https://localhost:7000/api/public` +- 受保护端点:`GET https://localhost:7000/api/protected`(需要令牌) + +### 完整的认证流程测试 + +1. 在示例前端 (4201) 登录 +2. 登录成功后获取访问令牌 +3. 点击"调用受保护API"按钮 +4. 应用会使用访问令牌调用示例API (7000) +5. 查看返回的用户信息 + +这展示了完整的OAuth 2.0授权码流程 + PKCE。 + +## 手动启动(高级选项) + +如果不使用Aspire,也可以手动启动各个服务: + +### 启动数据库 + +确保PostgreSQL和Redis正在运行。 + +### 运行数据库迁移 ```bash -cd samples/backend-dotnet +cd src/Services/MigrationService +dotnet run +``` + +### 启动IAM API + +```bash +cd src/Services/ApiService dotnet run ``` -访问:`https://localhost:5001/swagger` +API将在 `https://localhost:7070` 启动。 -#### 前端示例(Angular) +### 启动IAM前端 ```bash -cd samples/frontend-angular -npm install -npm start +cd src/ClientApp/WebApp +pnpm install +pnpm start ``` -访问:`http://localhost:4200` +前端将在 `http://localhost:4200` 启动。 -### 配置测试客户端 +### 启动示例后端 -在管理门户中配置Angular示例的客户端: +```bash +cd samples/backend-dotnet +dotnet run +``` -1. 导航到 **客户端** → **创建新客户端** -2. 配置: - - 客户端ID:`sample-angular-client` - - 应用类型:SPA(单页应用) - - 授权类型:Authorization Code with PKCE - - 重定向URI:`http://localhost:4200` - - 允许的作用域:`openid profile email sample-api` +将在 `https://localhost:7000` 启动。 -### 测试认证流程 +### 启动示例前端 -1. 启动IAM服务器 -2. 启动Angular示例应用 -3. 在浏览器中访问 `http://localhost:4200` -4. 点击"Login"按钮 -5. 使用管理员账号登录 -6. 验证成功登录并获取令牌 +```bash +cd samples/frontend-angular +pnpm install +pnpm start +``` + +将在 `http://localhost:4201` 启动。 + +## 服务端口总览 + +| 服务 | 端口 | 说明 | +|------|------|------| +| IAM API | 7070 | 身份认证和授权服务 | +| IAM 管理前端 | 4200 | 管理后台界面 | +| 示例 API | 7000 | 示例后端服务 | +| 示例前端 | 4201 | 示例前端应用 | +| PostgreSQL | 15432 | 数据库 | +| Redis | 16379 | 缓存 | ## 安全注意事项 @@ -147,10 +251,17 @@ npm start ### Q: 示例应用无法连接到IAM? **检查:** -- IAM服务器正在运行 -- URL配置正确(默认:https://localhost:7001) +- IAM服务器正在运行(端口7070) +- URL配置正确:`https://localhost:7070` - CORS配置允许示例应用的源 -- 客户端已在IAM中注册 +- 默认客户端(FrontClient, ApiClient)已自动创建 + +### Q: 端口被占用怎么办? + +**解决方案:** +- 检查并停止占用端口的服务 +- 或修改 `src/AppHost/Program.cs` 中的端口配置 +- 同时需要更新示例项目的配置文件 ### Q: 如何创建新用户? diff --git a/samples/README.md b/samples/README.md index 9b20684..7b4c476 100644 --- a/samples/README.md +++ b/samples/README.md @@ -2,6 +2,19 @@ 本目录包含演示如何对接IAM(身份与访问管理)系统的示例项目。 +所有应用现在通过 .NET Aspire 统一启动和管理,简化了开发流程。 + +## 服务端口配置 + +所有服务使用以下固定端口: + +| 服务 | 端口 | 说明 | +|------|------|------| +| IAM 后端 API | 7070 | 身份认证和授权服务 | +| IAM 前端管理平台 | 4200 | 管理后台界面 | +| 示例后端 API | 7000 | 示例API服务 | +| 示例前端应用 | 4201 | 示例前端界面 | + ## 可用示例 ### 1. 后端示例 (ASP.NET Core) @@ -14,7 +27,8 @@ ASP.NET Core Web API示例,演示: - 为SPA配置CORS - Swagger UI集成,支持JWT测试 -**使用的客户端**: `ApiTest`(已在IAM后台创建) +**服务端口**: `https://localhost:7000` +**使用的客户端**: `ApiClient`(自动创建的默认客户端) [查看后端示例文档](backend-dotnet/README.md) @@ -28,7 +42,8 @@ Angular应用示例,演示: - 受保护路由 - 调用API的HTTP拦截器 -**使用的客户端**: `FrontTest`(已在IAM后台创建) +**服务端口**: `http://localhost:4201` +**使用的客户端**: `FrontClient`(自动创建的默认客户端) [查看前端示例文档](frontend-angular/README.md) @@ -36,116 +51,182 @@ Angular应用示例,演示: ### 前置要求 -1. **IAM服务器运行中** - - 默认URL: `https://localhost:7001` - - 管理员凭据: `admin` / `MakeDotnetGreatAgain` - -2. **开发工具** - - .NET 9 SDK(用于后端示例) - - Node.js 20+ 和 npm(用于前端示例) - -### 步骤 1: 验证IAM配置 - -IAM后台已经配置了两个客户端用于示例项目: - -#### ApiTest 客户端(后端API) -- **客户端ID**: `ApiTest` -- **用途**: 后端API资源服务器 -- **类型**: 资源服务器/API -- **验证**: 登录IAM管理后台,导航到"应用管理",确认客户端存在 +1. **开发工具** + - .NET 9 SDK + - Node.js 20+ 和 pnpm + - Docker(用于运行PostgreSQL和Redis容器) -#### FrontTest 客户端(前端应用) -- **客户端ID**: `FrontTest` -- **用途**: 前端单页应用 -- **类型**: 公共客户端/SPA -- **授权类型**: 授权码 + PKCE -- **重定向URI**: 应包含 `http://localhost:4200` -- **验证**: 登录IAM管理后台,导航到"应用管理",确认客户端存在并配置正确 +### 步骤 1: 使用 Aspire 启动所有服务 -如果客户端不存在,请参考各示例的README中的配置说明进行创建。 - -### 步骤 2: 运行后端示例 +从项目根目录运行: ```bash -cd samples/backend-dotnet +cd src/AppHost dotnet run ``` -API将在 `https://localhost:5001` 启动 - -**测试端点:** -- 公开端点: `GET https://localhost:5001/api/public` -- 受保护端点: `GET https://localhost:5001/api/protected` (需要认证) -- Swagger UI: `https://localhost:5001/swagger` - -### 步骤 3: 运行前端示例 - -```bash -cd samples/frontend-angular -npm install -npm start -``` - -应用将在 `http://localhost:4200` 启动 +这将自动启动: +- PostgreSQL 数据库 +- Redis 缓存 +- IAM 数据库迁移服务 +- IAM API 服务 (https://localhost:7070) +- IAM 管理前端 (http://localhost:4200) +- 示例 API 服务 (https://localhost:7000) +- 示例前端应用 (http://localhost:4201) + +Aspire Dashboard 将在浏览器中自动打开,您可以在其中监控所有服务的状态。 + +### 步骤 2: 访问管理后台配置(可选) + +系统已自动创建默认配置,但您可以登录管理后台查看: + +1. 访问 IAM 管理后台: `http://localhost:4200` +2. 使用默认管理员凭据登录: + - 用户名: `admin` + - 密码: `MakeDotnetGreatAgain` + +### 步骤 3: 默认客户端和作用域 + +系统已自动创建以下默认配置: + +#### 默认作用域 (Scopes) +- `openid` - OpenID Connect身份认证(必需) +- `profile` - 用户基本信息 +- `email` - 用户邮箱地址 +- `address` - 用户地址信息 +- `phone` - 用户电话号码 +- `offline_access` - 离线访问权限(刷新令牌) + +#### FrontClient 客户端(前端应用) +- **客户端ID**: `FrontClient` +- **客户端类型**: 公共客户端(SPA) +- **授权流程**: 授权码 + PKCE +- **允许的作用域**: openid, profile, email, offline_access +- **重定向URI**: + - `http://localhost:4200` + - `https://localhost:4200` + - `http://localhost:4201` + - `https://localhost:4201` + +#### ApiClient 客户端(后端API) +- **客户端ID**: `ApiClient` +- **客户端密钥**: `ApiClient_Secret_2025` +- **客户端类型**: 机密客户端 +- **授权流程**: 客户端凭证流程 +- **允许的作用域**: openid ### 步骤 4: 测试集成 -1. 在浏览器中打开 `http://localhost:4200` +1. 在浏览器中打开示例前端: `http://localhost:4201` 2. 点击"登录"按钮 -3. 您将被重定向到IAM登录页面 +3. 您将被重定向到IAM登录页面 (端口 7070) 4. 输入凭据: `admin` / `MakeDotnetGreatAgain` -5. 成功登录后,您将被重定向回Angular应用 +5. 成功登录后,您将被重定向回示例应用 6. 导航到"受保护页面"查看用户信息 -7. 点击"调用受保护API"测试API集成 +7. 点击"调用受保护API"测试与后端API (端口 7000) 的集成 ## 架构概览 ``` -┌─────────────────┐ -│ Angular应用 │ -│ (端口 4200) │ -│ │ -│ - OIDC客户端 │ -│ - UI/UX │ -└────────┬────────┘ - │ - │ 1. 认证请求 - │ 3. 令牌请求 - ▼ -┌─────────────────┐ -│ IAM服务器 │ -│ (端口 7001) │ -│ │ -│ - 认证服务器 │ -│ - 用户存储 │ -│ - 令牌颁发 │ -└────────┬────────┘ - │ - │ 2. 用户登录 - │ - │ - │ 4. 访问令牌 - ▼ -┌─────────────────┐ -│ API服务器 │ -│ (端口 5001) │ -│ │ -│ - 令牌验证 │ -│ - 资源保护 │ -└─────────────────┘ +┌─────────────────────┐ ┌─────────────────────┐ +│ IAM管理前端 │ │ 示例前端应用 │ +│ (端口 4200) │ │ (端口 4201) │ +│ │ │ │ +│ - 管理界面 │ │ - OIDC客户端 │ +│ - 配置管理 │ │ - UI/UX │ +└──────────┬──────────┘ └──────────┬──────────┘ + │ │ + │ 管理API调用 │ 1. 认证请求 + │ │ 3. 令牌请求 + ▼ ▼ +┌──────────────────────────────────────────────────┐ +│ IAM 服务器 (端口 7070) │ +│ │ +│ - OAuth/OIDC认证服务器 │ +│ - 用户存储和管理 │ +│ - 令牌颁发和验证 │ +│ - 作用域和权限管理 │ +└────────────────────┬─────────────────────────────┘ + │ + │ 2. 用户登录 + │ 4. 访问令牌 + ▼ + ┌─────────────────┐ + │ 示例API服务器 │ + │ (端口 7000) │ + │ │ + │ - JWT令牌验证 │ + │ - 资源保护 │ + │ - 业务逻辑 │ + └─────────────────┘ ``` -## 认证流程 - -1. **用户发起登录** - 从Angular应用开始 -2. **Angular重定向** - 到IAM授权端点 -3. **用户认证** - 在IAM登录页面 -4. **IAM重定向回** - Angular应用,携带授权码 -5. **Angular交换令牌** - 使用授权码换取访问令牌、ID令牌和刷新令牌 -6. **Angular存储令牌** - 安全存储 -7. **API调用包含令牌** - 在Authorization头中 -8. **API验证令牌** - 使用IAM的公钥验证 -9. **API返回资源** - 受保护的数据 +## OAuth 2.0 / OIDC 认证授权流程 + +### 授权码流程 + PKCE (用于前端SPA) + +1. **用户发起登录** + - 用户在示例前端 (4201) 点击登录按钮 + +2. **生成PKCE挑战** + - 前端生成 code_verifier (随机字符串) + - 计算 code_challenge = BASE64URL(SHA256(code_verifier)) + +3. **重定向到授权端点** + - 前端重定向到: `https://localhost:7070/connect/authorize` + - 参数包括: client_id, redirect_uri, scope, response_type=code, code_challenge + +4. **用户认证** + - IAM显示登录页面 + - 用户输入用户名和密码 + - IAM验证凭据 + +5. **授权确认(可选)** + - 如果配置需要,显示授权确认页面 + - 用户同意授予权限 + +6. **返回授权码** + - IAM重定向回: `http://localhost:4201?code=xxx` + - 授权码是一次性使用的临时凭证 + +7. **交换令牌** + - 前端发送POST请求到: `https://localhost:7070/connect/token` + - 包含: code, client_id, redirect_uri, code_verifier + - IAM验证授权码和PKCE挑战 + +8. **获取令牌** + - IAM返回: + - access_token (访问令牌) + - id_token (ID令牌,包含用户信息) + - refresh_token (刷新令牌) + +9. **调用受保护API** + - 前端在HTTP请求头中添加: `Authorization: Bearer {access_token}` + - 调用: `https://localhost:7000/api/protected` + +10. **API验证令牌** + - 示例API从IAM的JWKS端点获取公钥 + - 验证JWT签名、过期时间、颁发者等 + - 验证通过后返回受保护资源 + +11. **令牌刷新** + - 当access_token过期时 + - 前端使用refresh_token请求新的access_token + - 无需用户重新登录 + +### 客户端凭证流程 (用于后端服务) + +1. **服务认证** + - 后端服务使用client_id和client_secret + - 发送POST请求到: `https://localhost:7070/connect/token` + - grant_type=client_credentials + +2. **获取令牌** + - IAM验证客户端凭证 + - 返回access_token + +3. **服务间调用** + - 使用access_token调用其他受保护的API ## 令牌类型 @@ -204,6 +285,14 @@ npm start ## 开发技巧 +### 使用 Aspire Dashboard 监控 + +Aspire Dashboard 提供了所有服务的实时监控: +- 查看服务状态和日志 +- 监控HTTP请求和响应 +- 查看数据库和缓存连接 +- 跟踪分布式追踪信息 + ### 调试OIDC流程 在Angular应用中启用调试日志: @@ -212,21 +301,27 @@ npm start logLevel: LogLevel.Debug ``` +在浏览器开发者工具中查看: +- Network标签:查看OAuth请求和响应 +- Application/Storage标签:查看存储的令牌 +- Console标签:查看OIDC客户端日志 + ### 使用不同用户测试 -在IAM中创建额外的测试用户: -1. 导航到 用户 → 创建新用户 -2. 分配适当的角色 -3. 测试不同的授权场景 +在IAM管理后台创建额外的测试用户: +1. 访问 `http://localhost:4200` +2. 导航到 用户 → 创建新用户 +3. 分配适当的角色 +4. 测试不同的授权场景 ### 使用Postman测试API 1. 获取访问令牌: - 在Postman中使用OAuth 2.0 - - 授权URL: `https://localhost:7001/connect/authorize` - - 令牌URL: `https://localhost:7001/connect/token` - - 客户端ID: 您的客户端ID - - 作用域: 所需的作用域 + - 授权URL: `https://localhost:7070/connect/authorize` + - 令牌URL: `https://localhost:7070/connect/token` + - 客户端ID: `FrontClient` + - 作用域: `openid profile email` 2. 在请求中使用令牌: - 添加头: `Authorization: Bearer {access_token}` @@ -236,60 +331,97 @@ logLevel: LogLevel.Debug 使用密码流程直接获取令牌: ```bash -curl -X POST https://localhost:7001/connect/token \ +curl -X POST https://localhost:7070/connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password" \ - -d "client_id=FrontTest" \ + -d "client_id=FrontClient" \ -d "username=admin" \ -d "password=MakeDotnetGreatAgain" \ - -d "scope=openid profile email ApiTest" + -d "scope=openid profile email" ``` ## 常见问题和解决方案 +### 问题: 服务无法启动 +**解决方案**: +- 确保Docker正在运行 +- 检查端口是否被占用 (7070, 7000, 4200, 4201) +- 查看Aspire Dashboard中的服务日志 + +### 问题: 数据库连接失败 +**解决方案**: +- 确保PostgreSQL容器已启动 +- 在Aspire Dashboard中检查数据库服务状态 +- 等待迁移服务完成 + ### 问题: CORS错误 -**解决方案**: 确保客户端的允许CORS源包括Angular应用URL +**解决方案**: +- 确保后端API的CORS配置包含前端URL +- 检查端口号是否正确 (示例前端应该是4201) ### 问题: 重定向URI不匹配 -**解决方案**: 验证IAM中的重定向URI与应用URL完全匹配(检查尾部斜杠) +**解决方案**: +- 验证使用的端口号正确 (IAM: 7070, 示例前端: 4201) +- 检查是否使用了正确的协议 (http vs https) +- 默认配置已包含常用的重定向URI ### 问题: 令牌验证失败 -**解决方案**: 检查API的authority配置是否匹配IAM的颁发者URL +**解决方案**: +- 确认示例API的Authority配置为 `https://localhost:7070` +- 检查IAM的JWKS端点是否可访问: `https://localhost:7070/.well-known/jwks` +- 验证令牌未过期 ### 问题: 无限重定向循环 -**解决方案**: 清除浏览器存储并检查OIDC配置 +**解决方案**: +- 清除浏览器存储 (localStorage和sessionStorage) +- 检查OIDC配置中的redirectUrl和postLogoutRedirectUri +- 验证客户端ID和作用域配置正确 ### 问题: SSL证书错误 -**解决方案**: 对于开发环境,接受自签名证书或配置适当的证书 +**解决方案**: +- 对于开发环境,在浏览器中接受自签名证书 +- 访问 `https://localhost:7070` 和 `https://localhost:7000` 并接受证书警告 +- 或配置开发证书信任 ### 问题: 后端API无法验证令牌 **解决方案**: -- 确认Audience配置与客户端ID匹配 -- 检查IAM的JWKS端点是否可访问 -- 验证令牌未过期 +- 确认Audience配置与预期一致 +- 检查Authority配置指向正确的IAM服务器: `https://localhost:7070` +- 验证IAM的discovery端点可访问: `https://localhost:7070/.well-known/openid-configuration` ## 配置检查清单 -### IAM服务器配置 -- [ ] IAM服务器正在运行 -- [ ] 管理员账号可以登录 -- [ ] ApiTest客户端已创建 -- [ ] FrontTest客户端已创建 -- [ ] FrontTest的重定向URI配置正确 -- [ ] 作用域配置正确 - -### 后端示例配置 -- [ ] .NET SDK已安装 -- [ ] appsettings.json中的Authority配置正确 -- [ ] Audience设置为"ApiTest" -- [ ] CORS允许的源包括前端URL - -### 前端示例配置 -- [ ] Node.js和npm已安装 -- [ ] app.config.ts中的authority配置正确 -- [ ] clientId设置为"FrontTest" -- [ ] scope包括所需的作用域 -- [ ] secureRoutes配置指向API URL +### Aspire 运行环境 +- [ ] Docker Desktop 正在运行 +- [ ] .NET 9 SDK 已安装 +- [ ] Node.js 20+ 和 pnpm 已安装 +- [ ] 端口 7070, 7000, 4200, 4201 未被占用 + +### IAM 服务配置(自动完成) +- [x] PostgreSQL 数据库容器已启动 +- [x] Redis 缓存容器已启动 +- [x] 数据库迁移已完成 +- [x] 管理员账号已创建 (admin / MakeDotnetGreatAgain) +- [x] 默认作用域已创建 (openid, profile, email等) +- [x] FrontClient 客户端已创建 +- [x] ApiClient 客户端已创建 +- [x] IAM API 服务运行在 https://localhost:7070 +- [x] IAM 管理前端运行在 http://localhost:4200 + +### 示例项目配置(自动完成) +- [x] 示例 API 服务运行在 https://localhost:7000 +- [x] 示例前端应用运行在 http://localhost:4201 +- [x] 示例 API 的 Authority 指向 https://localhost:7070 +- [x] 示例前端的 authority 指向 https://localhost:7070 +- [x] 示例前端的 secureRoutes 指向 https://localhost:7000 +- [x] CORS 配置正确 + +### 验证步骤 +- [ ] 访问 IAM 管理后台并成功登录 +- [ ] 在应用管理中查看 FrontClient 和 ApiClient +- [ ] 访问示例前端应用 +- [ ] 在示例应用中完成登录流程 +- [ ] 成功调用示例 API 的受保护端点 ## 额外资源 @@ -297,7 +429,7 @@ curl -X POST https://localhost:7001/connect/token \ - [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749) - [OpenID Connect规范](https://openid.net/specs/openid-connect-core-1_0.html) - [PKCE RFC](https://tools.ietf.org/html/rfc7636) -- [客户端集成指南](../../docs/CLIENT-INTEGRATION-GUIDE.md) +- [.NET Aspire文档](https://learn.microsoft.com/dotnet/aspire/) ## 支持 diff --git a/samples/backend-dotnet/Properties/launchSettings.json b/samples/backend-dotnet/Properties/launchSettings.json index 5922f6c..bc7d7f4 100644 --- a/samples/backend-dotnet/Properties/launchSettings.json +++ b/samples/backend-dotnet/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:7070" + "applicationUrl": "https://localhost:7000" } } } \ No newline at end of file diff --git a/samples/backend-dotnet/appsettings.json b/samples/backend-dotnet/appsettings.json index 3b9f243..0b915e2 100644 --- a/samples/backend-dotnet/appsettings.json +++ b/samples/backend-dotnet/appsettings.json @@ -7,15 +7,15 @@ }, "AllowedHosts": "*", "Authentication": { - "Authority": "https://localhost:7000", + "Authority": "https://localhost:7070", "Audience": "ApiTest", "ClientId": "ApiTest", "ValidateAudience": true }, "Cors": { "AllowedOrigins": [ - "http://localhost:4200", - "https://localhost:4200" + "http://localhost:4201", + "https://localhost:4201" ] } } diff --git a/samples/frontend-angular/src/app/app.config.ts b/samples/frontend-angular/src/app/app.config.ts index 33b35fc..4d2d6b5 100644 --- a/samples/frontend-angular/src/app/app.config.ts +++ b/samples/frontend-angular/src/app/app.config.ts @@ -16,7 +16,7 @@ export const appConfig: ApplicationConfig = { importProvidersFrom( AuthModule.forRoot({ config: { - authority: 'https://localhost:7000', + authority: 'https://localhost:7070', redirectUrl: window.location.origin, postLogoutRedirectUri: window.location.origin, clientId: 'FrontTest', @@ -25,7 +25,7 @@ export const appConfig: ApplicationConfig = { silentRenew: true, useRefreshToken: true, logLevel: LogLevel.Debug, - secureRoutes: ['https://localhost:5001/api'], + secureRoutes: ['https://localhost:7000/api'], customParamsAuthRequest: { } } diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs index fa97cb9..e0d7048 100644 --- a/src/AppHost/Program.cs +++ b/src/AppHost/Program.cs @@ -67,8 +67,11 @@ devPassword.WithParentRelationship(database!); var migration = builder.AddProject("MigrationService"); -var apiService = builder.AddProject("ApiService").WaitForCompletion(migration); -var sampleApi = builder.AddProject("SampleApi"); +var apiService = builder.AddProject("ApiService") + .WithHttpsEndpoint(port: 7070, name: "https") + .WaitForCompletion(migration); +var sampleApi = builder.AddProject("SampleApi") + .WithHttpsEndpoint(port: 7000, name: "https"); builder .AddNpmApp("SampleApp", "../../samples/frontend-angular") diff --git a/src/Services/ApiService/Properties/launchSettings.json b/src/Services/ApiService/Properties/launchSettings.json index 5427247..83218f9 100644 --- a/src/Services/ApiService/Properties/launchSettings.json +++ b/src/Services/ApiService/Properties/launchSettings.json @@ -7,7 +7,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:7000" + "applicationUrl": "https://localhost:7070" } } } \ No newline at end of file diff --git a/src/Services/MigrationService/Worker.cs b/src/Services/MigrationService/Worker.cs index 0c4cd1c..58375ac 100644 --- a/src/Services/MigrationService/Worker.cs +++ b/src/Services/MigrationService/Worker.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Entity.AccessMod; using Entity.IdentityMod; using Microsoft.EntityFrameworkCore; using Share.Services; @@ -58,6 +59,7 @@ await strategy.ExecuteAsync(async () => if (dbContext is DefaultDbContext defaultContext) { await SeedInitialDataAsync(defaultContext, cancellationToken); + await SeedOAuthDataAsync(defaultContext, cancellationToken); } }); } @@ -134,4 +136,160 @@ CancellationToken cancellationToken await dbContext.SaveChangesAsync(cancellationToken); } } + + /// + /// Seed OAuth/OIDC initial data including default clients and scopes + /// + private static async Task SeedOAuthDataAsync( + DefaultDbContext dbContext, + CancellationToken cancellationToken + ) + { + var passwordHasher = new PasswordHasherService(); + + // Create default scopes + var defaultScopes = new List<(string Name, string DisplayName, string Description, bool Required)> + { + ("openid", "OpenID", "OpenID Connect身份认证", true), + ("profile", "Profile", "用户基本信息", false), + ("email", "Email", "用户邮箱地址", false), + ("address", "Address", "用户地址信息", false), + ("phone", "Phone", "用户电话号码", false), + ("offline_access", "Offline Access", "离线访问权限(刷新令牌)", false) + }; + + foreach (var (name, displayName, description, required) in defaultScopes) + { + var scopeExists = await dbContext.Set().AnyAsync( + s => s.Name == name, + cancellationToken + ); + + if (!scopeExists) + { + var scope = new ApiScope + { + Name = name, + DisplayName = displayName, + Description = description, + Required = required, + Emphasize = required + }; + + dbContext.Set().Add(scope); + } + } + + await dbContext.SaveChangesAsync(cancellationToken); + + // Get all scopes for client assignment + var openidScope = await dbContext.Set().FirstAsync(s => s.Name == "openid", cancellationToken); + var profileScope = await dbContext.Set().FirstAsync(s => s.Name == "profile", cancellationToken); + var emailScope = await dbContext.Set().FirstAsync(s => s.Name == "email", cancellationToken); + var offlineAccessScope = await dbContext.Set().FirstAsync(s => s.Name == "offline_access", cancellationToken); + + // Create FrontClient for frontend applications + var frontClientId = "FrontClient"; + var frontClientExists = await dbContext.Set().AnyAsync( + c => c.ClientId == frontClientId, + cancellationToken + ); + + if (!frontClientExists) + { + var frontClient = new Client + { + ClientId = frontClientId, + DisplayName = "前端客户端", + Description = "默认的前端单页应用客户端,支持OIDC授权码流程+PKCE", + Type = "public", + ApplicationType = "spa", + RequirePkce = true, + ConsentType = "implicit", + RedirectUris = new List + { + "http://localhost:4200", + "https://localhost:4200", + "http://localhost:4201", + "https://localhost:4201" + }, + PostLogoutRedirectUris = new List + { + "http://localhost:4200", + "https://localhost:4200", + "http://localhost:4201", + "https://localhost:4201" + }, + Permissions = System.Text.Json.JsonSerializer.Serialize(new[] + { + "ept:authorization", + "ept:logout", + "ept:token", + "gt:authorization_code", + "gt:refresh_token", + "rst:code", + "rst:id_token", + "rst:id_token token", + "rst:token" + }) + }; + + dbContext.Set().Add(frontClient); + await dbContext.SaveChangesAsync(cancellationToken); + + // Assign scopes to FrontClient + var frontClientScopes = new[] + { + new ClientScope { ClientId = frontClient.Id, ScopeId = openidScope.Id }, + new ClientScope { ClientId = frontClient.Id, ScopeId = profileScope.Id }, + new ClientScope { ClientId = frontClient.Id, ScopeId = emailScope.Id }, + new ClientScope { ClientId = frontClient.Id, ScopeId = offlineAccessScope.Id } + }; + + dbContext.Set().AddRange(frontClientScopes); + } + + // Create ApiClient for backend API services + var apiClientId = "ApiClient"; + var apiClientExists = await dbContext.Set().AnyAsync( + c => c.ClientId == apiClientId, + cancellationToken + ); + + if (!apiClientExists) + { + var apiClientSecret = "ApiClient_Secret_2025"; + var apiClient = new Client + { + ClientId = apiClientId, + ClientSecret = passwordHasher.HashPassword(apiClientSecret), + DisplayName = "API客户端", + Description = "默认的后端API服务客户端,支持客户端凭证流程", + Type = "confidential", + ApplicationType = "web", + RequirePkce = false, + ConsentType = "implicit", + Permissions = System.Text.Json.JsonSerializer.Serialize(new[] + { + "ept:token", + "ept:introspection", + "ept:revocation", + "gt:client_credentials" + }) + }; + + dbContext.Set().Add(apiClient); + await dbContext.SaveChangesAsync(cancellationToken); + + // Assign scopes to ApiClient + var apiClientScopes = new[] + { + new ClientScope { ClientId = apiClient.Id, ScopeId = openidScope.Id } + }; + + dbContext.Set().AddRange(apiClientScopes); + } + + await dbContext.SaveChangesAsync(cancellationToken); + } }