Keycloak自定义REST扩展-通过用户属性进行用户搜索

Keycloak自定义REST扩展-通过用户属性进行用户搜索

需求背景

项目中用户和组织架构管理都是依托以Keycloak,但是Keycloak内置的用户搜索功能不满足需求,需要根据用户的属性值进行搜索,比如手机号;

Keycloak内置API能力代码走读

Keycloak 官方文档

在这里插入图片描述
在这里插入图片描述

看官方文档,貌似提供了search搜索查询

  • services\src\main\java\org\keycloak\services\resources\admin\UsersResource.java
    在这里插入图片描述
  • model\jpa\src\main\java\org\keycloak\models\jpa\JpaUserProvider.java
    在这里插入图片描述

所以只是提供了内置这几个参数的模糊搜索;
所以只能自己去定制扩展了;

官方的定制扩展方案

Keycloak定制开发文档

主要以下三步:

  • 实现一个 KeyCloakUserApiProviderFactory
  • 实现 KeyCloakUserApiProvider进行自己的业务逻辑书写
  • 在src\main\resources\META-INF\services\org.keycloak.services.resource.RealmResourceProviderFactory文件注册自己的提供器

本文参考的代码为:
https://dev.to/silentrobi/keycloak-custom-rest-api-search-by-user-attribute-keycloak-3a8c

在此基础上:

  • 去除了 userMapper的映射,因为我需要全量的用户信息
  • 实现了自己的Representation映射
  • 添加了 briefRepresentation 特性
  • 添加 token校验和角色校验

代码示意截图

  • src\main\java\keycloak\apiextension\KeyCloakUserApiProviderFactory.java
public class KeyCloakUserApiProviderFactory implements RealmResourceProviderFactory {
    public static final String ID = "userapi-rest";

    public RealmResourceProvider create(KeycloakSession session) {
        return new KeyCloakUserApiProvider(session);
    }

    public void init(Scope config) {
    }

    public void postInit(KeycloakSessionFactory factory) {
    }

    public void close() {
    }

    public String getId() {
        return ID;
    }
}
  • src\main\java\keycloak\apiextension\KeyCloakUserApiProvider.java
public class KeyCloakUserApiProvider implements RealmResourceProvider {
    private final KeycloakSession session;
    private final AuthenticationManager.AuthResult auth;
    private final String defaultAttr = "merchent_id";

    public KeyCloakUserApiProvider(KeycloakSession session) {
        this.session = session;
        this.auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
    }

    public void close() {
    }

    public Object getResource() {
        return this;
    }

    @GET
    @Path("users/search-by-attr")
    @NoCache
    @Produces({ MediaType.APPLICATION_JSON })
    @Encoded
    public List<UserRepresentation> searchUsersByAttribute(@DefaultValue(defaultAttr) @QueryParam("attr") String attr,
            @QueryParam("value") String value, @QueryParam("briefRepresentation") Boolean briefRepresentation) {
                checkRealmAdmin();
        boolean briefRepresentationB = briefRepresentation != null && briefRepresentation;
        RealmModel realm = session.getContext().getRealm();
        Stream<UserModel> userModels = session.users()
                .searchForUserByUserAttributeStream(session.getContext().getRealm(), attr, value);

        return userModels.map(user -> {
            UserRepresentation userRep = briefRepresentationB ? ModelToRepresentation.toBriefRepresentation(user)
                    : ModelToRepresentation.toRepresentation(session, realm, user);
            return userRep;
        }).collect(Collectors.toList());
    }

    private void checkRealmAdmin() {
        if (auth == null) {
            throw new NotAuthorizedException("Bearer");
        } else if (auth.getToken().getRealmAccess() == null || !auth.getToken().getRealmAccess().isUserInRole("admin")) {
            throw new ForbiddenException("Does not have realm admin role");
        }
    }
}

Installation

  1. Run mvn clean install command from CLI. This will generate a target folder. Under the target folder there will be {project artifact id}-*.jar file.
  2. Copy that jar file to the Keycloak standalone/deployments/ directory. For an example, If you run your Keycloak in docker container, you can use the following command:
docker cp <jar_file_path> keycloak:/opt/jboss/keycloak/standalone/deployments/

