KeyCloak 登录源码解析

登录/OTP页面怎么来的

表单动作操作实际上是调用各种 Form(freeMarker下页面绑定的类) 下的 action 或者是 authenticate 方法,这些方法实际上在调用 challenge 方法的时候会导致实际上表单的切换等行为的出现。至于静态资源和Form怎么绑定的就不知道了,看不懂

FreeMarkerLoginFormsProvider

登录页面的静态资源

login.ftl、loginpage.ts

请求发起方式

前端页面其实是通过RequiredConsentBuilder发起的请求

KeyCloak流程配置页面

KeyCloak鉴权流程

这些流程其实在KeyCloak的数据库中的 authentication_flow 是能够配置的。

登录流程配置页面

KeyCloak配置

KeyCloak配置

这些可选可增加的配置起始是由KeyCloak在加载自身的 ProviderFactory 继承类的时候提供的,同时 ProviderFactory 自身也作为Spi对外暴露,有任何需要自定义认证过程或者流程的,我们都可以对其做实现。公司框架中就提供了TplhkAuthenticatorFactory对其做实现

我们实际登录认证的时候,其实是按照flow中的顺序逐个走执行的,比如我们目前存在以下的 brower 配置,也就是图上的tplhk of brower forms 流程

账号密码认证

根据优先级,我们的登录流程会先对账号和密码进行认证,轮到优先级较低(priority=21) 的认证步骤,但此认证步骤其实是一个完整的 flow 而不是单一个 authenticator ,所以实际上还会执行以下流程 (两者都不是必须)

账号密码认证2

入口 - LoginActionsService

authenticate - Rest Api

@Path(AUTHENTICATE_PATH)
@GET
public Response authenticate(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
                             @QueryParam(SESSION_CODE) String code,
                             @QueryParam(Constants.EXECUTION) String execution, // 对应着最终的登录处理的 认证处理器 【前端会传过来】
                             @QueryParam(Constants.CLIENT_ID) String clientId,
                             @QueryParam(Constants.TAB_ID) String tabId,
                             @QueryParam(Constants.CLIENT_DATA) String clientData) {
	event.event(EventType.LOGIN);
    // 利用 SessionCodeChecks 来检查当前是否为 action 动作
    /** SesscopmCodeChecks 值判断
        actionRequest = false 对应
        如果传入的参数 code 是 null,并且 
            execution 为 null,或者 execution.equals(lastExecFromSession) 【与Session中保留的上一个执行相同,可能指代的是加载相同页面】 或者 当前执行的状态为 CHALLENGED
        actionRequest = true 对应
        如果传入的参数 code 不为 null,并且 ClientSessionCode.parseResult解析结果正确。
    在登录的时候,我们的code 也就是session_code不可能为null,所以只可能 actionRequest 为 true
    */
    SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, clientData, AUTHENTICATE_PATH);
    if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
        return checks.getResponse();
    }
    AuthenticationSessionModel authSession = checks.getAuthenticationSession();
    boolean actionRequest = checks.isActionRequest();
    processLocaleParam(authSession);
    // 关键调用
    return processAuthentication(actionRequest, execution, authSession, null);
}
protected void processLocaleParam(AuthenticationSessionModel authSession) {
    LocaleUtil.processLocaleParam(session, realm, authSession);
}
protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
    // 继续调用 processFlow
    return processFlow(action, execution, authSession, AUTHENTICATE_PATH, AuthenticationFlowResolver.resolveBrowserFlow(authSession), errorMessage, new AuthenticationProcessor());
}

processFlow

protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
    // 配置处理器
    processor.setAuthenticationSession(authSession)
        .setFlowPath(flowPath)
        .setBrowserFlow(true)
        .setFlowId(flow.getId())
        .setConnection(clientConnection)
        .setEventBuilder(event)
        .setRealm(realm)
        .setSession(session)
        .setUriInfo(session.getContext().getUri())
        .setRequest(request);
    // 如果有错误消息,对其处理
    if (errorMessage != null) {
        processor.setForwardedErrorMessage(new FormMessage(null, errorMessage));
    }
    // 检查并处理之前请求设置的转发错误消息
    // Check the forwarded error message, which was set by previous HTTP request
    String forwardedErrorMessage = authSession.getAuthNote(FORWARDED_ERROR_MESSAGE_NOTE);
    if (forwardedErrorMessage != null) {
        authSession.removeAuthNote(FORWARDED_ERROR_MESSAGE_NOTE);
        processor.setForwardedErrorMessage(new FormMessage(null, forwardedErrorMessage));
    }
    Response response;
    try {
        // 这里的 action 其实始终都是我们上面提到的 SesscopmCodeChecks 中的actionRequest
        // 在登录时,他应该是 true
        if (action) {
            // 调用processor做认证处理
            response = processor.authenticationAction(execution);
        } else {
            response = processor.authenticate();
        }
    } catch (WebApplicationException e) {
        response = e.getResponse();
        authSession = processor.getAuthenticationSession();
    } catch (Exception e) {
        response = processor.handleBrowserException(e);
        authSession = processor.getAuthenticationSession(); // Could be changed (eg. Forked flow)
    }
    return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action, request);
}

AuthenticationProcessor

authenticationAction

public Response authenticationAction(String execution) {
    // 初始验证和参数检查
    logger.debug("authenticationAction");
    checkClientSession(true);  // 确保客户端会话有效
    String current = authenticationSession.getAuthNote(CURRENT_AUTHENTICATION_EXECUTION);
    // 执行execution的id校验
    if (execution == null || !execution.equals(current)) {
        logger.debug("Current execution does not equal executed execution.  Might be a page refresh");
        return new AuthenticationFlowURLHelper(session, realm, uriInfo).showPageExpired(authenticationSession);
    }
    // 获取当前用户,并且做验证
    UserModel authUser = authenticationSession.getAuthenticatedUser();
    validateUser(authUser);
    // 获取执行器 (如果找不到,那么认证失败、并且重新尝试认证)
    AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution);
    if (model == null) {
        logger.debug("Cannot find execution, reseting flow");
        logFailure();
        resetFlow();
        return authenticate();
    }
    // 记录认证事件
    event.client(authenticationSession.getClient().getClientId())
        .detail(Details.REDIRECT_URI, authenticationSession.getRedirectUri())
        .detail(Details.AUTH_METHOD, authenticationSession.getProtocol());
    String authType = authenticationSession.getAuthNote(Details.AUTH_TYPE);
    if (authType != null) {
        event.detail(Details.AUTH_TYPE, authType);
    }
    // 认证流程的核心部分
    AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, model);
    Response challenge = authenticationFlow.processAction(execution);
    // 如果认证失败,做处理
    if (challenge != null) return challenge;
    if (authenticationSession.getAuthenticatedUser() == null) {
        throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER);
    }
    if (!authenticationFlow.isSuccessful()) {
        throw new AuthenticationFlowException(authenticationFlow.getFlowExceptions());
    }
    // 调用认证完成方法
    return authenticationComplete();
}

DefaultAuthenticationFlow

processAction

