Dpz.Core.Auth
Dpz.Core.Auth 是 Dpz.Core 的统一认证中心,负责 OpenID Connect / OAuth2 授权、Cookie 登录、Passkey / WebAuthn、双因素认证、客户端管理、授权记录、令牌管理、应用访问授权和用户个人中心骨架。
当前版本采用 ASP.NET Core MVC + OpenIddict + MongoDB + Vue 3 + Quasar + Vite + TypeScript。ASP.NET Core 永远是唯一 Web 服务器;前端开发模式只做 Vite watch build,不启动 Node Web Server。
架构流程
flowchart TD
User["用户浏览器"] --> Client["业务客户端应用"]
Client -->|跳转 /connect/authorize| Auth["Dpz.Core.Auth"]
Auth --> LoginCheck{"是否已登录"}
LoginCheck -->|否| SignIn["匿名入口 public<br/>密码 / TOTP / Passkey 登录"]
SignIn --> LoginApi["JSON API<br/>/api/auth/sign-in<br/>/api/auth/webauthn/*"]
LoginApi --> Cookie["写入认证 Cookie"]
Cookie --> Auth
LoginCheck -->|是| ConsentCheck{"是否需要授权确认"}
ConsentCheck -->|需要| Consent["身份流程入口 identity<br/>授权确认 / 拒绝"]
Consent --> ConsentApi["JSON API<br/>授权确认结果"]
ConsentApi --> IssueCode["OpenIddict 生成授权码"]
ConsentCheck -->|不需要| IssueCode
IssueCode --> Client
Client -->|/connect/token 换取 Token| Auth
Auth --> Token["Access Token / Refresh Token / Id Token"]
flowchart LR
Razor["MVC Shell 视图<br/>_Shell.cshtml"] --> AssetService["AuthAssetService"]
AssetService --> Manifest["wwwroot/auth-assets-manifest.json"]
Manifest --> AssetPath{"运行环境"}
AssetPath -->|生产| CDN["AuthAssets:AssetsHost + hash 文件"]
AssetPath -->|开发| Local["ASP.NET Core 静态文件<br/>wwwroot/auth-assets"]
CDN --> Browser["浏览器加载 Vue/Quasar 入口"]
Local --> Browser
flowchart TD
Admin["/admin 管理宿主视图"] --> Spa["admin 入口 SPA<br/>Hash 路由"]
Spa --> Apps["客户端应用<br/>/api/admin/applications"]
Spa --> Authz["授权记录<br/>/api/admin/authorizations"]
Spa --> Tokens["令牌管理<br/>/api/admin/tokens"]
Spa --> Requests["访问申请<br/>/api/admin/access-requests"]
Spa --> Grants["授权白名单<br/>/api/admin/grants"]
Apps --> Result["ResponseResult<PagedListWarp<T>>"]
Authz --> Result
Tokens --> Result
Requests --> Result
Grants --> Result
项目边界
- OpenIddict / Cookie / Antiforgery / 权限策略仍由 ASP.NET Core 负责。
- MVC 只保留最小宿主视图、登录边界、OIDC 回调边界和必要的后端流程。
- 前端交互统一通过 JSON API 完成,返回
ResponseResult或ResponseResult<T>。 - unsafe 请求必须携带 Antiforgery header:
RequestVerificationToken。 - 管理端使用独立 MVC 宿主视图
/admin,前端使用#hash 路由。 - 用户个人中心使用独立 MVC 宿主视图
/user,前端使用#hash 路由。 - 后台管理 API 必须统一使用
/api/admin/...REST 风格路由,不再新增 MVC 风格动作接口。 - 用户个人中心 API 必须统一使用
/api/user/...REST 风格路由;认证流程、安全绑定写入流程继续保留在/api/auth/...。 - Controller/API 公开入参和出参不得直接使用
Dpz.Core.Public.Entity实体类型,使用请求 DTO、ViewModel、Response 类型承载边界数据。 - 匿名页、身份流程页、用户中心页使用各自 MVC 宿主视图,引用带 hash 的构建产物。
路由分区:
| 分区 | 入口/前缀 | 说明 |
|---|---|---|
| 匿名认证 | /sign-in、/api/auth/sign-in | 登录、Passkey 登录、认证状态页 |
| 用户中心 | /user、/api/user/... | 个人资料、安全概览、授权应用、内容模块骨架 |
| 身份流程 | /connect/authorize、change-password.html、passkeys.html、security-onboarding.html、two-factor.html、unbind-two-factor.html | 登录后的授权确认、访问拒绝、改密、2FA、Passkey、安全引导页面 |
| 管理后台 | /admin、/api/admin/... | 客户端、授权、令牌、访问申请和白名单管理 |
前端工程
源码位于:
src/Dpz.Core.Auth/ClientApp
四入口:
| 入口 | 文件 | 说明 |
|---|---|---|
public | src/entries/public.ts | 落地页、登录页等匿名页面 |
identity | src/entries/identity.ts | 授权确认、访问拒绝、修改密码、2FA 绑定/解绑、Passkey 管理、登录后安全引导 |
user | src/entries/user.ts | 用户个人中心 SPA,使用 hash 路由 |
admin | src/entries/admin.ts | 管理后台 SPA,使用 hash 路由 |
用户个人中心菜单:
| Hash 路由 | 模块 | 状态 |
|---|---|---|
#/profile | 个人资料 | 已接入账号资料只读骨架 |
#/security | 安全设置 | 已聚合 2FA / Passkey 概览,具体操作跳转 identity 流程页 |
#/authorizations | 我的授权应用 | 已接入当前用户授权分页查询 |
#/sessions | 登录/会话记录 | 空分页骨架 |
#/articles | 我发表的文章 | 空分页骨架 |
#/mumbles | 我发表的碎碎念 | 空分页骨架 |
#/timelines | 我发表的时间轴 | 空分页骨架 |
#/albums | 我的相册 | 空分页骨架 |
常用命令:
cd src/Dpz.Core.Auth/ClientApp
npm install
npm run dev
npm run build
npm run lint
npm run format
npm run format:check
npx vue-tsc --noEmit
说明:
npm run dev执行vite build --watch --mode development,只持续编译前端资源。- 开发时浏览器仍访问 ASP.NET Core Auth 服务,例如
https://localhost:7183/admin。 npm run build会先删除旧的wwwroot/auth-assets,再生成新的 hash 产物和 manifest。wwwroot/auth-assets/**是构建产物,不进入 Git。wwwroot/auth-assets-manifest.json必须进入 Git,它指导后端拼接正确的本地/CDN 资源地址。- Quasar 使用
quasar/dist/quasar.css预编译 CSS,避免 Sass 编译链路和 deprecation 噪音。 - 每个 entry 只 import 自己的业务 CSS,匿名用户不应下载用户页或管理端私有样式。
CSS 样式组织
源码目录:
src/Dpz.Core.Auth/ClientApp/src/styles/
├── public.css ← public entry 样式入口
├── identity.css ← identity entry 样式入口
├── user.css ← user entry 样式入口
├── admin.css ← admin entry 样式入口
├── _base.css ← 公共 typography / body reset
├── _nav.css ← 公共品牌导航
├── _auth-shell.css ← 管理端/登录后 shell 背景
├── landing.css ← 落地页私有样式
└── signin.css ← 登录页私有样式
规则
- 按 entry 拆包。
public.ts只 importpublic.css,identity.ts只 importidentity.css,user.ts只 importuser.css,admin.ts只 importadmin.css。 - 公共 partial 用
_前缀。 例如_base.css、_nav.css;页面私有样式保持普通命名,例如landing.css、signin.css、user.css。 - 不要把所有页面样式重新汇总到单一全局入口。 入口 CSS 只能 import 当前 entry 需要的公共 partial 和私有样式。
- 暗色模式样式紧跟在对应规则后面,用
body.body--dark .selector覆写,不要集中写在一个暗色块里。 - 响应式断点写在所属文件的末尾,用
@media (max-width: 768px) { ... }包裹。 - 类名沿用 BEM 风格:
.block__element--modifier或.block-element(单破折号衔接)。不对齐某一种 strict BEM,但要求:- 有意义的命名空间前缀(如
.landing-、.hero-、.dashboard-、.signin-、.auth-) - 修饰符用双破折号(如
.hero-title--gradient、.hero-btn--primary)
- 有意义的命名空间前缀(如
- !important 只用于覆盖 Quasar 组件内置样式(如
border-radius: 14px !important),必须注释原因。 - 颜色不使用硬编码 hex,优先考虑:
- Quasar 内置颜色类(
text-primary、bg-positive等) - 组件内使用 Quasar 颜色属性
- CSS 文件中使用统一的色值集合(teal
#008080/ indigo#6366f1/ slate 系列#0f172a#475569#94a3b8)
- Quasar 内置颜色类(
- Vite 构建时解析
@import,每个 entry 输出自己的 CSS bundle,manifest 中应存在独立的entries/public.css、entries/identity.css、entries/user.css、entries/admin.css。
注意事项
- 样式文件是纯 CSS(不是 Sass/SCSS),不要引入预处理语法。
- 修改任何
.css后必须还原.vue文件也可能受影响的假设——Vue 组件应优先使用 Quasar 内置 class,其次才是自定义 CSS。 - 公共导航、基础 reset、跨入口背景写入
_*.css;页面私有交互和布局写入对应私有样式文件。 - 构建产物
wwwroot/auth-assets/**不进入 Git,但auth-assets-manifest.json必须提交。
资源加载
AuthAssetService 读取:
wwwroot/auth-assets-manifest.json
manifest 示例:
{
"entries/admin.js": "auth-assets/assets/entries/admin-xxxx.js",
"entries/admin.css": [
"auth-assets/assets/quasar-options-xxxx.css",
"auth-assets/assets/admin-xxxx.css"
],
"entries/public.css": [
"auth-assets/assets/quasar-options-xxxx.css",
"auth-assets/assets/public-xxxx.css"
],
"entries/identity.css": [
"auth-assets/assets/quasar-options-xxxx.css",
"auth-assets/assets/identity-xxxx.css"
],
"entries/user.css": [
"auth-assets/assets/quasar-options-xxxx.css",
"auth-assets/assets/user-xxxx.css"
]
}
生产环境可配置:
{
"AuthAssets": {
"AssetsHost": "https://cdn.example.com"
}
}
当 AssetsHost 存在时,后端会返回 AssetsHost + manifest path;否则返回 ASP.NET Core 静态文件路径。
CSS manifest 值可以是字符串或字符串数组;数组用于同时加载 Quasar 公共 CSS 和 entry 私有 CSS。业务 CSS 必须按 entry 分离,public 入口不应包含 .identity-*、.user-*、.admin-* 等登录后或管理端私有样式。
JSON API 约定
统一返回:
ResponseResult
ResponseResult<T>
分页统一返回:
ResponseResult<PagedListWarp<T>>
管理端已接入的分页端点:
| 模块 | 端点 | 返回类型 |
|---|---|---|
| 客户端应用 | GET /api/admin/applications | ResponseResult<PagedListWarp<AuthApplicationPageItem>> |
| 授权记录 | GET /api/admin/authorizations | ResponseResult<PagedListWarp<AuthAuthorizationPageItem>> |
| 令牌管理 | GET /api/admin/tokens | ResponseResult<PagedListWarp<AuthTokenPageItem>> |
| 访问申请 | GET /api/admin/access-requests | ResponseResult<PagedListWarp<AuthAccessRequestItem>> |
| 授权白名单 | GET /api/admin/grants | ResponseResult<PagedListWarp<AuthAllowedClientItem>> |
用户中心已接入的端点:
| 模块 | 端点 | 返回类型 |
|---|---|---|
| 个人资料 | GET /api/user/profile | ResponseResult<UserCenterProfileResponse> |
| 安全概览 | GET /api/user/security | ResponseResult<UserCenterSecurityResponse> |
| 我的授权应用 | GET /api/user/authorizations | ResponseResult<PagedListWarp<AuthAuthorizationPageItem>> |
| 登录/会话记录 | GET /api/user/sessions | ResponseResult<PagedListWarp<UserCenterSessionItem>> |
| 我发表的文章 | GET /api/user/articles | ResponseResult<PagedListWarp<UserCenterContentItem>> |
| 我发表的碎碎念 | GET /api/user/mumbles | ResponseResult<PagedListWarp<UserCenterContentItem>> |
| 我发表的时间轴 | GET /api/user/timelines | ResponseResult<PagedListWarp<UserCenterContentItem>> |
| 我的相册 | GET /api/user/albums | ResponseResult<PagedListWarp<UserCenterContentItem>> |
用户中心当前是骨架阶段:资料、安全概览和授权应用已接入现有身份域数据;会话、文章、碎碎念、时间轴、相册先返回标准空分页,后续再接入具体业务服务。
管理端写入端点:
| 操作 | 端点 |
|---|---|
| 获取客户端详情 | GET /api/admin/applications/{id} |
| 新建客户端 | POST /api/admin/applications |
| 更新客户端 | PUT /api/admin/applications/{id} |
| 删除客户端 | DELETE /api/admin/applications/{id} |
| 撤销授权 | POST /api/admin/authorizations/revoke |
| 撤销用户令牌 | POST /api/admin/tokens/revoke-by-user |
| 撤销客户端令牌 | POST /api/admin/tokens/revoke-by-client |
| 处理访问申请 | PATCH /api/admin/access-requests/{id} |
| 新增授权白名单 | POST /api/admin/grants |
| 删除授权白名单 | DELETE /api/admin/grants/{id} |
已移除的旧后台管理接口/代码:
- 后端删除
ApplicationController.Index、Upsert、AppDraft、PersistDraft、手写ToPageItem。 - 后端删除
AuthorizationManageController.Index、TokenController.Index、AccessRequestController.Index、GrantController.Index。 - 前端不再调用
/Application/Page、/Application/Create、/Application/Update、/Application/Delete、/AuthorizationManage/Revoke、/Token/RevokeByUser、/Token/RevokeByClient、/AccessRequest/Handle、/Grant/Grant、/Grant/Revoke。 - 后台客户端新建/编辑不再使用 MVC 表单或
FormData字段绑定,统一 JSON DTO + ASP.NET Core 模型验证。
失败时由后端填充 Success=false、Message 和必要错误码。前端不解析 MVC redirect 作为业务结果。
WebAuthn / Passkey
使用 NuGet 包:
Fido2
配置:
{
"WebAuthn": {
"ServerName": "Dpz.Core Auth",
"ServerDomain": "auth.example.com",
"Origins": [
"https://auth.example.com"
]
}
}
主要接口:
| 功能 | 端点 |
|---|---|
| 凭证列表 | GET /api/auth/webauthn/credentials |
| 注册选项 | POST /api/auth/webauthn/register/options |
| 完成注册 | POST /api/auth/webauthn/register/complete |
| 登录选项 | POST /api/auth/webauthn/assertion/options |
| 完成登录 | POST /api/auth/webauthn/assertion/complete |
| 删除凭证 | DELETE /api/auth/webauthn/credentials/{credentialId} |
Passkey 策略:
- Resident key required。
- User verification required。
- Challenge 存入 FusionCache,短时间有效,只允许完成对应注册/登录流程。
- 登录断言成功后更新签名计数和最近使用时间。
- Passkey 登录成功后会写入
Dpz.Auth.CurrentPasskeyCookie(Data Protection 加密、HttpOnly、30 天有效),记录当前设备最近使用的凭证;密码登录和退出登录会清除它。 - 该 Cookie 是敏感操作 MFA 的设备感知依据:只有 Cookie 指向的凭证仍归属当前账号时,才认为当前设备可用 Passkey。
用户安全绑定
登录成功后会聚合检查当前用户是否已绑定 2FA 和 Passkey:
| 功能 | 端点 |
|---|---|
| 安全绑定状态 | GET /api/auth/security-bindings |
| 获取 2FA 绑定信息 | GET /api/auth/two-factor/setup |
| 绑定 2FA | POST /api/auth/two-factor/bind |
| 解绑 2FA | POST /api/auth/two-factor/unbind |
返回模型:
UserSecurityBindingStatusResponse
包含 HasTwoFactor、HasPasskey、RequiresTwoFactor、RequiresPasskey、IsFullyBound。
规则:
- 密码登录和 Passkey 登录成功后都会检查绑定状态。
- 未同时绑定 2FA 与 Passkey 时,进入
security-onboarding.html,用户可以绑定缺失项,也可以跳过继续原始returnUrl。 - 安全状态查询必须是只读操作,不创建、不刷新 2FA 密钥。
- 绑定 2FA 时才生成 setup code。
- 解绑 2FA 必须输入当前 TOTP PIN。
- 落地页已登录导航根据绑定状态显示“绑定 2FA”或“解绑 2FA”。
敏感操作二次验证(MFA)
后台管理操作(新建/更新/删除客户端、撤销授权、撤销令牌)、用户中心撤销会话、删除 Passkey 均受 MFA 保护,统一由 UserMfaVerificationService.VerifyAdminOperationAsync 编排。
| 场景 | 行为 |
|---|---|
| 当前设备有可用 Passkey(Cookie 指向的凭证仍有效) | 优先发起 Passkey 断言验证,失败或取消时回退 2FA 动态验证码 |
| 当前设备无可用 Passkey,但已绑定 2FA | 直接要求 2FA 动态验证码,不再弹 Passkey 窗口 |
| 其他设备已绑定 Passkey、本设备无凭证且未绑定 2FA | 操作直接放行,返回 NeedsSecuritySetup=true,前端提醒安全绑定 |
| 账号未绑定任何 MFA 方式 | 操作直接放行,返回 NeedsSecuritySetup=true,前端引导安全设置 |
协议说明:
- 首次调用不带 MFA 凭证,服务端返回
RequiresVerification=true时,前端完成验证后按VerificationType(passkey/twoFactor)重新提交;AllowsTwoFactorFallback=true时,Passkey 失败后允许用 2FA 动态验证码兜底。 NeedsSecuritySetup=true表示操作已放行但账号缺少安全绑定,前端弹窗提醒并引导至security-onboarding.html;删除最后一个凭证且未绑定 2FA 时同样会返回该标记。- Passkey 断言限定为账号下全部凭证(非 discoverable),且仅在当前设备持有可用 Passkey 时发起,避免在未绑定通行密钥的设备上要求无法完成的验证。
- 前端 MFA 状态机收敛到
ClientApp/src/apps/composables/useMfaVerification.ts单份实现,管理后台、用户中心、Passkey 管理页共用;Passkey 失败回退 2FA 前会先提示原因(优雅回退)。
管理端功能
管理入口:
/admin
Hash 路由:
| 路由 | 功能 |
|---|---|
#/applications | 客户端应用 |
#/authorizations | 授权记录 |
#/tokens | 令牌管理 |
#/access-requests | 访问申请 |
#/grants | 授权白名单 |
高风险操作继续要求二次确认:
- 新建/更新/删除客户端
- 撤销授权
- 撤销用户令牌
- 撤销客户端令牌
- 用户中心撤销会话
- 删除 Passkey
二次验证优先使用当前设备的 Passkey,失败或当前设备不可用时自动回退 2FA 动态验证码;账号未绑定任何安全方式时操作直接放行,但会提醒用户前往安全设置完成绑定(详见"敏感操作二次验证(MFA)")。
运行环境
- .NET 10
- MongoDB
- Redis / FusionCache
- AgileConfig
- OpenIddict
- Node.js / npm 仅用于构建
ClientApp
关键配置:
{
"ConnectionStrings": {
"mongodb": ""
},
"AgileConfig": {
"appId": "",
"secret": "",
"nodes": "",
"env": "DEV"
},
"AuthAssets": {
"AssetsHost": ""
},
"WebAuthn": {
"ServerName": "",
"ServerDomain": "",
"Origins": []
}
}
本地验证
后端:
dotnet build src/Dpz.Core.Auth/Dpz.Core.Auth.csproj
前端:
cd src/Dpz.Core.Auth/ClientApp
npm install
npm run build
npm run lint
npm run format:check
npx vue-tsc --noEmit
手动验证:
- 未登录访问
/connect/authorize会跳转登录。 - 密码登录成功后,若未同时绑定 2FA 和 Passkey,会进入安全引导;否则回跳原始
returnUrl。 - Passkey 登录成功后遵循同一安全引导规则。
- 已登录首页导航根据实际状态显示“绑定 2FA”或“解绑 2FA”。
- 解绑 2FA 输入正确 PIN 后成功解绑,并回到首页刷新导航状态。
- 授权确认后返回客户端应用。
/admin可以加载管理端 hash 路由和分页数据。- 删除客户端、撤销授权、撤销令牌等操作会要求二次验证:当前设备有 Passkey 时优先 Passkey,否则要求 2FA 动态验证码;未绑定任何安全方式时操作直接放行并弹出安全绑定提醒。
- 在未绑定通行密钥的设备上执行敏感操作不会弹 Passkey 窗口(服务端按当前设备凭证判断),直接要求 2FA 动态验证码。
Docker 部署
cd /home/ubuntu/project/dpz.core
git pull
cd /home/ubuntu/project/dpz.core/src
sudo docker build -t dpz.core.auth -f Dpz.Core.Auth/Dockerfile .
sudo docker build \
-t 127.0.0.1:3383/dpz.core.auth:<version> \
-t 127.0.0.1:3383/dpz.core.auth:latest \
-f Dpz.Core.Auth/Dockerfile .
# 推送镜像
sudo docker push 127.0.0.1:3383/dpz.core.auth:<version>
sudo docker push 127.0.0.1:3383/dpz.core.auth:latest
sudo docker run --restart=always \
--name dpz.core.auth \
-e TZ=Asia/Shanghai \
-p 2377:8080 \
-d registry.dpangzi.com/dpz.core.auth:latest
前端构建产物发布说明:
- 仓库不跟踪
wwwroot/auth-assets/**。 - 发布生产环境前先执行
npm run build。 - 手动上传
wwwroot/auth-assets/**到自建 CDN。 - 同步提交/发布
wwwroot/auth-assets-manifest.json,确保后端能拼接正确 CDN 地址。