Keycloak实现手机验证码登录
背景說明
使用Keycloak作為賬號體系的項目中,經常會被問到Keycloak如何實現手機驗證碼登錄,Keycloak有沒有內置的基于短信的登錄實現SMS-based two-/multi-factor-authentication (2FA/MFA) ;
Keycloak目前只內置一種基于 Google Authenticator 的 2FA 選項。
這篇主要討論實現上述的需求的幾種方案,以及相應的優缺點:
-
定制 Authentication SPI,實現Keycloak統一的瀏覽器手機驗證碼登錄;
-
定制 Authentication SPI,實現基于 Resource Owner Password Credentials (Direct Access Grants)的手機驗證碼登錄;
-
使用 token-exchange
-
使用 Identity Providers 實現手機驗證碼登錄
定制 Authentication SPI,實現Keycloak統一的瀏覽器手機驗證碼登錄
原理及實現
這種方式主要的工作原理是:
定制Keycloak登錄主題,點擊發送驗證碼有Keycloak服務進行驗證碼的發送,緩存和驗證;
新增一個 keycloak-2fa-sms-authenticator 驗證器,進行手機號和驗證碼的校驗
具體實現代碼可以參考:
對應的github地址
優缺點
優點是:
- 基于Keycloak標準擴展實現,安全風險可控- 基于瀏覽器登錄,各業務統一的登錄邏輯缺點是:
- 瀏覽器登錄在某些端,比如APP,并不適合- 短信發送集成到Keycloak,各個業務無法再支持自定義模板及信息定義;基于 Resource Owner Password Credentials (Direct Access Grants)的手機驗證碼登錄
Direct Access Grants概念
Resource Owner Password Credentials Grant (Direct Access Grants)
This is referred to in the Admin Console as Direct Access Grants. This is used by REST clients that want to obtain a token on behalf of a user. It is one HTTP POST request that contains the credentials of the user as well as the id of the client and the client’s secret (if it is a confidential client). The user’s credentials are sent within form parameters. The HTTP response contains identity, access, and refresh tokens.
相關的 RESTApi請求
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=server-admin' \ --data-urlencode 'client_secret=ee0c1f08-775d-4195-b5ca-19eb9b923822' \ --data-urlencode 'username=admin1' \ --data-urlencode 'password=123456' \ --data-urlencode 'grant_type=password'在后臺的 Keycloak 的Flow中:
去掉Password 的Requirement 為 ALTERNATIVE即可
curl --location --request POST 'http://localhost:8018/auth/realms/austin-local/protocol/openid-connect/token' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'client_id=austin-app1' --data-urlencode 'client_secret=d622f435-4aad-48b7-bf18-e3cba0e4d76a' --data-urlencode 'username=admin1' --data-urlencode 'grant_type=password'可以在client中單獨設置Flow
優缺點
優點:
-
驗證碼的發送和校驗完全由業務方控制
-
業務方可以很方便的進行拓展,不管是基于手機號驗證碼,還是郵箱驗證碼;
缺點:
-
由于去掉了用戶的密碼校驗,所以client獲取用戶令牌的安全級別下降,需要很小心的控制 client 是否開啟 Direct Access Grants,以及client對應的scope;
-
手機號對應的用戶名,需要業務方自行保存,對應于手機號保存在業務方數據庫的實現是方便,但是如果把手機號放在Keycloak的User的attribute中則還需要額外的定制修改。
實現手機號驗證登錄
定制ValidateUserAttributeAuthenticator
該校驗器主要實現 Direct Access Grant 校驗用戶 的幾種方式
-
指定用戶名校驗用戶
-
指定用戶郵箱校驗用戶
-
根據全局設定的 屬性名校驗用戶
-
根據請求參數指定的屬性名校驗用戶
-
根據默認的屬性名 phone 校驗用戶
可能出現的報錯:
- 未指定用戶名,用戶郵箱,以及屬性值,會提示"Missing parameter: username"
- 指定的用戶名或者郵箱,找到多個用戶,提示: “Invalid user credentials”
- 提供的屬性名對應的屬性值找到多個用戶,提示: “Invalid user credentials is not unique”
- 提供的信息,找不到對應的用戶,提示: “Invalid user credentials”
完整的authenticate 函數如下
@Override public void authenticate(AuthenticationFlowContext context) {String username = retrieveUsername(context);AuthenticatorConfigModel config = context.getAuthenticatorConfig();String attributeName = null;if (config.getConfig() != null) {attributeName = config.getConfig().get("attributeName");} else {attributeName = retrieveAttributeValue(context, "attributeName");}if (attributeName == null) {attributeName = "phone";}String attributeValue = retrieveAttributeValue(context, attributeName);if (username == null && attributeValue == null) {context.getEvent().error(Errors.USER_NOT_FOUND);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Missing parameter: username");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}UserModel user = null;if (username != null) {try {user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);} catch (ModelDuplicateException mde) {logger.error(mde);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Invalid user credentials");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}}// find User By attributeif (user == null) {List<UserModel> users = context.getSession().users().searchForUserByUserAttributeStream(context.getRealm(), attributeName, attributeValue).collect(Collectors.toList());if (users.size() > 1) {logger.error(new ModelDuplicateException("User with " + attributeName + "=" + attributeValue + " is not unique!"));Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Invalid user credentials is not unique");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}if (users.size() == 1) {user = users.get(0);}}if (user == null) {context.getEvent().error(Errors.USER_NOT_FOUND);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}context.getEvent().detail(Details.USERNAME, user.getUsername());context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, user.getUsername());context.setUser(user);context.success(); enticator.ATTEMPTED_USERNAME, user.getUsername());context.setUser(user);context.success(); }指定屬性值名稱的邏輯
配置中的屬性名稱優先級最高,考慮的原則是管理員設置的安全性最高;
請求參數中的 “attributeName” 的值;
默認的 “phone”;
打包部署Provider
- META-INF.services 添加提供器信息
- maven 編譯
- 部署Jar包
把生成target下的jar包,拷貝到 $KC_HOME/standalone/deployments 目錄下
keycloak啟動時會自動解析加載該提供器
- 確認安裝成功
登錄Keycloak 管理控制臺,在右上角下拉菜單中,選擇 Server Info
查看Providers, 搜索我們的提供器ID,查看是否存在
定制校驗流程并綁定
- 在Authentication的Flows中選擇Direct Grant,并進行拷貝,命名為Direct Grant User Attribute
-
刪除原有的Username Validation
-
添加執行器,選擇我們定制的
添加后點擊左側箭頭,移動到最頂部
-
配置屬性名
-
全局綁定
-
客戶端作用域綁定
測試驗證
創建好兩個測試用戶,對應的屬性值如下
- admin1
其中 兩個 mobile的屬性值一樣;
phone 正確
phone 找不到
修改配置,指定屬性名為openid
刪除配置,就可以通過 attributeName 指定業務自己的屬性名
小結
到這里基本上就實現了手機驗證碼登錄的需求,這里的發送驗證碼,校驗驗證由各個可信業務方進行處理。
我們根據客戶端的是否開啟direct Grant,客戶端scope,以及客戶端綁定校驗流程,嚴格控制相關權限的發放。
總結
以上是生活随笔為你收集整理的Keycloak实现手机验证码登录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android clippling使用
- 下一篇: 菜鸟后端程序员花了两天半模仿写出了赶集网