# keycloak-avatar-extension **Repository Path**: Dracowyn/keycloak-avatar-extension ## Basic Information - **Project Name**: keycloak-avatar-extension - **Description**: Keycloak的头像插件 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2023-11-07 - **Last Updated**: 2026-03-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: Java, Quarkus, keycloak ## README # Keycloak头像插件 一个 Keycloak 头像扩展,提供头像上传、获取、删除、多尺寸输出,以及 OIDC `picture` / `picture_set` 映射能力。插件通过 SPI 扩展 Keycloak,可通过 REST API 和 Token Claim 使用头像信息。 参考了 [thomasdarimont](https://github.com/thomasdarimont/keycloak-avatar-minio-extension) 和 [mai1015](https://github.com/mai1015/keycloak-avatar-extension) 的实现思路。 ## 运行环境 - Java 21 - Keycloak 26.x ## 功能特性 - 用户头像上传、获取、删除 - 多尺寸头像输出:`xs=32`、`sm=48`、`md=64`、`lg=128`、`xl=256`、`xxl=512` - 默认头像回退 - 可扩展存储 SPI,当前内置文件系统存储 - OIDC `picture` 和 `picture_set` claim 映射 - 自动同步用户属性:`avatarId`、`avatar` ## 配置 ### `spi-avatar-provider-config` | 键 | 类型 | 说明 | 默认值 | |------------------|-----------|-------------------------------------------------|---------| | `defaultAvatar` | `string` | 当开启重定向模式且存储提供者没有返回地址时使用的后备地址 | `/` | | `alwaysRedirect` | `boolean` | 获取图片时是否始终返回重定向,而不是直接输出二进制图片 | `false` | | `defaultSize` | `string` | 默认头像尺寸,允许值:`xs` `sm` `md` `lg` `xl` `xxl` | `lg` | | `publicBaseUrl` | `string` | 可选。用于覆盖 REST JSON 和 OIDC 里生成出来的完整地址前缀,例如 CDN 域名 | 未设置 | | `storageService` | `string` | 头像存储提供者 ID | `file` | | `maxAge` | `int` | 图片响应的缓存时间,单位秒 | `1800` | 说明: - `publicBaseUrl` 适合放 CDN 域名、网关域名或反向代理后的统一公网入口,例如 `https://cdn.example.com`。 - 如果设置了 `publicBaseUrl`,REST 和 OIDC 中返回的完整 URL 会优先使用它。 - 兼容别名:`cdnUrl`。 ### `spi-avatar-storage-file` | 键 | 类型 | 说明 | 默认值 | |------------------|----------|-----------------------------|-------------------------------------------------| | `root` | `string` | 文件存储根目录 | `storage` | | `route` | `string` | 用户头像的文件路径与 URL 模板 | `/{realm}/avatar/{avatar_id}/avatar-{size}.png` | | `baseurl` | `string` | 对外访问头像时使用的 URL 前缀,不影响磁盘存储位置 | `/realms/` | | `default-avatar` | `string` | 默认头像路径模板 | `/{realm}/avatar/default.png` | 说明: - 文件系统实现会把 `route` 同时用于“磁盘路径模板”和“对外 URL 模板”。 - `baseurl` 只用于拼接“返回给客户端的访问地址”,不用于决定文件保存到哪里。 - 文件实际保存位置由 `root + route` 决定。 - 对外访问地址由 `baseurl + route` 决定。 - 例如默认配置下: 磁盘路径是 `storage/myrealm/avatar/123/avatar-lg.png` 对外地址是 `/realms/myrealm/avatar/123/avatar-lg.png` - 默认头像地址由 `default-avatar` 决定,因此默认头像的实际 URL 通常是 `/realms/{realm}/avatar/default.png`。 - 如果同时设置了 `publicBaseUrl`,REST JSON 和 OIDC 最终返回给客户端的完整地址会以 `publicBaseUrl` 为前缀,而不是当前 Keycloak 请求域名。 ## API ### 通用查询参数 | 参数 | 类型 | 说明 | 默认值 | |----------|---------------------------------|-------------------------------|--------------------| | `size` | `enum(xs, sm, md, lg, xl, xxl)` | 头像尺寸;非法值会回退到 `defaultSize` | 配置中的 `defaultSize` | | `format` | `enum(raw, json)` | `raw` 返回图片或重定向;`json` 返回 JSON | `raw` | ### 通过用户 ID 获取头像 ```http GET /realms/{realm}/avatar/by-userid/{user_id}?size={size}&format={format} ``` `format=json` 时响应示例: ```json { "status": 1, "avatar": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-lg.png", "avatar_tpl": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-%s.png", "provided_size": { "xs": 32, "sm": 48, "md": 64, "lg": 128, "xl": 256, "xxl": 512 } } ``` ### 通过用户名获取头像 ```http GET /realms/{realm}/avatar/by-username/{username}?size={size}&format={format} ``` ### 获取当前登录用户头像 ```http GET /realms/{realm}/avatar?size={size}&format={format} ``` 行为说明: - 已登录时返回当前用户头像。 - 未登录且 `format=json` 时返回 `403 Forbidden`。 - 未登录且 `format=raw` 时回退到默认头像。 ### 获取默认头像 ```http GET /realms/{realm}/avatar/default?size={size}&format={format} ``` 行为说明: - `format=json` 时返回完整 URL。 - 如果设置了 `publicBaseUrl`,则完整 URL 会优先使用该前缀。 - `format=json` 时返回默认头像地址。 - `format=raw` 时返回默认头像图片,或在 `alwaysRedirect=true` 时重定向到默认头像地址。 ### 上传头像 #### 通过用户 ID 上传 需要管理员可管理用户权限。 ```http POST /realms/{realm}/avatar/by-userid/{user_id} Content-Type: multipart/form-data ``` 表单字段: - `image`: 必填,图片文件 - `crop`: 可选,支持两种格式 `{"x":0,"y":0,"size":100}` `0,0,100` 成功响应: ```json { "status": 1, "avatar": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-lg.png", "avatarId": "{avatar_id}" } ``` #### 通过用户名上传 需要管理员可管理用户权限。 ```http POST /realms/{realm}/avatar/by-username/{username} Content-Type: multipart/form-data ``` 表单字段与按用户 ID 上传一致。 #### 当前用户上传 需要用户已登录。 ```http POST /realms/{realm}/avatar Content-Type: multipart/form-data ``` 表单字段与按用户 ID 上传一致。 ### 删除头像 仅支持通过用户 ID 删除,需要管理员可管理用户权限。 ```http DELETE /realms/{realm}/avatar/by-userid/{user_id} ``` 成功时: ```http HTTP/1.1 200 OK ``` 删除行为: - 删除各尺寸头像文件 - 删除 `USER_AVATAR` 表中的对应记录 - 清除用户属性 `avatar` 和 `avatarId` ### 直接访问头像文件 ```http GET /realms/{realm}/avatar/{avatar_id}/avatar-{size}.png ``` 说明: - 返回 PNG 图片 - 支持 `ETag` 和 `Last-Modified` - 响应缓存时间受 `spi-avatar-provider-config.maxAge` 控制 ## 错误响应 JSON 模式下,接口会返回真实 HTTP 状态码,而不是固定 `200`。 常见状态码: - `400 Bad Request`:缺少 `image`、上传内容不是有效图片、`crop` 参数非法 - `403 Forbidden`:未认证或权限不足 - `404 Not Found`:用户、头像或默认头像不存在 - `500 Internal Server Error`:存储或服务内部错误 JSON 错误响应示例: ```json { "status": 404, "error": "resource not found", "errormsg": "avatarNotFound" } ``` ## OpenID Connect 协议映射 ### `AvatarMapper` 插件提供 OIDC Protocol Mapper,会把头像信息写入 `picture`,并在有多尺寸可用时写入 `picture_set`。 #### 用户已设置头像 示例: ```json { "picture": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-lg.png", "picture_set": { "32px": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-xs.png", "48px": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-sm.png", "64px": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-md.png", "128px": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-lg.png", "256px": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-xl.png", "512px": "https://keycloak.example.com/realms/myrealm/avatar/{avatar_id}/avatar-xxl.png" } } ``` 说明: - `picture` 使用默认尺寸对应的头像 URL。 - `picture_set` 只包含当前能生成出地址的尺寸。 - 如果设置了 `publicBaseUrl`,则 `picture` 和 `picture_set` 里的完整 URL 会优先使用该前缀。 #### 用户未设置头像 如果默认头像可用,则 `picture` 会被设置为默认头像的完整 URL,例如: ```json { "picture": "https://keycloak.example.com/realms/myrealm/avatar/default.png" } ``` 如果默认头像不存在,则不会写入 `picture`。 #### 用户属性 上传成功后会同步: - `avatarId`: 头像唯一标识 - `avatar`: 默认尺寸头像 URL;如果过长无法写入用户属性,则写入 `"provided"` ## 安装部署 1. 构建项目 ```bash mvn clean package ``` 2. 复制产物到 Keycloak `providers` 目录 ```bash cp target/avatar-extension-{version}.jar $KEYCLOAK_HOME/providers/ ``` 3. 重启 Keycloak 4. 在 Keycloak 中配置对应 SPI ## 兼容性说明 - 当前版本的 JSON 错误响应使用真实 HTTP 状态码。 - 当前版本的 REST JSON 响应中的 `avatar` / `avatar_tpl` / 默认头像地址使用完整 URL。 - 当前版本的 OIDC 默认头像 `picture` 使用完整 URL,默认文件系统实现通常是 `/realms/{realm}/avatar/default.png`。 - 如果设置了 `publicBaseUrl`,REST 和 OIDC 返回的完整地址会优先使用该前缀;`cdnUrl` 可作为兼容别名。 - `crop` 目前同时兼容 JSON 格式和逗号分隔格式。 ## 存储提供者 当前仅内置文件系统存储提供者。头像文件按 `realm` 和 `avatarId` 组织。 后续可扩展到 MinIO、S3 等其他存储后端。