public Response processAction(String actionExecution) {
    logger.debugv("processAction: {0}", actionExecution);
    // 对传入参数做认证
    if (actionExecution == null || actionExecution.isEmpty()) {
        throw new AuthenticationFlowException("action is not in current execution", AuthenticationFlowError.INTERNAL_ERROR);
    }
    // 获取 execution Id 对应的 执行器
    AuthenticationExecutionModel model = processor.getRealm().getAuthenticationExecutionById(actionExecution);
    if (model == null) {
        throw new AuthenticationFlowException("Execution not found", AuthenticationFlowError.INTERNAL_ERROR);
    }
    /** 针对POST请求做一些特殊的处理流程处理,这里实际上只有请求时携带特殊的参数时才会生效 */
    if (HttpMethod.POST.equals(processor.getRequest().getHttpMethod())) {
        MultivaluedMap<String, String> inputData = processor.getRequest().getDecodedFormParameters();
        String authExecId = inputData.getFirst(Constants.AUTHENTICATION_EXECUTION);
        // 如果用户点击换一种验证方式 (MFA)
        // User clicked on "try another way" link
        if (inputData.containsKey("tryAnotherWay")) {
            logger.trace("User clicked on link 'Try Another Way'");

            processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED, "true");
            return createSelectAuthenticatorsScreen(model);
        }
        // 根据authExecId调用处理逻辑
        // check if the user has switched to a new authentication execution, and if so switch to it.
        if (authExecId != null && !authExecId.isEmpty()) {

            processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED);
            List<AuthenticationSelectionOption> selectionOptions = createAuthenticationSelectionList(model);
           // 根据 authExecId 找到目标的处理方案
            // Check if switch to the requested authentication execution is allowed
            selectionOptions.stream()
                .filter(authSelectionOption -> authExecId.equals(authSelectionOption.getAuthExecId()))
                .findFirst()
                .orElseThrow(() -> new AuthenticationFlowException("Requested authentication execution is not allowed",
                                                                   AuthenticationFlowError.INTERNAL_ERROR)
                            );
		   // 查询当前目标 realm 下的目标 authExecId 对应的流程模型
            model = processor.getRealm().getAuthenticationExecutionById(authExecId);
		   // 调用流程模型处理器来处理流程
            Response response = processSingleFlowExecutionModel(model, false);
            if (response == null) {
                return continueAuthenticationAfterSuccessfulAction(model);
            } else
                return response;
        }
    }
    // 检查当前执行器本身是不是一个认证流程,而不只是一个的认证器
    //handle case where execution is a flow - This can happen during user registration for example
    if (model.isAuthenticatorFlow()) {
        logger.debug("execution is flow");
        // 创建具体流程对应的 认证流程 对象,这里其实是根据 model【authentication_flow表】 创建对应的 Flow 对象
        // - 如果类型是 basic-flow 那么这里调用的其实是 DefaultAuthenticationFlow 也就是调用当前类以及方法
        // - 如果类型是 form-flow 那么这里调用的则是 FormAuthenticationFlow
        // - 如果类型是 client-flow 那么这里调用的是 ClientAuthenticationFlow
        AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
        // 递归调用processAction处理该子流程
        Response flowChallenge = authenticationFlow.processAction(actionExecution);
        // 如果流程处理没有"挑战响应"【结合前后代码来看,这里flowChallenge==null表示认证流程结束】
        if (flowChallenge == null) {
            // 对结果做验证,检查是否全部完成 【结果会设置到 Status】
            checkAndValidateParentFlow(model);
            // 继续处理剩下的流程【如果后续存在OTP认证,那么实际上是由procesFlow处理的】
            return processFlow();
        } else {
            // 如果存在 "挑战流程" 设置状态为已经完成
            setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
            // 返回结果【让用户进一步认证】
            return flowChallenge;
        }
    }

    // handle normal execution case
    // 如果当前认证器只是一个简单的认证器
    // 利用 execution id 获取实际上进行登录认证处理的 Authenticator 【对应authentication_execution表】
    AuthenticatorFactory factory = getAuthenticatorFactory(model);
    Authenticator authenticator = createAuthenticator(factory);
    AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
    result.setAuthenticationSelections(createAuthenticationSelectionList(model));

    if (factory instanceof AuthenticationFlowCallbackFactory) {
        AuthenticatorUtil.setAuthCallbacksFactoryIds(processor.getAuthenticationSession(), factory.getId());
    }

    logger.debugv("action: {0}", model.getAuthenticator());
    // 调用登录验证方法
    authenticator.action(result);
    Response response = processResult(result, true);
    if (response == null) {
        return continueAuthenticationAfterSuccessfulAction(model);
    } else return response;
}

需要注意的是,其中有一段针对Post的特殊处理逻辑,实际上进入HTTPPost的特殊处理逻辑前提要求是请求参数中包含一些特殊值,比如说请求表单中有 authExecId 获 tryAnotherway,目前登录流程暂为涉及。很有可能这部分的处理是提供给多种认证方案都可以登录时,用户切换认证方案时做处理的代码逻辑。

如果我们的登录流程只是一个简单的账户密码登录,那么登录流程到这里就结束了,但如果有其他类似的OTP登录后续的必需流程,接下来还需要process做处理。

processFlow

