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"}]
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Django-rest-framework是一个用于构建Web API的强大框架,它提供了许多有用的工具和库,可以帮助我们轻松地构建出一个安全可靠的用户注册和登录系统。 下面是一个简单的Django-rest-framework用户注册与登录的实现: 首先,我们需要安装Django-rest-framework: ``` pip install djangorestframework ``` 接着,我们需要在settings.py文件中添加以下配置: ```python INSTALLED_APPS = [ # ... 'rest_framework', ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], } ``` 上面的配置将启用TokenAuthentication身份验证和IsAuthenticated权限,这将确保只有已登录的用户才能访问我们的API。 现在,我们可以创建一个名为"users"的Django应用程序,并定义以下模型: ```python from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): pass ``` 接着,我们需要定义序列化器来将User模型转换为JSON格式: ```python from rest_framework import serializers from .models import User class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ('username', 'email', 'password') extra_kwargs = {'password': {'write_only': True}} def create(self, validated_data): user = User.objects.create_user( username=validated_data['username'], email=validated_data['email'], password=validated_data['password'] ) return user ``` 上面的代码定义了一个UserSerializer序列化器,将User模型转换为JSON格式。我们使用Meta类来指定模型和要序列化的字段,以及一些额外的参数。在create方法中,我们使用create_user方法创建新用户。 现在,我们可以定义视图来处理用户注册和登录请求: ```python from rest_framework import generics, permissions, status from rest_framework.response import Response from rest_framework.authtoken.models import Token from rest_framework.views import APIView from django.contrib.auth import authenticate, login from .models import User from .serializers import UserSerializer class RegisterView(generics.CreateAPIView): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [permissions.AllowAny] def post(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) class LoginView(APIView): permission_classes = [permissions.AllowAny] def post(self, request): username = request.data.get('username') password = request.data.get('password') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) token, created = Token.objects.get_or_create(user=user) return Response({'token': token.key}) else: return Response({'error': 'Invalid credentials'}) ``` 上面的代码定义了RegisterView和LoginView视图。RegisterView视图处理用户注册请求,LoginView视图处理用户登录请求。我们在视图中使用UserSerializer序列化器来验证输入数据,并使用TokenAuthentication身份验证来保护API。 现在我们已经完成了用户注册和登录的实现。可以使用POST请求来测试我们的API: - 用户注册: ``` POST /api/register/ { "username": "testuser", "email": "testuser@example.com", "password": "testpassword" } ``` - 用户登录: ``` POST /api/login/ { "username": "testuser", "password": "testpassword" } ``` 如果登录成功,API将返回一个包含Token身份验证密钥的JSON响应。现在,我们可以将此密钥用于所有受保护的API请求中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值