跳转到主要内容

使用 Keycloak 做为 OIDC 身份提供商与 Bookstack 进行 SSO 集成

SSO integration with Bookstack using Keycloak as OIDC Identity Provider
I did not find the corresponding detailed records in English, if you need, please use Google Translate :D.

身份提供商 Identity Provider (IdP) ,你可以简单理解为中心用户管理、验证系统,如此处的 Keycloak。

服务提供商 Service Provider (SP),实际服务提供者,比如此处的 Bookstack(本站所用程序)。

看起来似乎没用那么难,但还是踩了很多坑的,也没有搜到稍微详细一点的教程,在摸索完后记录一下。

Keycloak + NGINX 安装

详细请参阅官方文档,此次仅列举一些官方文档没有提到或模糊的地方。

本例使用单机 Keycloak + NGINX 做为反向代理

使用 systemd 管理 Keycloak

我不太喜欢环境变量,于是配置都写在 <keycloak路径>/conf/keycloak.conf

/etc/systemd/system/keycloak.service
[Unit]
Description=Keycloak Open Source Identity and Access Management
After=syslog.target
After=network.target


[Service]
Type=simple
Restart=on-failure
RestartSec=2s

# Disable timeout logic and wait until process is stopped
TimeoutStopSec=0

# SIGTERM signal is used to stop the Java process
KillSignal=SIGTERM

# Send the signal only to the JVM rather than its control group
KillMode=process

# Java process is never killed
SendSIGKILL=no

# When a JVM receives a SIGTERM signal it exits with code 143
SuccessExitStatus=143
LimitMEMLOCK=infinity
LimitNOFILE=65535
User=<keycloak 用户>
Group=<keycloak 组>
WorkingDirectory=<工作目录>


# Database
#Environment=KC_DB=mysql
#Environment=KC_DB_URL_DATABASE={{ kc_db_name |default('keycloak')}}
#Environment=KC_DB_PASSWORD={{ kc_db_pass}}
#Environment=KC_DB_USERNAME={{ kc_db_user }}
#Environment=KC_DB_URL_HOST={{ kc_db_host}}


# Proxy
#Environment=KC_PROXY=edge
#Environment=KC_HOSTNAME_STRICT=false


# HTTPS
#Environment=KC_HTTPS_PORT={{ kc_port }}
## Debian default ssl-cert snakeoil cert
#Environment=KC_HTTPS_CERTIFICATE_FILE={{ kc_cert |default('/etc/ssl/certs/ssl-cert-snakeoil.pem') }}
#Environment=KC_HTTPS_CERTIFICATE_KEY_FILE={{ kc_cert_key |default('/etc/ssl/private/ssl-cert-snakeoil.key') }}



# Default user
Environment=KEYCLOAK_ADMIN=<管理员账号>
Environment=KEYCLOAK_ADMIN_PASSWORD=<管理员密码>



#{% if kc_port |int <= 1024 %}
## Allow ports below 1024.
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
#AmbientCapabilities=CAP_NET_BIND_SERVICE
#{% endif %}


ExecStart=<keycloak路径>/bin/kc.sh --config-file=<keycloak路径>/conf/keycloak.conf --verbose start --spi-connections-jpa-quarkus-migration-strategy=update

#WARNING: The '--auto-build' option for 'start' command is DEPRECATED and no longer needed. When executing the 'start' command, a new server image is automatically built based on the configuration. If you want to disable this behavior and achieve an optimal startup time, use the '--optimized' option instead.

[Install]
WantedBy=multi-user.target

使用 Mysql 做为数据库,由于使用 NGINX 进行本地反代,所以使用 http 模式就够了 (proxy=edge)。

<keycloak路径>/conf/keycloak.conf
# Basic settings for running in production. Change accordingly before deploying the server.

# Database

# The database vendor.
db=mysql

db-url-host=localhost

# The username of the database user.
db-username=

db-url-database=

# The password of the database user.
db-password=

# The full database JDBC URL. If not provided, a default URL is set based on the selected database vendor.
db-url=jdbc:mysql://localhost:3306/keycloak?useSSL=false&characterEncoding=UTF-8