测试验证

获取访问令牌

 curl --location --request POST 'http://localhost:8080/auth/realms/austintest/protocol/openid-connect/token' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'client_id=admin-cli' --data-urlencode 'username=admin1' --data-urlencode 'password=123456' --data-urlencode 'grant_type=password' 

不带令牌访问

Request:

curl --location --request GET 'http://localhost:8080/auth/realms/austintest/userapi-rest/users/search-by-attr?attr=phone&value=14255633'

Response:
返回无权限

{"error":"HTTP 401 Unauthorized"}

不带admin角色的用户

Request:

 curl --location --request GET 'http://localhost:8080/auth/realms/austintest/userapi-rest/users/search-by-attr?attr=merchant_id&value=1' --header 'Authorization: Bearer eyJhbGciOiJSUInR5cCIgOiAiSldUIiwia2lkIiA6ICJOdHBtTmtOV1dBLWs4TlNTeWpSZHBLX0RMLUdLVFR2WXdsTndPdzM5VXJRIn0.eyJleHAiOjE2MzQ2MzgxMjksImlhdCI6MTYzNDYzNzgyOSwianRpIjoiNDIyODBkYzgtOTkyNS00OGE4LWI4MTYtODcA1ZWZmIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2F1c3RpbnRlc3QiLCJzdWIiOiI5Y2M5MjBhZC1jYjQ1LTQ4MzAtYmIxYS1kYTVmZDM0NWQ3Y2IiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLaW9uX3N0YXRlIjoiOTNlNWZiMWItMzEzOC00NmQ4LTg2ZTctNGZkZmQxNzY1MTcwIiwiYWNyIjoiMSIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6ImFkbWluIGF1c3RpbiIsInByZWZlcnJVybmFtZSI6ImFkbWluMSIsImdpdmVuX25hbWUiOiJhZG1pbiIsImZhbWlseV9uYW1lIjoiYXVzdGluIiwiZW1haWwiOiJhZG1pbjFAcXEuY29tIn0.H2QwOG6LRN-TF1YCVpbaVU7ILd0OVNfCEtDZZ5zZnObArkphgaCd9BaHk9tbcGsH8OR55qUI3S1ZkZim0EHwaWluo9CVrE-orOccs3Tth_awJeOJMtRTBeNr5I5rYGi0aSP1YZEsyxvjigkekP4z82IizPdZjyfs9LjZJEKq5SKxUVL5LIAzfsE99aJp_AAGeITqswTsjkpN3wOZ4TcwEeb-XHMhTekEjAl1fQuE9eshPBe3qMdAXD2eN8mC21KBY8RzZq4bZjdwLAia_a2WxTjFr12pCUSuIVrv5kw2nqoPWxj5I0HHHyIMdUNDWvdc9wz9o2LA'

Response:
返回无权限

{"error":"Does not have realm admin role"}

令牌携带角色正确

Request:

 curl --location --request GET 'http://localhost:8080/auth/realms/austintest/userapi-rest/users/search-by-attr?attr=merchant_id&value=1' --header 'Authorization: Bearer eyJhbGciOiJSUInR5cCIgOiAiSldUIiwia2lkIiA6ICJOdHBtTmtOV1dBLWs4TlNTeWpSZHBLX0RMLUdLVFR2WXdsTndPdzM5VXJRIn0.eyJleHAiOjE2MzQ2MzgxMjksImlhdCI6MTYzNDYzNzgyOSwianRpIjoiNDIyODBkYzgtOTkyNS00OGE4LWI4MTYtODcA1ZWZmIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2F1c3RpbnRlc3QiLCJzdWIiOiI5Y2M5MjBhZC1jYjQ1LTQ4MzAtYmIxYS1kYTVmZDM0NWQ3Y2IiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLaW9uX3N0YXRlIjoiOTNlNWZiMWItMzEzOC00NmQ4LTg2ZTctNGZkZmQxNzY1MTcwIiwiYWNyIjoiMSIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6ImFkbWluIGF1c3RpbiIsInByZWZlcnJVybmFtZSI6ImFkbWluMSIsImdpdmVuX25hbWUiOiJhZG1pbiIsImZhbWlseV9uYW1lIjoiYXVzdGluIiwiZW1haWwiOiJhZG1pbjFAcXEuY29tIn0.H2QwOG6LRN-TF1YCVpbaVU7ILd0OVNfCEtDZZ5zZnObArkphgaCd9BaHk9tbcGsH8OR55qUI3S1ZkZim0EHwaWluo9CVrE-orOccs3Tth_awJeOJMtRTBeNr5I5rYGi0aSP1YZEsyxvjigkekP4z82IizPdZjyfs9LjZJEKq5SKxUVL5LIAzfsE99aJp_AAGeITqswTsjkpN3wOZ4TcwEeb-XHMhTekEjAl1fQuE9eshPBe3qMdAXD2eN8mC21KBY8RzZq4bZjdwLAia_a2WxTjFr12pCUSuIVrv5kw2nqoPWxj5I0HHHyIMdUNDWvdc9wz9o2LA'

