美文网首页
iOS Developer的全栈之路 - Keycloak(9)

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

作者: 西西的一天 | 来源:发表于2020-03-08 20:17 被阅读0次

这一节我们来看一看Keycloak的Authentication SPI。先来说说我们为什么需要它,当我们使用Keycloak进行登录注册的时候,默认设置下都是通过web页面完成的,流程是相对固定的,当然也有一些可配置项,例如OTP。这样会带来什么问题呢?

  1. 当我们想通过Rest请求来完成登录注册过程。登录,可以通过第二节中的方式进行;注册相对来说就比较麻烦了,需要一个搭配另一个server,再配合第五节中Admin API来进行。但这样的方式过于繁琐,以至于会开始怀疑为什么还需要Keycloak。
  2. 当我们想要自定义一些登录注册的流程时,比如想通过短信验证码进行登录。

Authentication Flow

解决这两个问题的方式就是Authentication SPI,它可以用来扩展或是替代已有的认证流程,通过下图,看看已有的流程都有哪些: authentication bindings.png

Browser Flow:使用浏览器登录的流程;
Registration Flow:使用浏览器注册的流程;
Direct Grant Flow第二节介绍的通过Post请求获取token的流程;
Reset Credentials:使用浏览器重置密码的流程;
Client Authentication:Keycloak保护的server的认证流程。

右侧的下拉列表中可以选择相应的流程,这些流程的定义如下图所示,我们以Browser为例进行解释:

browser flow.png

先来解释一下图中的表格,它定义了通过浏览器完成登录操作所经历的步骤/流程。其中又包含了两个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
    ]
  ]
]

当一个流程包含子流程时,那么这个流程就变成了抽象概念了。右侧的Requirement则定义了当前流程的状态,包括 Required,Alternative,Disabled 和 Conditional。对于同级流程标记为Alternative,则表示在同级流程中只要有一个可以完成操作,则不会再需要其他流程的参与;Require则表示这个流程是必须的。

接下来,我们来看一下在登录过程中这个表格是如何控制整个流程的。当我们从浏览器发起登录请求时,Keycloak会首先检查请求中的cookie,若cookie验证通过,则直接返回登录成功,而不会进行下面的流程,若cookie验证失败,则进入下一个流程的验证(Cookie校验是一个特殊的流程,它无需用户参与,当发起请求时,即可自发完成),Kerberos,在Requirement中标记该流程为Disabled,将直接跳过。其后的两个流程Identity Provider Redirector和Forms(即用户名密码登录),选其一即可,正如之前章节所展示的demo,用户可选择第三方登录或用户名密码登录。Browser - Conditional OTP 则是一个可选操作,设置OTP并通过OTP进一步验证用户身份(Multi-factor)

自定义Authentication SPI

现在,通过一个demo来演示如何通过自定义Authentication SPI来实现一个短信验证码登录需求,这里的登录指的是通过postman发送一个post请求来获取token,如下图所示: sms opt request.png

从代码层面,需要两个类:实现Authenticator接口的SmsOtpAuthenticator 和 实现AuthenticatorFactory/ConfigurableAuthenticatorFactory接口的SmsOtpAuthenticatorFactory

从概念上理解,Authenticator就是上面分析的一个验证流程/步骤,SmsOtpAuthenticatorFactory为工厂类,这样的搭配和上一节中User Storage SPI是相同,而这个demo也是在上一节的基础上进行的。

Authenticator & AuthenticatorFactory

先来看一下SmsOtpAuthenticator,它的主要逻辑都集中在authenticate方法中,当发起request token请求时,将通过此方法要校验参数的合法性。通过context的getHttpRequest便可request对象,再从中获取我们期望的参数。在这里我们做了一个mock短信验证码,假设合法otpId123otpValue1111,当验证通过后,再从session.users()中通过username获取UserModel,最后将获取的userModel赋给当前context,并调用context.success()。期间有任何异常都将调用context.failure(...)退出当前认证流程。

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标记了当前流程的状态,当为Conditional时,表示该流程的执行与否取决于运行时的判断,configuredFor便是处理这个逻辑的。

Factory的实现相对就简单很多,getId()用于标示这个SPI,getRequirementChoices()用于标示这个流程支持哪些Requirement,create(...)则用于创建SmsOtpAuthenticator,Factory对于当前运行的Keycloak是一个单例,而SmsOtpAuthenticator则在每次请求时,都有机会创建一个新的实例。

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

部署方式和上一节中的User Storage相同,需要在src/main/resources/META-INF/services目录下创建org.keycloak.authentication.AuthenticatorFactory文件,并在其中添加SmsOtpAuthenticatorFactory的包名:

com.iossocket.SmsOtpAuthenticatorFactory

在通过mvn package进行打包,放置于standalone/deployments目录下。再通过admin console配置SmsOtpAuthenticator,步骤如下所示:

  1. 创建新的流程容器 create new flow.png
  2. 为新创建的流程容器起一个别名 create top level form.png
  3. 选择刚创建好的流程容器,并添加一个execution add execution.png
  4. 将原先的Direct Grant Flow改为新的流程容器,并选中Required change existing binding.png

测试

此时再通过postman发起请求时,即可获得token。http://localhost:8080/auth/realms/demo/protocol/openid-connect/token

sms opt request.png
源码可详见:https://github.com/iossocket/userstorage

相关文章

网友评论

      本文标题:iOS Developer的全栈之路 - Keycloak(9)

      本文链接:https://www.haomeiwen.com/subject/afphdhtx.html