folder
folder-controller
folder-middleware
folder-class
folder
folder-secure
folder-controller
folder-views
folder-public
csharp
docker
visualstudio
csharp
readme
csharp

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&lt;PagedListWarp&lt;T&gt;&gt;"]
    Authz --> Result
    Tokens --> Result
    Requests --> Result
    Grants --> Result

项目边界

  • OpenIddict / Cookie / Antiforgery / 权限策略仍由 ASP.NET Core 负责。
  • MVC 只保留最小宿主视图、登录边界、OIDC 回调边界和必要的后端流程。
  • 前端交互统一通过 JSON API 完成,返回 ResponseResultResponseResult<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/authorizechange-password.htmlpasskeys.htmlsecurity-onboarding.htmltwo-factor.htmlunbind-two-factor.html登录后的授权确认、访问拒绝、改密、2FA、Passkey、安全引导页面
管理后台/admin/api/admin/...客户端、授权、令牌、访问申请和白名单管理

前端工程

源码位于:

src/Dpz.Core.Auth/ClientApp

四入口:

入口文件说明
publicsrc/entries/public.ts落地页、登录页等匿名页面
identitysrc/entries/identity.ts授权确认、访问拒绝、修改密码、2FA 绑定/解绑、Passkey 管理、登录后安全引导
usersrc/entries/user.ts用户个人中心 SPA,使用 hash 路由
adminsrc/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        ← 登录页私有样式

规则

  1. 按 entry 拆包。 public.ts 只 import public.cssidentity.ts 只 import identity.cssuser.ts 只 import user.cssadmin.ts 只 import admin.css
  2. 公共 partial 用 _ 前缀。 例如 _base.css_nav.css;页面私有样式保持普通命名,例如 landing.csssignin.cssuser.css
  3. 不要把所有页面样式重新汇总到单一全局入口。 入口 CSS 只能 import 当前 entry 需要的公共 partial 和私有样式。
  4. 暗色模式样式紧跟在对应规则后面,用 body.body--dark .selector 覆写,不要集中写在一个暗色块里。
  5. 响应式断点写在所属文件的末尾,用 @media (max-width: 768px) { ... } 包裹。
  6. 类名沿用 BEM 风格.block__element--modifier.block-element(单破折号衔接)。不对齐某一种 strict BEM,但要求:
    • 有意义的命名空间前缀(如 .landing-.hero-.dashboard-.signin-.auth-
    • 修饰符用双破折号(如 .hero-title--gradient.hero-btn--primary
  7. !important 只用于覆盖 Quasar 组件内置样式(如 border-radius: 14px !important),必须注释原因。
  8. 颜色不使用硬编码 hex,优先考虑
    • Quasar 内置颜色类(text-primarybg-positive 等)
    • 组件内使用 Quasar 颜色属性
    • CSS 文件中使用统一的色值集合(teal #008080 / indigo #6366f1 / slate 系列 #0f172a #475569 #94a3b8
  9. Vite 构建时解析 @import,每个 entry 输出自己的 CSS bundle,manifest 中应存在独立的 entries/public.cssentries/identity.cssentries/user.cssentries/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/applicationsResponseResult<PagedListWarp<AuthApplicationPageItem>>
授权记录GET /api/admin/authorizationsResponseResult<PagedListWarp<AuthAuthorizationPageItem>>
令牌管理GET /api/admin/tokensResponseResult<PagedListWarp<AuthTokenPageItem>>
访问申请GET /api/admin/access-requestsResponseResult<PagedListWarp<AuthAccessRequestItem>>
授权白名单GET /api/admin/grantsResponseResult<PagedListWarp<AuthAllowedClientItem>>

用户中心已接入的端点:

模块端点返回类型
个人资料GET /api/user/profileResponseResult<UserCenterProfileResponse>
安全概览GET /api/user/securityResponseResult<UserCenterSecurityResponse>
我的授权应用GET /api/user/authorizationsResponseResult<PagedListWarp<AuthAuthorizationPageItem>>
登录/会话记录GET /api/user/sessionsResponseResult<PagedListWarp<UserCenterSessionItem>>
我发表的文章GET /api/user/articlesResponseResult<PagedListWarp<UserCenterContentItem>>
我发表的碎碎念GET /api/user/mumblesResponseResult<PagedListWarp<UserCenterContentItem>>
我发表的时间轴GET /api/user/timelinesResponseResult<PagedListWarp<UserCenterContentItem>>
我的相册GET /api/user/albumsResponseResult<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.IndexUpsertAppDraftPersistDraft、手写 ToPageItem
  • 后端删除 AuthorizationManageController.IndexTokenController.IndexAccessRequestController.IndexGrantController.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=falseMessage 和必要错误码。前端不解析 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.CurrentPasskey Cookie(Data Protection 加密、HttpOnly、30 天有效),记录当前设备最近使用的凭证;密码登录和退出登录会清除它。
  • 该 Cookie 是敏感操作 MFA 的设备感知依据:只有 Cookie 指向的凭证仍归属当前账号时,才认为当前设备可用 Passkey。

用户安全绑定

登录成功后会聚合检查当前用户是否已绑定 2FA 和 Passkey:

功能端点
安全绑定状态GET /api/auth/security-bindings
获取 2FA 绑定信息GET /api/auth/two-factor/setup
绑定 2FAPOST /api/auth/two-factor/bind
解绑 2FAPOST /api/auth/two-factor/unbind

返回模型:

UserSecurityBindingStatusResponse

包含 HasTwoFactorHasPasskeyRequiresTwoFactorRequiresPasskeyIsFullyBound

规则:

  • 密码登录和 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 时,前端完成验证后按 VerificationTypepasskey / 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 地址。
评论加载中...