Keycloak自定义REST扩展-通过用户属性进行用户搜索
需求背景
项目中用户和组织架构管理都是依托以Keycloak,但是Keycloak内置的用户搜索功能不满足需求,需要根据用户的属性值进行搜索,比如手机号;
Keycloak内置API能力代码走读
看官方文档,貌似提供了search搜索查询
- services\src\main\java\org\keycloak\services\resources\admin\UsersResource.java
- model\jpa\src\main\java\org\keycloak\models\jpa\JpaUserProvider.java
所以只是提供了内置这几个参数的模糊搜索;
所以只能自己去定制扩展了;
官方的定制扩展方案
主要以下三步:
- 实现一个 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
- Run
mvn clean install
command from CLI. This will generate atarget
folder. Under thetarget
folder there will be{project artifact id}-*.jar
file. - Copy that
jar
file to the Keycloakstandalone/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"}]