基于 Keycloak 的网协通行证。
旧版通行证:https://github.com/BITNP/keycloak-account-service
准备以下服务:
- Docker 与 Docker Compose,或者 Podman
- PostgreSQL
- Keycloak 26 实例
- S3 兼容对象存储与公有读头像桶(启用头像上传时需要)
在仓库根目录执行:
cp .env.example .env编辑 .env,填写实际配置:
| 变量 | 说明 |
|---|---|
BITNP_PASSPORT_IMAGE |
应用镜像名,本机构建可用 bitnp-passport:local |
APP_URL |
通行证公开地址,如 https://accounts.bitnp.net,使用域名根路径 |
DATABASE_URL |
通行证 PostgreSQL 连接串 |
S3_ENDPOINT |
公有读 S3 兼容端点,如 https://s3.bitnp.net,使用 path-style |
S3_ACCESS_KEY / S3_SECRET_KEY |
对象存储凭据,需要 PutObject、DeleteObject |
S3_BUCKET |
已存在的公有读头像桶 |
OIDC_ISSUER_URL |
Keycloak realm 地址,如 https://sso.example.com/realms/master,不带末尾斜杠 |
OIDC_CLIENT_ID |
通行证客户端 ID,默认示例为 bitnp-passport |
OIDC_CLIENT_SECRET |
通行证客户端密钥 |
KEYCLOAK_SERVICE_CLIENT_ID |
服务客户端 ID,默认示例为 bitnp-passport-service |
KEYCLOAK_SERVICE_CLIENT_SECRET |
服务客户端密钥 |
SESSION_ENCRYPTION_KEY |
32 字节随机密钥的 Base64 编码,用于保存登录会话 |
LOG_LEVEL |
日志级别,通常使用 info,排查问题时可改为 debug |
头像桶需要已存在并允许匿名 GetObject;通行证使用 PutObject 写入,并在更换或移除头像时清理旧对象。S3_ENDPOINT 会直接拼成浏览器访问的头像 URL,必须是用户可访问的公有地址。未配置 S3_* 时头像上传不可用,已有头像的显示不受影响。
生产部署建议使用 Forgejo Container Registry 中的固定版本镜像,例如 git.bitnp.net/bitnp/bitnp-passport:v1.0.0。
可用以下命令生成会话密钥,将结果填入 SESSION_ENCRYPTION_KEY:
openssl rand -base64 32下面的 <APP_URL> 需要被替换为 .env 配置的地址
创建 OpenID Connect 客户端 bitnp-passport:
| 设置 | 值 |
|---|---|
| 客户端认证 | 开启 Client authentication |
| 登录流程 | 开启 Standard flow,并保持 Use refresh tokens 开启 |
| Valid redirect URIs | <APP_URL>/auth/callback |
| Valid post logout redirect URIs | <APP_URL> 和 <APP_URL>/i/* |
| PKCE | S256 |
关闭 Implicit flow、Direct access grants 和 Service accounts。保留 profile、email、roles client scopes 及其默认映射。
启用内置的 account 客户端,确保普通用户拥有其 manage-account 角色,并允许该角色进入通行证客户端签发的 access token。Token 的 audience 需要包含 account,可在通行证客户端添加 Audience mapper,Included Client Audience 设为 account,并开启 Add to access token。
将通行证客户端密钥填入 OIDC_CLIENT_SECRET。
创建 OpenID Connect 客户端 bitnp-passport-service:
- 开启 Client authentication
- 开启 Service accounts,关闭 Standard flow、Implicit flow 和 Direct access grants
- 在 Service account roles 中,授予
master-realm客户端的manage-users、view-realm、view-clients和query-clients角色 - 将客户端密钥填入
KEYCLOAK_SERVICE_CLIENT_SECRET
如果现有实例启用了细粒度管理权限,还需允许服务账户查询用户与群组、创建群组和管理成员。服务账户用于通行证访问 Keycloak;管理员权限规则见下文。
- 关闭通行证客户端的 Front channel logout
- Backchannel logout URL 填
<APP_URL>/auth/backchannel-logout - 开启 Backchannel logout session required,并保证 Keycloak 可以访问该 URL
- 在 Realm settings → User profile 中,取消
lastName的 Required;通行证用一个“真实姓名”字段保存姓名,可将firstName的 Display name 设为“真实姓名”,保留用户编辑权限 - 使用头像功能时,在 User profile 中添加单值属性
picture,允许用户和管理员查看、用户编辑,长度上限设为 2048
修改密码、删除认证器等操作会跳转到 Keycloak 完成,然后返回通行证。
keycloak/ 提供注册事件监听器。新用户注册成功且未设置 picture 时,随机保存 https://s3.bitnp.net/avatar/avatars/default/1.jpg 至 9.jpg 中的一张。
本地开发的 Keycloak 镜像已包含该监听器。源码更新后执行:
docker compose -f compose.dev.yaml build keycloak
docker compose -f compose.dev.yaml up -d keycloak固定下载地址:bitnp-default-avatar.jar。
也可在本地单独构建插件:
mvn -f keycloak/pom.xml package将 keycloak/target/bitnp-default-avatar.jar 放入 Keycloak 的 /opt/keycloak/providers/。使用 start --optimized 的实例需要重新执行 kc.sh build,然后重启 Keycloak。
- 允许自行注册时,在 realm 中开启 User registration
- 关闭 Duplicate emails
- 启用 Forgot password 并配置 SMTP,供用户通过邮件重置密码;按需配置 Verify email、密码策略和多因素认证
- “网协现任”由
position-*角色决定:普通成员通过部门组的position-<department>-member获得,职务通过个人position-*授予;position-association-vice-chair仅作为复合角色父级,不单独计入 - 当前届组织取
/bitnp/active-YYYY下四个部门组都配置了position-<department>-member的最新一届;增删shared/identity.ts中的departments时需同步调整并迁移已有届的映射 - 通行证本地会话保留两周,登录或令牌续期后延长;令牌是否有效仍由 Keycloak 的 SSO Session、Client Session 等设置控制
帮助页的“重置密码”和账户安全页的“忘记密码?”均通过 /auth/reset-password 进入 Keycloak 的 protocol/openid-connect/forgot-credentials 重置流程,复用通行证的 OIDC 回调,完成后返回账户安全页。
完成 .env 和 Keycloak 配置后,在仓库根目录执行:
docker compose build webdocker compose run --rm web node scripts/migrate.ts该命令创建或升级通行证数据表,并初始化后台任务队列。每次发布执行一次。
通行证从 Keycloak 读取有效角色。系统管理使用 permission-system-admin,部门管理使用 permission-<department>-admin。具体请见内部 Keycloak 权限文档。
docker compose up -d
docker compose ps
docker compose logs --tail=100 web workerCompose 将网站绑定到宿主机的 127.0.0.1:3000。配置宿主机上的反向代理,将 APP_URL 对应域名的全部请求转发到 http://127.0.0.1:3000,并提供 HTTPS。
git remote add forgejo https://git.bitnp.net/BITNP/bitnp-passport.git
docker login git.bitnp.net先更新 package.json 中的版本号,再发布:
scripts/publish-release.sh群组在 Keycloak 中存在后,可在后台“群组配置”中明确纳入或取消纳入。
后台“任职管理”维护个人职务,“系统管理权限”展示有效系统权限及来源。换届页面从 Keycloak 读取当前组织,确认四个部门的成员身份映射、个人任职和旧届邀请变化。历史组织关系及个人特殊授权保持独立。
pnpm install --frozen-lockfile
cp dev/development.env .env
pnpm dev:up
pnpm db:migrate
pnpm dev在另一个终端进入仓库目录,启动后台任务:
pnpm worker本地默认不配置 S3,头像上传会返回“头像存储未配置”。需要测试上传时,在 dev/development.env 中补充 S3_ENDPOINT、S3_ACCESS_KEY、S3_SECRET_KEY 和 S3_BUCKET,使用现有共享 RustFS,不要另起实例。
| 服务 | 地址 |
|---|---|
| 通行证 | http://localhost:3000 |
| Keycloak 管理控制台 | http://localhost:8080/admin/ |
| PostgreSQL | localhost:5432,用户名和密码均为 bitnp |
开发 Keycloak 使用与线上相同的 position-* / permission-* 模型,包含 64 个组织群组和 168 个测试账户。所有测试账户的密码均为 dev-password。
| 用户名 | Keycloak 身份与权限 |
|---|---|
admin |
个人特批系统管理员 |
manager |
技术部部长,只有技术部管理权限 |
member |
本届技术部普通成员 |
outsider |
无组织归属,用于测试邀请注册和加入 |
vice-techdept-1 |
分管技术部的副主席,无系统权限 |
vice-techdept-special |
相同职务,另有个人特批系统权限 |
数据库已经初始化后,每次开发只需运行:
pnpm dev:up
pnpm dev另一个终端运行 pnpm worker。