iOS Developer的全棧之路 - Keycloak(9)

這一節(jié)我們來看一看Keycloak的Authentication SPI。先來說說我們?yōu)槭裁葱枰?dāng)我們使用Keycloak進(jìn)行登錄注冊(cè)的時(shí)候,默認(rèn)設(shè)置下都是通過web頁(yè)面完成的,流程是相對(duì)固定的,當(dāng)然也有一些可配置項(xiàng),例如OTP。這樣會(huì)帶來什么問題呢?

  1. 當(dāng)我們想通過Rest請(qǐng)求來完成登錄注冊(cè)過程。登錄,可以通過第二節(jié)中的方式進(jìn)行;注冊(cè)相對(duì)來說就比較麻煩了,需要一個(gè)搭配另一個(gè)server,再配合第五節(jié)中Admin API來進(jìn)行。但這樣的方式過于繁瑣,以至于會(huì)開始懷疑為什么還需要Keycloak。
  2. 當(dāng)我們想要自定義一些登錄注冊(cè)的流程時(shí),比如想通過短信驗(yàn)證碼進(jìn)行登錄。

Authentication Flow

解決這兩個(gè)問題的方式就是Authentication SPI,它可以用來擴(kuò)展或是替代已有的認(rèn)證流程,通過下圖,看看已有的流程都有哪些:
authentication bindings.png

Browser Flow:使用瀏覽器登錄的流程;
Registration Flow:使用瀏覽器注冊(cè)的流程;
Direct Grant Flow第二節(jié)介紹的通過Post請(qǐng)求獲取token的流程;
Reset Credentials:使用瀏覽器重置密碼的流程;
Client Authentication:Keycloak保護(hù)的server的認(rèn)證流程。

右側(cè)的下拉列表中可以選擇相應(yīng)的流程,這些流程的定義如下圖所示,我們以Browser為例進(jìn)行解釋:

browser flow.png

先來解釋一下圖中的表格,它定義了通過瀏覽器完成登錄操作所經(jīng)歷的步驟/流程。其中又包含了兩個(gè)Column,Auth Type和Requirement,Auth Type中Cookie,Kerberos,Identity Provider Redirector和Forms是同一級(jí)的流程,而Username Password Form和Browser - Conditional OTP是Forms的子流程,同理,Condition - User Configured 和 OTP Form又是Browser - Conditional OTP的子流程。換一種方式來理解一下:

[
  Cookie,
  Kerberos,
  Identity Provider Redirector,
  [ // Forms
    Username Password Form,
    [  // Browser - Conditional OTP
      Condition - User Configured,
      OTP Form
    ]
  ]
]

當(dāng)一個(gè)流程包含子流程時(shí),那么這個(gè)流程就變成了抽象概念了。右側(cè)的Requirement則定義了當(dāng)前流程的狀態(tài),包括 Required,Alternative,Disabled 和 Conditional。對(duì)于同級(jí)流程標(biāo)記為Alternative,則表示在同級(jí)流程中只要有一個(gè)可以完成操作,則不會(huì)再需要其他流程的參與;Require則表示這個(gè)流程是必須的。

接下來,我們來看一下在登錄過程中這個(gè)表格是如何控制整個(gè)流程的。當(dāng)我們從瀏覽器發(fā)起登錄請(qǐng)求時(shí),Keycloak會(huì)首先檢查請(qǐng)求中的cookie,若cookie驗(yàn)證通過,則直接返回登錄成功,而不會(huì)進(jìn)行下面的流程,若cookie驗(yàn)證失敗,則進(jìn)入下一個(gè)流程的驗(yàn)證(Cookie校驗(yàn)是一個(gè)特殊的流程,它無需用戶參與,當(dāng)發(fā)起請(qǐng)求時(shí),即可自發(fā)完成),Kerberos,在Requirement中標(biāo)記該流程為Disabled,將直接跳過。其后的兩個(gè)流程Identity Provider Redirector和Forms(即用戶名密碼登錄),選其一即可,正如之前章節(jié)所展示的demo,用戶可選擇第三方登錄或用戶名密碼登錄。Browser - Conditional OTP 則是一個(gè)可選操作,設(shè)置OTP并通過OTP進(jìn)一步驗(yàn)證用戶身份(Multi-factor)

自定義Authentication SPI

現(xiàn)在,通過一個(gè)demo來演示如何通過自定義Authentication SPI來實(shí)現(xiàn)一個(gè)短信驗(yàn)證碼登錄需求,這里的登錄指的是通過postman發(fā)送一個(gè)post請(qǐng)求來獲取token,如下圖所示:
sms opt request.png

從代碼層面,需要兩個(gè)類:實(shí)現(xiàn)Authenticator接口的SmsOtpAuthenticator 和 實(shí)現(xiàn)AuthenticatorFactory/ConfigurableAuthenticatorFactory接口的SmsOtpAuthenticatorFactory。

從概念上理解,Authenticator就是上面分析的一個(gè)驗(yàn)證流程/步驟,SmsOtpAuthenticatorFactory為工廠類,這樣的搭配和上一節(jié)中User Storage SPI是相同,而這個(gè)demo也是在上一節(jié)的基礎(chǔ)上進(jìn)行的。

