這一節(jié)我們來看一看Keycloak的Authentication SPI茵宪。先來說說我們?yōu)槭裁葱枰裕?dāng)我們使用Keycloak進行登錄注冊的時候,默認設(shè)置下都是通過web頁面完成的,流程是相對固定的庞溜,當(dāng)然也有一些可配置項张抄,例如OTP砂蔽。這樣會帶來什么問題呢?
- 當(dāng)我們想通過Rest請求來完成登錄注冊過程署惯。登錄左驾,可以通過第二節(jié)中的方式進行;注冊相對來說就比較麻煩了极谊,需要一個搭配另一個server诡右,再配合第五節(jié)中Admin API來進行。但這樣的方式過于繁瑣怀酷,以至于會開始懷疑為什么還需要Keycloak稻爬。
- 當(dāng)我們想要自定義一些登錄注冊的流程時,比如想通過短信驗證碼進行登錄蜕依。
Authentication Flow
解決這兩個問題的方式就是Authentication SPI桅锄,它可以用來擴展或是替代已有的認證流程琉雳,通過下圖,看看已有的流程都有哪些:Browser Flow
:使用瀏覽器登錄的流程友瘤;
Registration Flow
:使用瀏覽器注冊的流程翠肘;
Direct Grant Flow
:第二節(jié)介紹的通過Post請求獲取token的流程;
Reset Credentials
:使用瀏覽器重置密碼的流程辫秧;
Client Authentication
:Keycloak保護的server的認證流程束倍。
右側(cè)的下拉列表中可以選擇相應(yīng)的流程,這些流程的定義如下圖所示盟戏,我們以Browser
為例進行解釋:
先來解釋一下圖中的表格绪妹,它定義了通過瀏覽器完成登錄操作所經(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腮猖,如下圖所示:從代碼層面鉴扫,需要兩個類:實現(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è)合法otpId
為123
州疾,otpValue
為1111
,當(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妙痹,步驟如下所示:
-
創(chuàng)建新的流程容器create new flow.png
-
為新創(chuàng)建的流程容器起一個別名create top level form.png
-
選擇剛創(chuàng)建好的流程容器铸史,并添加一個executionadd execution.png
-
將原先的Direct Grant Flow改為新的流程容器,并選中Requiredchange existing binding.png
測試
此時再通過postman發(fā)起請求時怯伊,即可獲得token琳轿。http://localhost:8080/auth/realms/demo/protocol/openid-connect/token
源碼可詳見:https://github.com/iossocket/userstorage