# Observability

# If the server should expose healthcheck endpoints.
health-enabled=true

# If the server should expose metrics endpoints.
metrics-enabled=false

# HTTP
# Hostname for the Keycloak server.
#hostname=tree.pha.pub

#hostname-strict-https=true
hostname-strict=false
hostname-strict-https=false

https-port=8443
http-port=8444

# The file path to a server certificate or certificate chain in PEM format.
#https-certificate-file=/usr/local/nginx/conf/ssl/2x.pha.pub.crt

# The file path to a private key in PEM format.
#https-certificate-key-file=/usr/local/nginx/conf/ssl/2x.pha.pub.key

#https-protocols=TLSv1.3,TLSv1.2

# The proxy address forwarding mode if the server is behind a reverse proxy.
#proxy=reencrypt
proxy=edge


# Do not attach route to cookies and rely on the session affinity capabilities from reverse proxy
#spi-sticky-session-encoder-infinispan-should-attach-route=false

Java 安装

Keycloak 默认会直接使用 java 环境变量来运行,如果你有多个 java 安装,这可能不是那么稳定。

目前的 Keycloak 21.0.1要求使用 Java 11。

你可以在 <keycloak路径>/bin/kc.sh 中找到下面这几行代码,并将 java 路径修改至固定值。
当然,你也可以修改 JAVA_OPTS= 来自定义内存大小等。

if [ "x$JAVA" = "x" ]; then
    if [ "x$JAVA_HOME" != "x" ]; then
        JAVA="<keycloak路径>/graalvm-ce-java11-22.3.1/bin/java"
    else
        JAVA="<keycloak路径>/graalvm-ce-java11-22.3.1/bin/java"
    fi
fi

NGINX 设置

https://www.keycloak.org/server/reverseproxy  文档中不建议直接暴露 / 路径,而是建议使用 /auth 等路径,但是说实话没搞太懂,所以我就直接暴露 / 路径了,当然你也可以改一下主页做一下伪装,或者屏蔽掉 /admin 的访问。

nginx-keycloak.conf
upstream keycloakserver {
    server 127.0.0.1:9444;
    keepalive 120;
}


server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name keycloak.example.com;


   #SSL 配置等略


   location / {
        proxy_pass http://keycloakserver;
        proxy_set_header Host $host;
        #注意你的真实IP请求头,如果你前面还有 CDN 什么的,需使用 CDN 提供的头
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }





}

Keycloak 设置

使用你在 /etc/systemd/system/keycloak.service 中配置的账号登录。

1.创建 releam(可选)
2.切换到创建的 releam
3.Manage -> Clinet -> Create clinet

4.Next

    • Client authentication:On
    • Authorization:On
    • Authentication flow:保持默认

5.Next

    • Root URL:<你的Bookstack网址>
    • Home URL:<你的Bookstack网址>
    • Valid redirect URIs:<你的Bookstack网址>/oidc/callback

6.创建完成
7.点击 Credentials

    • Client Authenticator:Client ID and Secret
    • Save
    • Client secret:先点击 regenerate 然后复制,等下要用。

8.Keycloak 客户端配置完成

BookStack 设置

https://www.bookstackapp.com/docs/admin/oidc-auth/

根据你的配置,在 bookstack/.env 中添加如下配置:

注:# Issuer URL 实际可以在你的 Keycloak -> Configure -> Realm settings -> Endpoints 处找到。

bookstack/.env
# Set OIDC to be the authentication method
AUTH_METHOD=oidc
#AUTH_METHOD=standard
#AUTH_METHOD=saml2


# Control if BookStack automatically initiates login via your OIDC system 
# if it's the only authentication method. Prevents the need for the
# user to click the "Login with x" button on the login page.
# Setting this to true enables auto-initiation.
AUTH_AUTO_INITIATE=false

# Set the display name to be shown on the login button.
# (Login with <name>)
OIDC_NAME='登录提供商名'