Authenticator & AuthenticatorFactory

先來看一下SmsOtpAuthenticator,它的主要邏輯都集中在authenticate方法中,當(dāng)發(fā)起request token請(qǐng)求時(shí),將通過此方法要校驗(yàn)參數(shù)的合法性。通過context的getHttpRequest便可request對(duì)象,再?gòu)闹蝎@取我們期望的參數(shù)。在這里我們做了一個(gè)mock短信驗(yàn)證碼,假設(shè)合法otpId123otpValue1111,當(dāng)驗(yàn)證通過后,再?gòu)?code>session.users()中通過username獲取UserModel,最后將獲取的userModel賦給當(dāng)前context,并調(diào)用context.success()。期間有任何異常都將調(diào)用context.failure(...)退出當(dāng)前認(rèn)證流程。

public class SmsOtpAuthenticator implements Authenticator {
    ...
    public void authenticate(AuthenticationFlowContext context) {
        logger.info("SmsOtpAuthenticator authenticate");
        MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
        String otpId = params.getFirst("otpId");
        String otpValue = params.getFirst("otpValue");
        String username = params.getFirst("username");
        if (otpId == null || otpValue == null || username == null) {
            logger.error("invalid params");
            context.failure(AuthenticationFlowError.INTERNAL_ERROR);
            return;
        }
        // some mock validation, to validate the username is bind to the otpId and otpValue
        if (!otpId.equals("123") || !otpValue.equals("1111")) {
            context.failure(AuthenticationFlowError.INVALID_CREDENTIALS);
            return;
        }

        UserModel userModel = session.users().getUserByUsername(username, context.getRealm());
        if (userModel == null) {
            context.failure(AuthenticationFlowError.INVALID_USER);
            return;
        }
        context.setUser(userModel);
        context.success();
    }

    public boolean requiresUser() {
        return false;
    }

    public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
        return true;
    }
  ...
}

主要的邏輯分析完后,我們?cè)賮砜纯雌渌姆椒ā?br> requiresUser():有些完整流程(例如,Browser)是有多個(gè)步驟/流程共同組成的,其中一步完成后,會(huì)進(jìn)入下一步進(jìn)行驗(yàn)證,而這一步有時(shí)就需要用到上一步中賦值于context中的UserModel,而requiresUser()便表示是否需要上一步中的UserModel。
configuredFor(...):表格中的Requirement標(biāo)記了當(dāng)前流程的狀態(tài),當(dāng)為Conditional時(shí),表示該流程的執(zhí)行與否取決于運(yùn)行時(shí)的判斷,configuredFor便是處理這個(gè)邏輯的。

Factory的實(shí)現(xiàn)相對(duì)就簡(jiǎn)單很多,getId()用于標(biāo)示這個(gè)SPI,getRequirementChoices()用于標(biāo)示這個(gè)流程支持哪些Requirement,create(...)則用于創(chuàng)建SmsOtpAuthenticator,F(xiàn)actory對(duì)于當(dāng)前運(yùn)行的Keycloak是一個(gè)單例,而SmsOtpAuthenticator則在每次請(qǐng)求時(shí),都有機(jī)會(huì)創(chuàng)建一個(gè)新的實(shí)例。

public class SmsOtpAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
...
    private static final String ID = "sms-otp-auth";

    public String getDisplayType() {
        return "SMS OTP Authentication";
    }

    public String getReferenceCategory() {
        return ID;
    }

    public boolean isConfigurable() {
        return true;
    }

    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
        return new AuthenticationExecutionModel.Requirement[] {
                AuthenticationExecutionModel.Requirement.REQUIRED
        };
    }

    public boolean isUserSetupAllowed() {
        return true;
    }

    public String getHelpText() {
        return "Validates SMS OTP";
    }

    public String getId() {
        return ID;
    }

    public Authenticator create(KeycloakSession session) {
        logger.info("SmsOtpAuthenticatorFactory create");
        return new SmsOtpAuthenticator(session);
    }
...
}

Deployment

部署方式和上一節(jié)中的User Storage相同,需要在src/main/resources/META-INF/services目錄下創(chuàng)建org.keycloak.authentication.AuthenticatorFactory文件,并在其中添加SmsOtpAuthenticatorFactory的包名:

com.iossocket.SmsOtpAuthenticatorFactory

在通過mvn package進(jìn)行打包,放置于standalone/deployments目錄下。再通過admin console配置SmsOtpAuthenticator,步驟如下所示:

  1. 創(chuàng)建新的流程容器
    create new flow.png
  2. 為新創(chuàng)建的流程容器起一個(gè)別名
    create top level form.png
  3. 選擇剛創(chuàng)建好的流程容器,并添加一個(gè)execution
    add execution.png
  4. 將原先的Direct Grant Flow改為新的流程容器,并選中Required
    change existing binding.png

測(cè)試

此時(shí)再通過postman發(fā)起請(qǐng)求時(shí),即可獲得token。http://localhost:8080/auth/realms/demo/protocol/openid-connect/token

sms opt request.png

源碼可詳見:https://github.com/iossocket/userstorage

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容