美文网首页
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