启用策略强制执行器配置时,对于未经身份验证的请求,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。
根据Keycloak架构图,策略执行检查发生在授权/认证之前。因此,您无法通过策略执行来实现预期的输出。
我建议您使用策略evaluator/provider或使用基于角色的授权来实现这一点。
请参阅我在这篇文章中的回答:Spring Security 插件应该响应 401 而不是 403
它帮助我正确设置状态代码
一个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 以处理失败的身份验证。
创建新类 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));
}
}