Keycloak 13 自定义用户身份认证流程(User Storage SPI)

Keycloak

版本:13.0.0

spring-boot 项目 Github
user-storage-spi 项目 Github

介绍

Keycloak 是为现代应用程序和服务提供的一个开源的身份和访问管理的解决方案。

Keycloak 在测试环境可以使用内嵌数据库,生产环境需要重新配置数据库。以下将一一介绍如何使用内嵌数据库、重新配置数据库。

特别需要注意 Keycloak 是在 WildFly 上构建的。

安装

系统要求

  • Java8 JDK
  • 至少 512M 内存
  • 至少 1G 磁盘
  • 如果要设置 Keycloak 集群则需要数据库,比如:PostgreSQL、Oracle、MySQL 等
  • 如果要运行集群,需要网络支持广播。当然也可以不需要,只不过需要更改一堆配置

目录结构

  • bin/ —— 各种启动服务、服务器上执行管理的脚本
  • domain/ —— 集群模式下的配置文件和工作目录
  • modules/ —— 服务使用的 Java 包
  • standalone/ —— 单机模式下的配置文件和工作目录
  • standalone/deployments/ —— 你自定义的扩展文件
  • themes/ —— 界面主题文件

使用 Docker 安装 Keycloak

  1. 启动容器

    docker run -p 10010:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:13.0.0
    
  2. 成功启动后访问:http://127.0.0.1:10010/

  3. 登录 Keycloak 服务

    a. 点击 Administration Console

    b. 输入账号:admin,密码:admin

    c. 进入控制台界面

使用

创建 realm 和 用户

创建一个 realm 和用户以访问内置的账户管理控制台。把 realm 想像成租户的概念。

  • Master realm —— 这个 realm 是初始化时创建的,它包含超级管理员。使用这个 realm 管理其它的 realm。
  • Other realm —— 超级管理员创建的 realm 。在这些 realm 里,超级管理员创建用户和应用程序,用户拥有应用程序。

创建 realm

  1. 点击 Add realm 按钮

  2. 输入 realm 名称

  3. 点击 create 按钮完成创建
    点击创建完成 demo realm 创建。

创建用户

  1. 切换 realm 到 Demo

  2. 创建用户,Users -> Add User,输入 Username ,点击 Save 按钮

  3. 设置密码

新建用户登录控制台

  1. 退出 admin 账户登录
  2. 地址输入:http://127.0.0.1:10010/auth/realms/demo/account/,点击 Sign In 按钮,使用新创建的用户 Zhang 登录
  3. 设置新密码
  4. 登录成功

Spring-Boot 认证

注册客户端到 Keycloak 中

应用程序或服务为了能使用 Keycloak,必须在 Keycloak 中注册一个客户端。可以通过超级管理员界面注册,客户端也可以自己通过 Keycloak 服务注册。

客户端注册服务提供内置支持:Keycloak Client Representations、OIDC 客户端元数据、SAML 实体描述。客户端注册服务地址是:/auth/realms/<realm>/clients-registrations/<provider>

内置的 provider

  • default —— Keycloak Client Representation(JSON)
  • install —— Keycloak Adapter Configuration(JSON)
  • openid-connect —— OIDC 客户端元数据描述(JSON)
  • saml2-entity-descriptor —— SAML 实体描述者(XML)

认证

调用客户端注册服务需要令牌。令牌可以是 bearer 令牌,初始化访问令牌或者注册令牌。不需要令牌注册客户端也可以,但是需要配置客户端注册策略。

Bearer 令牌

Bearer 令牌可以代表用户或者服务账户。调用端点需要以下权限:

  • create-client 或者 manage-client —— 创建客户端
  • view-client 或者 manage-client —— 查看客户端
  • manage-client —— 更新或者删除客户端

如果使用 Bearer 令牌创建客户端,推荐使用来自服务账户(create-client 角色)的令牌。

初始化访问令牌

推荐使用初始化访问令牌注册客户端。初始化访问令牌只能用于创建客户端,并且可以配置有效期,同时可以配置可以创建多少客户端。

初始化访问令牌可以通过超级管理员控制台创建。

点击保存,完成令牌的创建。

当点击保存以后会生成令牌,这个令牌如果忘记复制了,那么就只能重新创建了。使用 bearer 令牌:

Authorization: bearer eyJhbGciOiJSUz...
注册访问令牌

当通过客户端注册服务创建客户端时,返回值会包含一个注册访问令牌。注册访问令牌提供检索客户端配置、更新或者删除客户端的权限。注册访问令牌使用方式和 bear 令牌、初始化访问令牌的使用方式是一样的。注册访问令牌是一次性的,当它使用时,返回值里总会包含一个新的令牌。

