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

這一節(jié)我們來看一看Keycloak的Authentication SPI茵宪。先來說說我們?yōu)槭裁葱枰裕?dāng)我們使用Keycloak進行登錄注冊的時候,默認設(shè)置下都是通過web頁面完成的,流程是相對固定的庞溜,當(dāng)然也有一些可配置項张抄,例如OTP砂蔽。這樣會帶來什么問題呢?

  1. 當(dāng)我們想通過Rest請求來完成登錄注冊過程署惯。登錄左驾,可以通過第二節(jié)中的方式進行;注冊相對來說就比較麻煩了极谊,需要一個搭配另一個server诡右,再配合第五節(jié)中Admin API來進行。但這樣的方式過于繁瑣怀酷,以至于會開始懷疑為什么還需要Keycloak稻爬。
  2. 當(dāng)我們想要自定義一些登錄注冊的流程時,比如想通過短信驗證碼進行登錄蜕依。

Authentication Flow

解決這兩個問題的方式就是Authentication SPI桅锄,它可以用來擴展或是替代已有的認證流程琉雳,通過下圖,看看已有的流程都有哪些:
authentication bindings.png

Browser Flow:使用瀏覽器登錄的流程友瘤;
Registration Flow:使用瀏覽器注冊的流程翠肘;
Direct Grant Flow第二節(jié)介紹的通過Post請求獲取token的流程;
Reset Credentials:使用瀏覽器重置密碼的流程辫秧;
Client Authentication:Keycloak保護的server的認證流程束倍。

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

browser flow.png

先來解釋一下圖中的表格绪妹,它定義了通過瀏覽器完成登錄操作所經(jīng)歷的步驟/流程。其中又包含了兩個Column柿究,Auth Type和Requirement邮旷,Auth Type中Cookie,Kerberos蝇摸,Identity Provider Redirector和Forms是同一級的流程婶肩,而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)一個流程包含子流程時弄唧,那么這個流程就變成了抽象概念了啡专。右側(cè)的Requirement則定義了當(dāng)前流程的狀態(tài)险毁,包括 Required,Alternative植旧,Disabled 和 Conditional辱揭。對于同級流程標記為Alternative,則表示在同級流程中只要有一個可以完成操作病附,則不會再需要其他流程的參與问窃;Require則表示這個流程是必須的。

接下來完沪,我們來看一下在登錄過程中這個表格是如何控制整個流程的域庇。當(dāng)我們從瀏覽器發(fā)起登錄請求時,Keycloak會首先檢查請求中的cookie覆积,若cookie驗證通過听皿,則直接返回登錄成功,而不會進行下面的流程宽档,若cookie驗證失敗尉姨,則進入下一個流程的驗證(Cookie校驗是一個特殊的流程,它無需用戶參與吗冤,當(dāng)發(fā)起請求時又厉,即可自發(fā)完成)九府,Kerberos,在Requirement中標記該流程為Disabled覆致,將直接跳過侄旬。其后的兩個流程Identity Provider Redirector和Forms(即用戶名密碼登錄),選其一即可煌妈,正如之前章節(jié)所展示的demo儡羔,用戶可選擇第三方登錄或用戶名密碼登錄。Browser - Conditional OTP 則是一個可選操作璧诵,設(shè)置OTP并通過OTP進一步驗證用戶身份(Multi-factor)

自定義Authentication SPI

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

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

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

Authenticator & AuthenticatorFactory

先來看一下SmsOtpAuthenticator柠掂,它的主要邏輯都集中在authenticate方法中项滑,當(dāng)發(fā)起request token請求時,將通過此方法要校驗參數(shù)的合法性涯贞。通過context的getHttpRequest便可request對象枪狂,再從中獲取我們期望的參數(shù)。在這里我們做了一個mock短信驗證碼宋渔,假設(shè)合法otpId123州疾,otpValue1111,當(dāng)驗證通過后皇拣,再從session.users()中通過username獲取UserModel严蓖,最后將獲取的userModel賦給當(dāng)前context,并調(diào)用context.success()氧急。期間有任何異常都將調(diào)用context.failure(...)退出當(dāng)前認證流程颗胡。

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;
    }
  ...
}

主要的邏輯分析完后,我們再來看看其他的方法吩坝。
requiresUser():有些完整流程(例如毒姨,Browser)是有多個步驟/流程共同組成的,其中一步完成后钉寝,會進入下一步進行驗證弧呐,而這一步有時就需要用到上一步中賦值于context中的UserModel闸迷,而requiresUser()便表示是否需要上一步中的UserModel
configuredFor(...):表格中的Requirement標記了當(dāng)前流程的狀態(tài)泉懦,當(dāng)為Conditional時稿黍,表示該流程的執(zhí)行與否取決于運行時的判斷,configuredFor便是處理這個邏輯的崩哩。

Factory的實現(xiàn)相對就簡單很多巡球,getId()用于標示這個SPI,getRequirementChoices()用于標示這個流程支持哪些Requirement邓嘹,create(...)則用于創(chuàng)建SmsOtpAuthenticator酣栈,F(xiàn)actory對于當(dāng)前運行的Keycloak是一個單例,而SmsOtpAuthenticator則在每次請求時汹押,都有機會創(chuàng)建一個新的實例矿筝。

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進行打包窖维,放置于standalone/deployments目錄下。再通過admin console配置SmsOtpAuthenticator妙痹,步驟如下所示:

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

測試

此時再通過postman發(fā)起請求時怯伊,即可獲得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)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市耿芹,隨后出現(xiàn)的幾起案子崭篡,更是在濱河造成了極大的恐慌,老刑警劉巖吧秕,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琉闪,死亡現(xiàn)場離奇詭異,居然都是意外死亡寇甸,警方通過查閱死者的電腦和手機塘偎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拿霉,“玉大人吟秩,你說我怎么就攤上這事≌捞裕” “怎么了涵防?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我壮池,道長偏瓤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任椰憋,我火速辦了婚禮厅克,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘橙依。我一直安慰自己证舟,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布窗骑。 她就那樣靜靜地躺著女责,像睡著了一般。 火紅的嫁衣襯著肌膚如雪创译。 梳的紋絲不亂的頭發(fā)上抵知,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音软族,去河邊找鬼刷喜。 笑死,一個胖子當(dāng)著我的面吹牛立砸,可吹牛的內(nèi)容都是我干的吱肌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼仰禽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纺蛆?” 一聲冷哼從身側(cè)響起吐葵,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎桥氏,沒想到半個月后温峭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡字支,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年凤藏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片堕伪。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡揖庄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出欠雌,到底是詐尸還是另有隱情蹄梢,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布富俄,位于F島的核電站禁炒,受9級特大地震影響而咆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜幕袱,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一暴备、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧们豌,春花似錦涯捻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至擂煞,卻和暖如春混弥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背对省。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工蝗拿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蒿涎。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓哀托,卻偏偏與公主長得像,于是被迫代替她去往敵國和親劳秋。 傳聞我的和親對象是個殘疾皇子仓手,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容