Keyclock springboot 对于未经身份验证的请求返回 403 而不是 401

问题描述 投票:0回答:4

启用策略强制执行器配置时,对于未经身份验证的请求,Keyclock 返回 403 而不是 401。删除策略执行器配置时,它返回 401。

使用此配置我得到 403 空响应。

keycloak:
  realm: ${KEYCLOAK_REALM}
  auth-server-url: ${KEYCLOAK_AUTH_SERVER_URL}
  ssl-required: external
  resource: ${KEYCLOAK_CLIENT_ID}
  credentials.secret: ${KEYCLOAK_CLIENT_SECRET}
  use-resource-role-mappings: true
  cors: true
  public-client: false
  bearer-only: true
  policy-enforcer-config:
    lazy-load-paths: true
    http-method-as-scope: true
    path-cache-config:
      max-entries: 1000
      lifespan: 1000
    paths:
      - name: Insecure Resource
        path: /
        enforcement-mode: DISABLED
      - name: Swagger UI
        path: /swagger-ui/*
        enforcement-mode: DISABLED
      - name: Swagger Resources
        path: /swagger-resources/*
        enforcement-mode: DISABLED
      - name: Swagger api Resources
        path: /api-docs
        enforcement-mode: DISABLED
  securityConstraints:
    - authRoles:
       - '*'
      securityCollections:
        - name: protected
          patterns:
            - '/v1/*'
            - '/intranet/*'

如果我像这样删除策略执行者

keycloak:
  realm: ${KEYCLOAK_REALM}
  auth-server-url: ${KEYCLOAK_AUTH_SERVER_URL}
  ssl-required: external
  resource: ${KEYCLOAK_CLIENT_ID}
  credentials.secret: ${KEYCLOAK_CLIENT_SECRET}
  use-resource-role-mappings: true
  cors: true
  public-client: false
  bearer-only: true
#  policy-enforcer-config:
#    lazy-load-paths: true
#    http-method-as-scope: true
#    path-cache-config:
#      max-entries: 1000
#      lifespan: 1000
#    paths:
#      - name: Insecure Resource
#        path: /
#        enforcement-mode: DISABLED
#      - name: Swagger UI
#        path: /swagger-ui/*
#        enforcement-mode: DISABLED
#      - name: Swagger Resources
#        path: /swagger-resources/*
#        enforcement-mode: DISABLED
#      - name: Swagger api Resources
#        path: /api-docs
#        enforcement-mode: DISABLED
  securityConstraints:
    - authRoles:
       - '*'
      securityCollections:
        - name: protected
          patterns:
            - '/v1/*'
            - '/intranet/*'

返回401

{
    "timestamp": "2021-10-05T11:25:33.116+0000",
    "status": 401,
    "error": "Unauthorized",
    "message": "No message available",
    "path": "/v1/approve-documents"
}

即使未经过身份验证,所有请求都会执行策略。如果令牌无效或丢失,如何返回 401。

完整代码https://github.com/prajintst/keyclock-permissions

spring-boot authentication spring-security authorization keycloak
4个回答
2
投票

根据Keycloak架构图,策略执行检查发生在授权/认证之前。因此,您无法通过策略执行来实现预期的输出。

我建议您使用策略evaluator/provider或使用基于角色的授权来实现这一点。


1
投票

请参阅我在这篇文章中的回答:Spring Security 插件应该响应 401 而不是 403

它帮助我正确设置状态代码


0
投票

一个hacky但有效的解决方案。

在你的 Spring Boot 项目中创建一个包:

org.keycloak.adapters.tomcat

在上面的包中创建一个名为 AbstractKeycloakAuthenticatorValve.java 的 JAVA 文件,其中包含以下内容:

package org.keycloak.adapters.tomcat;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.catalina.*;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.*;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.enums.TokenStore;
import org.springframework.http.MediaType;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;

public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
    public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
    private static final Logger log = Logger.getLogger(AbstractKeycloakAuthenticatorValve.class);
    protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
    protected AdapterDeploymentContext deploymentContext;
    protected NodesRegistrationManagement nodesRegistrationManagement;

    public AbstractKeycloakAuthenticatorValve() {
    }

    public void lifecycleEvent(LifecycleEvent event) {
        if ("start".equals(event.getType())) {
            this.cache = false;
        } else if ("after_start".equals(event.getType())) {
            this.keycloakInit();
        } else if (event.getType() == "before_stop") {
            this.beforeStop();
        }

    }

    protected void logoutInternal(Request request) {
        KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
        if (ksc != null) {
            CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, (HttpServletResponse)null);
            KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
            if (ksc instanceof RefreshableKeycloakSecurityContext) {
                ((RefreshableKeycloakSecurityContext)ksc).logout(deployment);
            }

            AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
            tokenStore.logout();
            request.removeAttribute(KeycloakSecurityContext.class.getName());
        }

        request.setUserPrincipal((Principal)null);
    }

    protected void beforeStop() {
        if (this.nodesRegistrationManagement != null) {
            this.nodesRegistrationManagement.stop();
        }

    }

    public void keycloakInit() {
        String configResolverClass = this.context.getServletContext().getInitParameter("keycloak.config.resolver");
        if (configResolverClass != null) {
            try {
                KeycloakConfigResolver configResolver = (KeycloakConfigResolver)this.context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
                this.deploymentContext = new AdapterDeploymentContext(configResolver);
                log.debugv("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
            } catch (Exception var4) {
                log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, var4.getMessage());
                this.deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
            }
        } else {
            InputStream configInputStream = getConfigInputStream(this.context);
            KeycloakDeployment kd;
            if (configInputStream == null) {
                log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
                kd = new KeycloakDeployment();
            } else {
                kd = KeycloakDeploymentBuilder.build(configInputStream);
            }

            this.deploymentContext = new AdapterDeploymentContext(kd);
            log.debug("Keycloak is using a per-deployment configuration.");
        }

        this.context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), this.deploymentContext);
        AbstractAuthenticatedActionsValve actions = this.createAuthenticatedActionsValve(this.deploymentContext, this.getNext(), this.getContainer());
        this.setNext(actions);
        this.nodesRegistrationManagement = new NodesRegistrationManagement();
    }

    private static InputStream getJSONFromServletContext(ServletContext servletContext) {
        String json = servletContext.getInitParameter("org.keycloak.json.adapterConfig");
        if (json == null) {
            return null;
        } else {
            log.trace("**** using org.keycloak.json.adapterConfig");
            return new ByteArrayInputStream(json.getBytes());
        }
    }

    private static InputStream getConfigInputStream(Context context) {
        InputStream is = getJSONFromServletContext(context.getServletContext());
        if (is == null) {
            String path = context.getServletContext().getInitParameter("keycloak.config.file");
            if (path == null) {
                log.trace("**** using /WEB-INF/keycloak.json");
                is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json");
            } else {
                try {
                    is = new FileInputStream(path);
                } catch (FileNotFoundException var4) {
                    log.errorv("NOT FOUND {0}", path);
                    throw new RuntimeException(var4);
                }
            }
        }

        return (InputStream)is;
    }

    public void invoke(Request request, Response response) throws IOException, ServletException {
        CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response);
        Manager sessionManager = request.getContext().getManager();
        CatalinaUserSessionManagementWrapper sessionManagementWrapper = new CatalinaUserSessionManagementWrapper(this.userSessionManagement, sessionManager);
        PreAuthActionsHandler handler = new PreAuthActionsHandler(sessionManagementWrapper, this.deploymentContext, facade);
        if (!handler.handleRequest()) {
            this.checkKeycloakSession(request, facade);
            super.invoke(request, response);
        }
    }

    protected abstract PrincipalFactory createPrincipalFactory();

    protected abstract boolean forwardToErrorPageInternal(Request var1, HttpServletResponse var2, Object var3) throws IOException;

    protected abstract AbstractAuthenticatedActionsValve createAuthenticatedActionsValve(AdapterDeploymentContext var1, Valve var2, Container var3);

    protected boolean authenticateInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException {
        CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response);
        KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
        if (deployment != null && deployment.isConfigured()) {
            AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
            this.nodesRegistrationManagement.tryRegister(deployment);
            CatalinaRequestAuthenticator authenticator = this.createRequestAuthenticator(request, facade, deployment, tokenStore);
            AuthOutcome outcome = authenticator.authenticate();
            if (outcome == AuthOutcome.AUTHENTICATED) {
                return !facade.isEnded();
            } else {
                AuthChallenge challenge = authenticator.getChallenge();
                if (challenge != null) {
                    challenge.challenge(facade);
                }
                writeResponse(response, 401, "Failed to verify token");
                return false;
            }
        } else {
            facade.getResponse().sendError(401);
            return false;
        }
    }

    private void writeResponse(HttpServletResponse response, int status, String message) throws IOException {

        // Generate your intended response here, e.g.:
        ObjectMapper objectMapper = new ObjectMapper();
        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        Map<String, String> body = new HashMap<>();
        body.put("message", message);
        response.getOutputStream().println(objectMapper.writerWithDefaultPrettyPrinter()
                .writeValueAsString(body));
    }

    protected CatalinaRequestAuthenticator createRequestAuthenticator(Request request, CatalinaHttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
        return new CatalinaRequestAuthenticator(deployment, tokenStore, facade, request, this.createPrincipalFactory());
    }

    protected void checkKeycloakSession(Request request, HttpFacade facade) {
        KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
        AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
        tokenStore.checkCurrentToken();
    }

    public void keycloakSaveRequest(Request request) throws IOException {
        this.saveRequest(request, request.getSessionInternal(true));
    }

    public boolean keycloakRestoreRequest(Request request) {
        try {
            return this.restoreRequest(request, request.getSessionInternal());
        } catch (IOException var3) {
            throw new RuntimeException(var3);
        }
    }

    protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) {
        AdapterTokenStore store = (AdapterTokenStore)request.getNote("TOKEN_STORE_NOTE");
        if (store != null) {
            return store;
        } else {
            Object store1;
            if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) {
                store1 = this.createSessionTokenStore(request, resolvedDeployment);
            } else {
                store1 = new CatalinaCookieTokenStore(request, facade, resolvedDeployment, this.createPrincipalFactory());
            }

            request.setNote("TOKEN_STORE_NOTE", store1);
            return (AdapterTokenStore)store1;
        }
    }

    private AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
        AdapterTokenStore store = new CatalinaSessionTokenStore(request, resolvedDeployment, this.userSessionManagement, this.createPrincipalFactory(), this);
        return store;
    }
}

基本上,我们正在修改 Keycloak 中存在的文件 AbstractKeycloakAuthenticatorValve 以处理失败的身份验证。


0
投票

创建新类 CustomContainer.java

import org.apache.catalina.Valve;
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
import java.util.Optional;

@Component
public class CustomContainer implements
        WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        Optional<Valve> valve = factory.getContextValves().stream().filter(currentValve -> KeycloakAuthenticatorValve.class.getClass().equals(currentValve.getClass())).findAny();
        valve.ifPresentOrElse(v -> factory.getContextValves().remove(v),()->{});
        factory.addContextValves(new CustomKeycloakAuthenticationValve());
    }
}

创建新类 CustomKeycloakAuthenticationValve

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.catalina.connector.Request;
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static ai.g42hc.omicsbackend.constants.ResponseConstants.AUTHENTICATION_FAILED;

public class CustomKeycloakAuthenticationValve extends KeycloakAuthenticatorValve {
    
    public boolean authenticate(Request request, HttpServletResponse response) throws IOException {
        boolean val = authenticateInternal(request, response, request.getContext().getLoginConfig());
        if(Boolean.FALSE == val) {
            writeResponse(response, 401, AUTHENTICATION_FAILED);
        }
        return val;
    }

    private void writeResponse(HttpServletResponse response, int status, String message) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        Map<String, String> map = new HashMap<>();
        map.put("message", message);
        response.getOutputStream().println(objectMapper.writeValueAsString(map));
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.