如果客户端在客户端注册服务之外创建,注册访问令牌就不会和客户端关联起来了。但可以在超级管理员控制台生成注册访问令牌。

Keycloak Representations

default 客户端注册提供商可以创建、检索、更新、删除客户端。使用 Keycloak Client Representation 转换提供配置客户端支持,就像在超级管理员控制台配置的一样。

创建 Client Representation(JSON)执行 HTTP POST 请求 /auth/realms/<realm>/clients-registrations/default

检索 Client Representation 执行 GET 请求/auth/realms/<realm>/clients-registrations/default/<client id>

它也会返回新的注册访问令牌。

要更新 Client Representation 执行 HTTP PUT 请求 /auth/realms/<realm>/clients-registrations/default/<client id>

它也会返回一个新的注册访问令牌。

要删除 Client Representation 执行 HTTP DELETE 请求 /auth/realms/<realm>/clients-registrations/default/<client id>

Keycloak 适配器配置

installation 客户端注册提供商可以用于为客户端获取适配器配置。除了令牌身份验证之外,还可以使用 HTTP basic 认证(通过客户端凭证)。使用下列请求头以完成 HTTP basic 认证:

Authorization: basic BASE64(client-id + ':' + client-secret)

要获取适配器配置执行 HTTP GET 请求:/auth/realms/<realm>/clients-registrations/install/<client id>

公共客户端不需要身份认证。这意味着 JavaScript 适配器可以通过以上 URL 直接从 Keycloak 加载客户端配置。

OIDC 动态客户端注册

终端在 Keycloak 中注册客户端 /auth/realms/<realm>/clients-registrations/openid-connect[/<client id>]

在 OIDC 发现中可以为 realm 找到终端,/auth/realms/<realm>/.well-known/openid-configuration

客户端注册策略

Keycloak 当前支持两种方式注册客户端(通过客户端注册服务)。

  • 认证请求 —— 注册客户端请求要么包含初始化访问令牌,要么包含 Bearer 令牌
  • 匿名请求 —— 注册客户端不需要包含任何令牌

匿名客户端注册请求是非常有趣和强大的功能,任何人都可以注册客户端并且没有限制。因此提出了客户端注册策略 SPI,它提供了一个限制的方式(谁能注册,在什么条件下)。

在 Keycloak 超级管理员控制台中,你可以看到匿名请求策略配置和认证请求策略配置。

当前支持的策略:

  • Trusted Hosts Policy —— 可以配置信任的 host 和域名。默认的,没有白名单 host,所以匿名客户端注册实际上是禁用的。
  • Consent Required Policy —— 新注册的客户端 Consent Allowed 开关是启动的。所以身份认证成功后,用户将看到准许会话(如果需要的话)。
  • Protocol Mapper Policy —— 允许配置协议映射实现白名单。
  • Client Scope Policy —— 允许 Client Scopes 白名单,用于新注册的客户端或者更新的客户端。
  • Full Scope Policy —— 新注册的客户端 Full Scope Allowed开关是关闭的。意味这这些客户端没有任何 realm 角色或者客户端角色。
  • Max Clients Policy —— 如果注册的客户端数量在 realm 中大于或等于设定值将被驳回。默认值 200。
  • Client Disabled Policy —— 新注册客户端是禁用的。意味着超级管理员需要手动通过和启用新注册的客户端。这个策略默认不启用。

管理客户端

客户端是用户请求身份认证的实体。客户端有两种格式。第一种是单点登录的(SSO)。另一种是类型是获取访问令牌然后代表用户访问服务。

OIDC 客户端