public Response processFlow() {
    logger.debugf("processFlow: %s", flow.getAlias());
	// 检查Session之中是否配置了 AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED ,如果有那么说明当前并非首个认证方案
    // 获取上一个认证方案的ID和认证执行器
    if (Boolean.parseBoolean(processor.getAuthenticationSession().getAuthNote(AuthenticationProcessor.AUTHENTICATION_SELECTOR_SCREEN_DISPLAYED))) {
        logger.tracef("Refreshed page on authentication selector screen");
        String lastExecutionId = processor.getAuthenticationSession().getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
        if (lastExecutionId != null) {
            AuthenticationExecutionModel executionModel = processor.getRealm().getAuthenticationExecutionById(lastExecutionId);
            if (executionModel != null) {
                return createSelectAuthenticatorsScreen(executionModel);
            }
        }
    }
	// 将实际要执行的模型分成两类
    //separate flow elements into required and alternative elements
    List<AuthenticationExecutionModel> requiredList = new ArrayList<>();
    List<AuthenticationExecutionModel> alternativeList = new ArrayList<>();
    fillListsOfExecutions(executions.stream(), requiredList, alternativeList);

    // 遍历必须要执行的列表
    //handle required elements : all required elements need to be executed
    boolean requiredElementsSuccessful = true;
    Iterator<AuthenticationExecutionModel> requiredIListIterator = requiredList.listIterator();
    while (requiredIListIterator.hasNext()) {
        // 迭代认证器Model进行处理
        AuthenticationExecutionModel required = requiredIListIterator.next();
        // 如果是条件流程,那么对条件进行计算,如果为false且没有被处理过,那么继续处理
        //Conditional flows must be considered disabled (non-existent) if their condition evaluates to false.
        //If the flow has been processed before it will not be removed to consider its execution status.
        if (required.isConditional() && !isProcessed(required) && isConditionalSubflowDisabled(required)) {
            requiredIListIterator.remove();
            continue;
        }
        // 对某个认证方案调用处理,并且如果他有响应那么直接响应
        Response response = processSingleFlowExecutionModel(required, true);
        requiredElementsSuccessful &= processor.isSuccessful(required) || isSetupRequired(required);
        if (response != null) {
            return response;
        }
        // 如果某个必需的认证失败,那么直接中断整个流程
        // Some required elements were not successful and did not return response.
        // We can break as we know that the whole subflow would be considered unsuccessful as well
        if (!requiredElementsSuccessful) {
            break;
        }
    }

    // 对可选的认证任务做处理
    //Evaluate alternative elements only if there are no required elements. This may also occur if there was only condition elements
    if (requiredList.isEmpty()) {
        //check if an alternative is already successful, in case we are returning in the flow after an action
        if (alternativeList.stream().anyMatch(alternative -> processor.isSuccessful(alternative) || isSetupRequired(alternative))) {
            return onFlowExecutionsSuccessful();
        }
		// 对可选列表循环执行认证处理
        //handle alternative elements: the first alternative element to be satisfied is enough
        for (AuthenticationExecutionModel alternative : alternativeList) {
            try {
                // 执行处理
                Response response = processSingleFlowExecutionModel(alternative, true);
                if (response != null) {
                    return response;
                }
                // 如果执行成功或者认证方案执行结果状态是 SET_REQUIRED
                if (processor.isSuccessful(alternative) || isSetupRequired(alternative)) {
                    return onFlowExecutionsSuccessful();
                }
            } catch (AuthenticationFlowException afe) {
                //consuming the error is not good here from an administrative point of view, but the user, since he has alternatives, should be able to go to another alternative and continue
                afeList.add(afe);
                setExecutionStatus(alternative, AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
            }
        }
    } else {
		// 如果必需的列表中不为空,并且全都成功了,那么调用 onFlowExecutionsSuccessful
        if (requiredElementsSuccessful) {
            return onFlowExecutionsSuccessful();
        }
    }
    return null;
}

private boolean isSetupRequired(AuthenticationExecutionModel model) {
    return AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED.equals(processor.getAuthenticationSession().getExecutionStatus().get(model.getId()));
}
private Response onFlowExecutionsSuccessful() {
    if (flow.isTopLevel()) {
        logger.debugf("Authentication successful of the top flow '%s'", flow.getAlias());
        executeTopFlowSuccessCallbacks();
    }

    successful = true;
    return null;
}

认证方案的切换 (MFA)

processSingleFlowExecutionModel

private Response processSingleFlowExecutionModel(AuthenticationExecutionModel model, boolean calledFromFlow) {

    logger.debugf("check execution: '%s', requirement: '%s'", logExecutionAlias(model), model.getRequirement());
    // 检查需要执行的模型是否已经完成了执行工作,如果是那么直接返回NULL
    if (isProcessed(model)) {
        logger.debugf("execution '%s' is processed", logExecutionAlias(model));
        return null;
    }
    // 如果目标模型不是单个模型,而不是一整个完整的认证
    //handle case where execution is a flow
    if (model.isAuthenticatorFlow()) {
        // 创建认证流程
        AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
        // 调用 processFlow 完成整个认证流程
        Response flowChallenge = authenticationFlow.processFlow();
        if (flowChallenge == null) {
            if (authenticationFlow.isSuccessful()) {
                logger.debugf("Flow '%s' successfully finished", logExecutionAlias(model));
                setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.SUCCESS);
            } else {
                logger.debugf("Flow '%s' failed", logExecutionAlias(model));
                setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.FAILED);
            }
            return null;
        } else {
            setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
            return flowChallenge;
        }
    }
	// 如果目标认证确实只是一个简单的单个认证
    //handle normal execution case
    // 创建认证方案的工厂
    AuthenticatorFactory factory = getAuthenticatorFactory(model);
    // 创建认证方案
    Authenticator authenticator = createAuthenticator(factory);
    logger.debugv("authenticator: {0}", factory.getId());
    UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
	// 创建认证选择列表
    //If executions are alternative, get the actual execution to show based on user preference
    List<AuthenticationSelectionOption> selectionOptions = createAuthenticationSelectionList(model);
	/**
		如果存在可选的认证选项
			- 过滤出不是流程一部分,并且还没执行过的认证
			- 如果是空,那么直接返回 null , 否则选择首个选项作为 认证方案
			- 构造首个方案的认证方案工厂和认证方案
	*/
    if (!selectionOptions.isEmpty() && calledFromFlow) {
        List<AuthenticationSelectionOption> finalSelectionOptions = selectionOptions.stream().filter(aso -> !aso.getAuthenticationExecution().isAuthenticatorFlow() && !isProcessed(aso.getAuthenticationExecution())).collect(Collectors.toList());
        if (finalSelectionOptions.isEmpty()) {
            //move to next
            return null;
        }
        model = finalSelectionOptions.get(0).getAuthenticationExecution();
        factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator());
        if (factory == null) {
            throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
        }
        authenticator = createAuthenticator(factory);
    }
    AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executions);
    context.setAuthenticationSelections(selectionOptions);
    // 检查认证方案是否必须配置用户?
    if (authenticator.requiresUser()) {
        if (authUser == null) {
            // 如果必须配置用户,而当前没有用户,那么抛出异常
            throw new AuthenticationFlowException("authenticator '" + factory.getId() + "' requires user to be set in the authentication context by previous authenticators, but user is not set yet", AuthenticationFlowError.UNKNOWN_USER);
        }
        // 检查认证方案是否是当前用户配置
        if (!authenticator.configuredFor(processor.getSession(), processor.getRealm(), authUser)) {
            // 检查是否有用户允许、是否是必须等
            if (factory.isUserSetupAllowed() && model.isRequired() && authenticator.areRequiredActionsEnabled(processor.getSession(), processor.getRealm())) {
                // 设置认证状态和认证方案为必须
                //This means that having even though the user didn't validate the
                logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId());
                setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED);
                authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser());
                return null;
            } else {
                throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
            }
        }
    }
    else {
		// 对认证器是否已经配置、是否允许用户进行配置,如果条件吻合说明出现认证器数据有问题
        if ((authUser != null) &&
            !authenticator.configuredFor(processor.getSession(), processor.getRealm(), authUser) &&
            !factory.isUserSetupAllowed() &&
            (authenticator instanceof CredentialValidator)) {
            throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
        }
    }
    logger.debugv("invoke authenticator.authenticate: {0}", factory.getId());
    // 做实际的认证处理
    authenticator.authenticate(context);
    return processResult(context, false);
}

补充问题

浏览器请求的时候携带的execution是哪里来的?

首先我们查看源码,可以发现在KeyCloak服务刚启动的时候,其实会调用 KeyCloakServicestart 方法,该方法之中调用过 setupDevConfig 方法,该方法内部会通过 KeycloakSessionFactory 构造 KeyCloakSession 。