【问题与需求重述】
现有多个系统,其分别拥有自己的用户数据库,希望将所有系统中的用户体系去掉,统一使用 Keycloak 来管理。将单个系统的用户登录改造成使用 Keycloak,已经完成。首先,参考《(使用 Keycloak 接管 SpringBoot 应用的用户认证功能 - Jeff Tian的文章 - 知乎)[https://zhuanlan.zhihu.com/p/587831808] 》,使用一个用户,验证了端到端的可行性;然后,参考《往 Keycloak 里批量添加用户 - Jeff Tian的文章 - 知乎 》将系统一中的所有用户都迁移到了 Keycloak。
现在面临的问题是,对于第二个系统,以及后续更多的其他系统,不能直接迁移用户了,因为本质上,还是同一批用户,他们已经从系统一中,迁移到了 Keycloak。只是这同一批用户,在系统二中的用户名,和在系统一中的用户名不一样而已。即,系统二的用户,其实已经在 Keycloak 里了,只是不能通过用户名关联上。也就是说,在系统二中做登录改造时,需要使用另一种方式来查找用户。
登录本质上分两步,第一步根据某种方式查找用户是否存在,第二步验证提供的凭据是否匹配。对于目前的情况,第一步不能使用用户名,但第二步还是一样的。
思路
虽然同一个用户在系统一和系统二中用户名不相同,但这两个系统中都保存了用户的手机号。我们可以根据用户的手机号来做关联。即,在系统二做登录时,根据用户输入的用户名,查找到手机号。然后,去 Keycloak 中,用手机号查找到该用户,然后,就可以取到该用户在 Keycloak 中的用户名了。后面就是和用户名密码登录一样了。
Keycloak 冷知识
自定义属性
Keycloak 为用户定义了自定义属性字典,可以存储自定义的键值对。我们可以利用该知识,在做批量迁移用户时,将用户的手机号存储在这个自定义属性字典里。当然,迁移完毕后,也可以继续向已有用户插入任意的自定义属性。如果通过界面来查看,是这样的:
使用自定义属性查询用户
Keycloak 的 Admin API 提供了查询用户接口,文档见: https://www.keycloak.org/docs-api/15.1/rest-api/index.html#_users_resource。查询方式有很多,比如根据 email 查询、根据用户的姓氏来查等等。该接口还提供了一个 url 查询字符串 q,可以根据自定义属性来查询用户:
该接口返回的数据如下:
json [ { id: 71792e89-1d72-4509-b19f-ab19b35adb61, createdTimestamp: 1590325541253, username: [email protected], enabled: true, totp: false, emailVerified: true, firstName: Jeff, lastName: Tian, email: [email protected], attributes: { saml.persistent.name.id.for.jeff-tian-gitlab-wq766wrh9r57-8929.githubpreview.dev: [ G-6533c9f6-0700-4dce-9080-50ea65ed5b72 ], phone: [ 12345678901 ], saml.persistent.name.id.for.jeff-tian-gitlab-vjv44x636w5v-8929.githubpreview.dev: [ G-214ff264-b530-49da-8181-a23f6fb2d09d ], saml.persistent.name.id.for.uniheart-gitlab: [ G-28155cbf-3d55-4226-8851-42e7d7f0806d ] }, disableableCredentialTypes: [], requiredActions: [], notBefore: 0, access: { manageGroupMembership: true, view: true, mapRoles: true, impersonate: true, manage: true } } ]
实现
在前面两篇文章的基础上,最主要的是在示例 SpringBoot 项目中的 /login-by-username-password�接口基础上,增加一个 /login-by-attribute方法,模拟系统二的登录改造。
接口设计
在系统二中,用户输入的是系统二中的用户名和密码。系统二在自己的库中查找到手机号后,后续的步骤,我们使用 /login-by-attribute接口模拟,它使用上面介绍的自定义属性去 Keycloak 里查找用户,并取出用户名,然后复用前面的用户名密码登录。所以该接口接收两个参数:attr和 password。为了简单,接口使用了 GET 方法,在真实项目里,不要这样做!
入口
java @GetMapping(value=/login-by-attribute) @ResponseBody public KeycloakAccessTokenPayload loginByAttribute(String attr, String password) throws IOException { return new KeycloakHelper(new OkHttpClient().newBuilder().build()).getUserTokenByAttributeAndPassword(attr, password); }
关键代码
java public KeycloakAccessTokenPayload getUserTokenByAttributeAndPassword(String attr, String password) throws IOException { var url = https://keycloak.jiwai.win/auth/admin/realms/UniHeart/users?q= + attr; var request = new Request.Builder().url(url).method(GET, null).addHeader(Authorization, java.lang.String.format(Bearer %s, getAdminAccessToken().access_token)).build(); var response = client.newCall(request).execute(); var s = Objects.requireNonNull(response.body()).string(); var users = JsonHelper.parseUsersFrom(s);
if(users.length == 0) {
return null;
}
return getUserTokenByPassword(users[0].username, password);
}
完整提交见: https://github.com/Jeff-Tian/keycloak-springboot/commit/c6dfbbadf809fa575fcad462a7dd8b9bf4ea2e5d。
测试效果
以上面的测试用户为例,其有一个自定义属性,phone,其值为 12345678901 。调用新加的 /login-by-attribute,结果如下:
成功拿到用户的登录结果。