背景
几年前,基于 Keycloak 实现了微信公众号关注即登录方案,详见《基于 keycloak 的关注公众号即登录功能的设计与实现 - Jeff Tian的文章 - 知乎 》。
该方案通过为 Keycloak 写了一个 Identity Provider 来实现,效果是在 Keycloak 的默认登录页面下面增加了微信登录按钮,这个流程可以通过 https://keycloak.jiwai.win/realms/Brickverse/account/ 进行实际体验:
然而,有些使用 Keycloak 的企业希望将关注微信公众号即登录的方式设置为首选,即在登录页直接展示二维码,而不是需要用户点击一次后再展示。反过来,如果用户希望使用用户名密码来登录,可以再点击一次后切换到用户名密码登录页面。
要实现将微信二维码做为登录页的首选,可以通过 Keycloak 的配置来实现。而要在微信二维码页面设置回到其他登录方式的按钮,就需要一点点编程了,并且得借助 Keycloak.js 来完成整个功能。
在线演示
可以通过 https://keycloak.jiwai.win/realms/hardway/account/#/ 体验,打开链接,点击“登录”按钮:
在打开的登录页面中,首先展示的是微信二维码,即首选微信扫码登录。
如果不想使用微信扫码,可以点击下面的按钮,选择其他的登录方式。
实现步骤
添加“weixin”到身份供应商
这里不详细说明了,因为在之前专门写过,参考:《【继续更新】尝试在 Keycloak 里打通整个微信生态 - Jeff Tian的文章 - 知乎 》。
通过配置将微信二维码页面做为首选登录方式
这一步非常简单,并且将是唯一需要做的一件事(后面会讲解代码,仅为有需要的同学们自己开发时做参考。因为我已经将代码写好了,所以可以直接使用)。通过领域的身份验证部分,找到 Identity Provider Redirector,然后点击设置:
接着,在弹出的对话框中,将别名和 Default Identity Provider 都设置为 weixin。
点击保存之后,你就能看到效果了。在登录页面上直接展示了二维码。二维码下面的按钮,需要一些开发,后面的步骤就是讲解它们的开发细节,尽管做为用户不需要了解,但为了照顾到其他开发同学,并不直接使用我写好的插件,而是想自己开发,那么不妨了解一下我是怎么开发的。
Keycloak.js
需要了解一件事情,就是 Keycloak 服务器上其实有一个 Keycloak js 文件,其路径是 /js/keycloak.js。比如 https://keycloak.jiwai.win/js/keycloak.js 。
我们先在二维码显示页面中引入该 js 文件。
html
接着我们就需要初始化 Keycloak 对象。这一步非常非常重要,因为要设置一些比如像 redirectUri,以及 PKCE 挑战码相关的算法等等。如果初始化不正确,就会导致按钮点击之后报各种错误。
html
再添加一个返回其他登录方式的按钮:
html
注意 onclick 事件,就是调用 keycloak.login方法,但需要传入参数。 redirectUri 是指登录之后的回调页面地址,我这里设置为用户账号页面,比如: https://keycloak.jiwai.win/realms/hardway/account 。
另一个参数,idpHint,非常重要。因为我们在前面的步骤中设置了微信是默认的登录方式,所以这里的其他登录方式按钮,不能再回到这个登录方式,就需要通过 idpHint 来指向别的登录方式。这里我写了一个“username”,其实是一个不存在的身份供应商,这导致 Keycloak 渲染了用户名密码登录方式,正是我们需要的!
到这里,就完成了全部的开发,代码详见: https://github.com/Jeff-Tian/keycloak-services-social-weixin/blob/master/src/main/java/org/keycloak/social/weixin/resources/QrCodeResourceProvider.java
java @GET @Path(mp-qr) @Produces(MediaType.TEXT_HTML) public Response mpQrUrl(@QueryParam(ticket) String ticket, @QueryParam(qr-code-url) String qrCodeUrl, @QueryParam(state) String state, @QueryParam(OAUTH2_PARAMETER_REDIRECT_URI) String redirectUri) { logger.info(展示一个 HTML 页面,该页面使用 React 展示一个组件,它调用一个后端 API,得到一个带参二维码 URL,并将该 URL 作为 img 的 src 属性值);
var host = session.getContext().getUri().getBaseUri().toString();
var realmName = session.getContext().getRealm().getName();
var accountRedirectUri = host + /realms/ + realmName + /account;
logger.info(String.format(host is %s, realmName is %s, host, realmName));
var template =
<!DOCTYPE html>
<html>
<head>
<title>QR Code Page</title>
</head>
<body>
<p>请使用微信扫描下方二维码:</p>
<div id=qrCodeContainer>
<img src=%s alt=%s>
<p></p>
<p><button id=login-by-username-password onclick=keycloak.login({ idpHint: username, redirectUri: %s }); type=button>使用密码登录</button></p>
</div>
<script type=text/javascript>
async function fetchQrScanStatus() {
const res = await fetch(mp-qr-scan-status?ticket=%s, {
headers: {
Content-Type: application/json
}
})
const {status, openid} = await res.json()
if (openid) {
window.location.href = %s?openid=${openid}&state=%s
} else {
setTimeout(fetchQrScanStatus, 1000)
}
}
fetchQrScanStatus()
</script>
<script src=/js/keycloak.js type=text/javascript></script>
<script type=text/javascript>
const keycloak = new Keycloak({
url: %s,
realm: %s,
clientId: account-console,
redirectUri: %s
});
keycloak.init({onLoad: check-sso, pkceMethod: S256, promiseType: native});
</script>
</body>
</html>
;
String htmlContent = String.format(template, qrCodeUrl, ticket, accountRedirectUri, ticket, redirectUri, state, host, realmName, accountRedirectUri);
// 返回包含HTML内容的响应
return Response.ok(htmlContent, MediaType.TEXT_HTML_TYPE).build();
}
总结
通过设置 idpHint,可以指定 Keycloak 渲染不同的登录方式。我们先通过配置的方式,设置了微信登录为默认的登录方式。又通过设置 idpHint 为一个不存在的身份供应商,结合 keycloak.js 实现了在微信登录页面回到用户名密码登录方式的按钮。
出于演示,代码写得非常简单粗暴。首先,页面比较简陋,没有写 css 去美化,这是未来的改进方向。其次,在控制器方法里直接返回了一个 html 字符串实现了网页的渲染,而在拼接字符串时直接使用了 String.format 方法,这样做并不是好的工程实践,未来可以改为模板渲染方式。