Response:

[{"id":"d099df5f-286d-4b77-9911-f30a3da7cffb","createdTimestamp":1627290490426,"username":"测试用户","enabled":true,"totp":false,"emailVerified":false,"firstName":"杭州有限公司","lastName":"名称","email":"austin@qq.com","attributes":{"haha":["hall"],"avatar":["https://qhyxpicoss.kujiale.com/avatars/41.jpg"],"phone":["14255633"]},"disableableCredentialTypes":[],"requiredActions":[],"notBefore":0}]

带briefRepresentation=true参数

Request:

curl --location --request GET 'http://localhost:8080/auth/realms/austintest/userapi-rest/users/search-by-attr?attr=phone&value=14255633&briefRepresentation=true' --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJOdHBtTmtOV1dBLWs4TlNTeWpSZHBLX0RMLUdLVFR2WXdsTndPdzM5VXJRIn0.eyJleHAiOjE2MzQ2Mzg1NTEsImlhdCI6MTYzNDYzODI1MSwianRpIjoiNDQ1ZTIyOTEtMWIyMi00YTVhLThmMjgtYTc1OWQzOTU1NDMxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2F1c3RpbnRlc3QiLCJhdWQiOlsicmVhbG0tbWFuYWdlbWVudCIsImFjY291bnQiXSwic3ViIjoiOWNjOTIwYWQtY2I0NS00ODMwLWJiMWEtZGE1ZmQzNDVkN2NiIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYWRtaW4tY2xpIiwic2Vzc2lvbl9zdGF0ZSI6IjE1ZWQ2MGEwLTQ1YWQtNDE4MC05M2QzLWYzZTMyYzcwNzFjOSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1hdXN0aW50ZXN0Iiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicmVhbG0tbWFuYWdlbWVudCI6eyJyb2xlcyI6WyJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsInZpZXctcmVhbG0iLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsInJlYWxtLWFkbWluIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJhZG1pbiBhdXN0aW4iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbjEiLCJnaXZlbl9uYW1lIjoiYWRtaW4iLCJmYW1pbHlfbmFtZSI6ImF1c3RpbiIsImVtYWlsIjoiYWRtaW4xQHFxLmNvbSJ9.fTRmGM2mZmC_NQC5s_9s79oySWOSPpPsk2GF8lcBgDnV_BO54ebTQmImyzvpfx6RsaWCc1Cba85s5Qx1FKBjmEDYFjjahfojMN3fO2-fxhK5mcqGgTBLk3tZeIA6b_dcSwVjqNZSc9p7tvKGEatpF8Ll58dPGMut0fTr60A7pgo7FV42_9wmX-oAmcwERJqbBqgzIeb_-hdQPz2-NHBAJBb79xTuBrcKBLNhUagbTaIOJNVGmSksaR2G9svsqnhabrPalSOwVfTH5AHg869qbrPy1s-PyxQdyruI4RBL6aHWTXK-pd0wzEwOkdDDlt4Re8dhFyvuQU4VNcAGJ1-mfQ'

Response:

[{"id":"d099df5f-286d-4b77-9911-f30a3da7cffb","createdTimestamp":1627290490426,"username":"测试用户","enabled":true,"emailVerified":false,"firstName":"杭州有限公司","lastName":"名称","email":"austin@qq.com"}]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值