# Name of the claims(s) to use for the user's display name.
# Can have multiple attributes listed, separated with a '|' in which 
# case those values will be joined with a space.
# Example: OIDC_DISPLAY_NAME_CLAIMS=given_name|family_name
OIDC_DISPLAY_NAME_CLAIMS=preferred_username|username|name

# OAuth Client ID to access the identity provider
OIDC_CLIENT_ID='你刚刚填的Clinet ID'

# OAuth Client Secret to access the identity provider
OIDC_CLIENT_SECRET='你刚刚复制的Clinet secret'

# Issuer URL
# Must start with 'https://'
OIDC_ISSUER=https://<你的keycloak网址>/realms/<领域名>
#你可以访问 https://<你的keycloak网址>/realms/<领域名>/.well-known/openid-configuration 来测试是否正常

# Enable auto-discovery of endpoints and token keys.
# As per the standard, expects the service to serve a 
# `<issuer>/.well-known/openid-configuration` endpoint.
OIDC_ISSUER_DISCOVER=true

# Configure a custom ID Token claim to be used as the
# "External Authentication ID" within BookStack.
OIDC_EXTERNAL_ID_CLAIM=sub



#以下是组同步,如果你不需要就先注释,否则无法登录

# Enable OIDC group sync.
OIDC_USER_TO_GROUPS=true

# Set the attribute from which BookStack will read groups names from.
OIDC_GROUPS_CLAIM=realm_access.roles

# Additional scopes to send with the authentication request.
# By default BookStack only sends the 'openid', 'profile' & 'email' scopes.
# Many platforms require specific scopes to be requested for group data.
# Multiple scopes can be added via comma separation.
OIDC_ADDITIONAL_SCOPES=roles

# Remove the user from roles that don't match OIDC groups upon login.
# Note: While this is enabled the "Default Registration Role", editable within the 
# BookStack settings view, will be considered a matched role and assigned to the user.
OIDC_REMOVE_FROM_GROUPS=true


# Enable the BookStack general debug mode, Provides more error insight.
# Note, Can leak sensitive details so only use in private, secure environments.
APP_DEBUG=false

# Dump out the details fetched from the identity provider.
# Only set this option to true if debugging since it will block logins
# and potentially show private details.
OIDC_DUMP_USER_DETAILS=false

# Option to override settings passed to the underlying onelogin library
# used by BookStack. For development, debugging and testing only.
# Options provided will be recursively merged into other default & dynamic options.
# Defaults found within app/Config/saml2.php, under the 'onelogin' key.
#SAML2_ONELOGIN_OVERRIDES=<json_format_data>

组/角色同步

1.首先在 bookstack 中创建对应的角色,分配好权限,只能用英文;

2.在你的 keycloak 中创建名称相同的 Groups 和 Realm roles,在组设置的 Role mapping 将角色分配到同名组;

3.在你的 keycloak 中的 Client scopes 中找到 roles -> Mappers -> realm roles;

    • Add to ID token:On
    • Add to access token:On
    • Add to userinfo:On

4.在你的 keycloak 中创建用户,并分配到对应的 Group;

5.然后如果你在 bookstack/.env 中设置 OIDC_DUMP_USER_DETAILS=true 然后登录,你应该可以在输出的 JSON  中找到

"realm_access": {
	"roles": [
		"default-roles-master",
		"offline_access",
		"uma_authorization",
		"你的自定义组"
	]
}

6.在 bookstack/.env 中添加如下字段:
OIDC_GROUPS_CLAIM=即指示从哪里获取组名。

# Set the attribute from which BookStack will read groups names from.
OIDC_GROUPS_CLAIM=realm_access.roles

# Additional scopes to send with the authentication request.
# By default BookStack only sends the 'openid', 'profile' & 'email' scopes.
# Many platforms require specific scopes to be requested for group data.
# Multiple scopes can be added via comma separation.
OIDC_ADDITIONAL_SCOPES=roles

# Remove the user from roles that don't match OIDC groups upon login.
# Note: While this is enabled the "Default Registration Role", editable within the 
# BookStack settings view, will be considered a matched role and assigned to the user.
OIDC_REMOVE_FROM_GROUPS=true

7.登录后你应该会被自动分配到该组;

主要参考