创建 OIDC 客户端
  1. 客户端列表

  2. 添加客户端,Client ID 是客户端的身份标识。下一步选择客户端协议 openid-connect

    Client ID
    数据字母字符串,用于客户端身份识别(当 OIDC 请求时)。

    Name
    客户端名称。

    Description
    客户端描述。

    Enabled
    如果关闭,客户端将不允许请求验证。

    Consent Required
    如果打开,用户将得到一个准许页面(用于询问用户是否授权应用程序访问)。页面同时显示客户端要访问的元数据信息,用户可以看到客户端要访问的信息。

    Access Type
    OIDC 客户端类型。

    • confidential:机密访问类型用于服务端客户端(需要执行浏览器登录和需要客户端密码)。这个类型用于服务端应用程序。
    • public:Public 访问类型是客户端类型客户端(需要执行浏览器登录)。客户端类型应用程序没有安全保存秘密的方式。相反,通过为客户端配置正确的重定向 URI 来限制访问非常重要。
    • bearer-only:Bearer-only 访问类型意味着应用程序仅允许 bearer 令牌请求。如果打开这个,应用程序不能参与浏览器登录。

    Standard Flow Enabled
    如果打开这个,客户端将使用 OIDC 授权码工作流。

    Implicit Flow Enabled
    如果打开这个,客户端将使用 OIDC 隐式工作流。

    Direct Access Grants Enabled
    如果打开这个,客户端将使用 OIDC 直接访问授权。

    OAuth 2.0 Device Authorization Grant Enabled
    如果打开这个,客户端将使用 OIDC 设备授权许可。

    OpenID Connect Client Initiated Backchannel Authentication Grant Enabled
    如果打开这个,客户端将使用 OIDC 客户端初始化后端渠道认证许可。

    Root URL
    如果 Keycloak 不使用任何相对 URL,这个值就是预留值。

    Valid Redirect URIs
    这个是必填字段。输入 URL 模版然后点击 + 号添加。点击 - 号移除 URL。记住,最后还要点击 Sava 按钮。通配符 * 只能用于 URI 的末端,例如:http://host.com/*

    注册重定向 URL 模版时,你应该考虑避免被攻击。

    Base URL
    如果 Keycloak 要链接客户端,这个值需要设置。

    Admin URL
    为 Keycloak 指定客户端适配器,这个值是客户端回调终端。Keycloak 服务将使用这个 URI 回调(比如:推送取消策略、执行后端渠道退出登录、其它超级管理员操作)。对于 Keycloak servlet 适配器来说,这个值是 servlet 应用程序的 root URL。

    Web Origins
    这个设置是以 CORS 为中心。

  3. 保密客户端证书
    如果客户端 access type 设置为 confidential 时,页面将会显示 Credentials 标签。注意,选择 Confidential 标签要保存以后才会能看到 Credentials标签。

    Client Authenticator 下拉框指定你的加密客户端证书类型。默认是 Client Id and Secret。secret 自动生成,并且 Regenerate Secret 按钮可以重新生成 secret。

    此外,可以选择 Signed Jwt 或者 X509 Certificate 验证代替 secret。

    Signed JWT

    当选择 Signed Jwt 类型时,你需要为客户端生成私钥和证书。私钥用于 JWT 签名,证书用于服务端验证签名。点击 Generate new keys and certificate 按钮生成私钥和证书。

    也可以使用其它工具生成,然后导入。

  4. 新建 Spring-boot 项目

  5. 创建 Keycloak 客户端,导航到 http://127.0.0.1:10010/,切换到 Demo realm ,点击 Clients 菜单,点击 Create 按钮,创建一个 Demo realm 下的客户端。客户端 ID 为:spring-boot-toy。选择 Access Typeconfidential

  6. 创建角色 toy-admin,并给用户赋予角色


  7. Spring 项目添加 Maven 引用

    <?xml version="1.0" encoding="UTF-8"?>
     <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
         <modelVersion>4.0.0</modelVersion>
         <parent>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-parent</artifactId>
             <version>2.3.10.RELEASE</version>
             <relativePath/> <!-- lookup parent from repository -->
         </parent>
         <groupId>com.toy.keycloak</groupId>
         <artifactId>toy-keycloak</artifactId>
         <version>0.0.1-SNAPSHOT</version>
         <name>toy-keycloak</name>
    
         <properties>
             <java.version>1.8</java.version>
         </properties>
         <dependencies>
             <dependency>
                 <groupId>org.keycloak</groupId>
                 <artifactId>keycloak-spring-boot-starter</artifactId>
             </dependency>
    
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-devtools</artifactId>
                 <scope>runtime</scope>
                 <optional>true</optional>
             </dependency>
             <dependency>
                 <groupId>org.projectlombok</groupId>
                 <artifactId>lombok</artifactId>
                 <optional>true</optional>
             </dependency>
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-starter-test</artifactId>
                 <scope>test</scope>
                 <exclusions>
                     <exclusion>
                         <groupId>org.junit.vintage</groupId>
                         <artifactId>junit-vintage-engine</artifactId>
                     </exclusion>
                 </exclusions>
             </dependency>
         </dependencies>
    
         <dependencyManagement>
             <dependencies>
                 <dependency>
                     <groupId>org.keycloak.bom</groupId>
                     <artifactId>keycloak-adapter-bom</artifactId>
                     <version>13.0.0</version>
                     <type>pom</type>
                     <scope>import</scope>
                 </dependency>
             </dependencies>
         </dependencyManagement>
    
         <build>
             <plugins>
                 <plugin>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-maven-plugin</artifactId>
                     <configuration>
                         <excludes>
                             <exclude>
                                 <groupId>org.projectlombok</groupId>
                                 <artifactId>lombok</artifactId>
                             </exclude>
                         </excludes>
                     </configuration>
                 </plugin>
             </plugins>
         </build>
    
     </project>
    
    
  8. 暴露 Api 接口

     package com.toy.keycloak.webapi;
    
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RequestMapping;
     import org.springframework.web.bind.annotation.RestController;
    
     /**
     * @author Zhang_Xiang
     * @since 2021/5/11 16:56:57
     */
     @RestController
     @RequestMapping("temp")
     public class TempController {
    
         @GetMapping("weather")
         public String weather(){
             return "晴天☀️";
         }
    
     }
    
    
  9. 配置

    使用 Tomcat、Undertow、Jetty 不需要额外配置。在 application.properties 中配置 Keycloak 如下:

     keycloak.realm=demo  # realm 名称
     keycloak.auth-server-url=http://127.0.0.1:10010/auth   # Keycloak 基础服务地址
     keycloak.ssl-required=external
     keycloak.resource=spring-boot-toy   # 应用程序客户端 ID
     keycloak.credentials.secret=91437668-b8f8-425b-ba9d-38439115dfbc
     keycloak.use-resource-role-mappings=true
     keycloak.securityConstraints[0].authRoles[0]=toy-admin
     keycloak.security-constraints[0].securityCollections[0].patterns[0]=/*
    

    设置 keycloak.enabled = false 可以停用 Keycloak 。

  10. 启动 Spring 应用程序,并访问 http://localhost:8080/temp/weather

    输入用户名 zhang,密码123456

  11. 管理登录用户会话,用户登录后,进入 Keycloak 管理后台,切换到 Demo realm 可以看到目前登录的会话列表。

    点击 Logout all 按钮,所有用户都将退出登录。

使用外部数据库

Keycloak 内嵌了 H2 内存数据库。Keycloak 默认使用 H2 持久化数据。H2 数据库不适用于高并发场景并且不适用于集群。

Keycloak 使用两层技术持久化关系数据。底层技术是 JDBC。JDBC 用于连接 RDBMS。每个数据库提供商都有不同的 JDBC 驱动。顶层技术用于持久化的是 Hibernate JPA。

用户存储 SPI

使用用户存储 SPI 扩展 Keycloak 以连接外部用户数据和证书存储。当 Keycloak 运行时查找用户时,比如用户登录,Keycloak 执行几个步骤定位用户。首先看用户是在在用户缓存中,然后在本地数据库中查找,如果找不到,循环用户存储 SPI 执行查找。

用户存储 SPI 提供商实现打包和部署和 Java EE 组件相似。默认不启用组件,如果要启用需要在 Keycloak 超级管理员控制台界面中配置 User Feberation

打包和部署

用户存储提供商打包为 JAR 然后部署到 Keycloak 运行时或者从 Keycloak 运行时取消部署,就像 WildFly 应用程序服务部署服务一样。你也可以直接拷贝 JAR 包到 standalone/deployments/ 服务器目录,或者使用 JBoss CLI 执行部署。

为了 Keycloak 能识别服务提供商,你需要添加一个文件到 JAR 包中:META-INF/services/org.keycloak.storage.UserStorageProviderFactory。这个文件必须包含实现 UserStorageProviderFactory 类的全路径:

org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory

自定义用户存储 Provider

使用已有用户数据库。

MysqlUserStorageProvider 实现了很多接口,实现 UserStorageProvider 是实现是 SPI 的基本要求,换句话说,不实现 UserStorageProvider 就不能实现 SPI,UserLookupProvider 用于实现从外部数据库查找用户以实现用户登录,UserQueryProvider 定义复杂查询以查找用户,CredentialInputValidator 验证不同的证书类型(比如:验证登录密码)。

  1. provider

    public class MysqlUserStorageProvider implements UserLookupProvider, UserQueryProvider, CredentialInputValidator, UserStorageProvider {
    
        protected KeycloakSession session;
        protected ComponentModel model;
    
        public MysqlUserStorageProvider(KeycloakSession session, ComponentModel model) {
            this.session = session;
            this.model = model;
        }
    
        ...
    }
    
    
  2. ProviderFactory,这里的 ProviderConfigProperty 用于自定义标签,换句话说,在这里定义了标签以后,可以在 Keycloak 管理界面设置值,代码可以直接读取到这些值。添加自定义 SPI 时,要留意添加在什么 realm 下。

    public class MysqlUserStorageProviderFactory implements UserStorageProviderFactory<MysqlUserStorageProvider> {
    
    protected final List<ProviderConfigProperty> configMetadata;
    
    public MysqlUserStorageProviderFactory() {
        configMetadata = ProviderConfigurationBuilder.create()
                .property()
                .name(CONFIG_KEY_JDBC_DRIVER)
                .label("JDBC Driver Class")
                .type(ProviderConfigProperty.STRING_TYPE)
                .defaultValue("org.h2.Driver")
                .helpText("Fully qualified class name of the JDBC driver")
                .add()
                .property()
                .name(CONFIG_KEY_JDBC_URL)
                .label("JDBC URL")
                .type(ProviderConfigProperty.STRING_TYPE)
                .defaultValue("jdbc:h2:mem:customdb")
                .helpText("JDBC URL used to connect to the user database")
                .add()
                .property()
                .name(CONFIG_KEY_DB_USERNAME)
                .label("Database User")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("Username used to connect to the database")
                .add()
                .property()
                .name(CONFIG_KEY_DB_PASSWORD)
                .label("Database Password")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("Password used to connect to the database")
                .secret(true)
                .add()
                .property()
                .name(CONFIG_KEY_VALIDATION_QUERY)
                .label("SQL Validation Query")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("SQL query used to validate a connection")
                .defaultValue("select 1")
                .add()
                .build();
    }
    
    @Override
    public MysqlUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new MysqlUserStorageProvider(session, model);
    }
    
    @Override
    public String getId() {
        return "user-center-provider";
    }
    
    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configMetadata;
    }
    
    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
        try (Connection c = DbUtil.getConnection(config)) {
            System.out.println(config.get(CONFIG_KEY_VALIDATION_QUERY));
            c.createStatement().execute(config.get(CONFIG_KEY_VALIDATION_QUERY));
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
            throw new ComponentValidationException("Unable to validate database connection", ex);
        }
    }
    }
    


  3. 查看用户列表

Spring-boot 使用外部用户认证

  1. application.properties

     keycloak.realm=demo
     keycloak.auth-server-url=http://127.0.0.1:10010/auth
     keycloak.ssl-required=external
     keycloak.resource=spring-boot-toy
     keycloak.credentials.secret=91437668-b8f8-425b-ba9d-38439115dfbc
     keycloak.use-resource-role-mappings=true
     keycloak.verify-token-audience=true
     keycloak.securityConstraints[0].authRoles[0]=user
     keycloak.security-constraints[0].securityCollections[0].patterns[0]=/temp/weather
    
    • resource:客户端 ID
    • use-resource-role-mappings:当设置为 true 时,OIDC Java 适配器将 Token 中查找应用程序级用户角色映射。设置为 false 时,将从 realm 里查找用户角色映射。默认设置为 false。
    • ssl-required:确保所有和 Keycloak 通讯的请求是 HTTPS,生产环境应设置为 all,默认值是 external,即外部请求需要 HTTPS,可选值是:allexternalnone
    • verify-token-audience:设置为 true 时,Bearer Token 进行身份认证时,适配器会验证令牌是否包含客户端名称。启用将改善安全性(推荐)。

Keycloak 角色

一个 Realm 有多个客户端,一个客户端有多个用户。在 Keycloak 有3种角色:

  • Realm Role:全局角色,属于指定 realm。任何客户端都可以访问这个角色,并且把这个角色映射到任何用户。
  • Client Role:属于指定客户端的角色。只能映射到该客户端下的用户。
  • Composite Role:多个角色的组合。

在客户端设置中启用 Service Accounts Enabled,在 demo Realm 中添加 ordinary_user 角色。

Service Account Roles 标签中添加 ordinary_user 角色。

添加该角色以后,切换到 User 标签,可以看到用户中已经关联了角色。

使用数据库用户登录

  1. 添加角色限制 ordinary_user

    keycloak.realm=demo
    keycloak.auth-server-url=http://127.0.0.1:10010/auth
    keycloak.ssl-required=external
    keycloak.resource=spring-boot-toy
    keycloak.credentials.secret=91437668-b8f8-425b-ba9d-38439115dfbc
    keycloak.use-resource-role-mappings=true
    keycloak.verify-token-audience=true
    keycloak.securityConstraints[0].authRoles[0]=toy-admin
    keycloak.security-constraints[0].auth-roles[1]=ordinary_user
    keycloak.security-constraints[0].securityCollections[0].patterns[0]=/temp/weather
    
  2. 启动服务,访问 localhost:8080/temp/weather,输入自定义数据库中的用户、密码,以访问接口。

posted @ 2021-05-17 15:43  Zhang_Xiang  阅读(8494)  评论(4编辑  收藏  举报