为啥 Spring Boot 不会重定向到 403 上的 Keycloak 登录页面?
Posted
技术标签:
【中文标题】为啥 Spring Boot 不会重定向到 403 上的 Keycloak 登录页面?【英文标题】:Why won't Spring Boot redirect to Keycloak login page on 403?为什么 Spring Boot 不会重定向到 403 上的 Keycloak 登录页面? 【发布时间】:2022-01-09 07:22:12 【问题描述】:我正在尝试使用 Keycloak(单独的实例)提供的身份验证设置 Spring Boot api。全部在本地 docker swarm/compose 中运行。麻烦的是,当我将用户定向到由@RolesAllowed("ROLE_USER")
控制的/api/v3/login 时,我会返回带有消息There was an unexpected error (type=Forbidden, status=403)
的标准白标签错误页面。我希望浏览器被定向到 Keycloak 客户端登录页面。
下面的设置。
ApplicationConfiguration - 这样做是为了让我们从数据库而不是配置文件中提取 Keycloak 客户端配置。我们将拥有多个客户端,具体取决于用户的电子邮件域(通过 cookie 提供给我们):
@ComponentScan("com.mycompany")
@Configuration
@EnableJpaRepositories(basePackages = "com.mycompany")
@EntityScan("com.mycompany")
public class ApplicationConfiguration
...
@Bean
public KeycloakConfigResolver keycloakConfigResolver()
return new CustomKeycloakConfigResolver();
CustomKeycloakConfigResolver:
public class CustomKeycloakConfigResolver implements KeycloakConfigResolver
@Autowired
private KeycloakConfigService keycloakConfigService;
...
@Override
@Transactional
public KeycloakDeployment resolve(final HttpFacade.Request request)
HttpFacade.Cookie cookie = request.getCookie("authDomain");
if (cookie == null)
return generateNullDeployment();
final Pageable defaultPaging = PageRequest.of(0,1,Sort.by("id").ascending());
Page<KeycloakConfig> page = keycloakConfigService.readConfigsByFilter(
"domain", cookie.getValue(), defaultPaging
);
if ((page == null) || (page.getContent().size() < 1))
return generateNullDeployment();
KeycloakConfig config = page.getContent().get(0);
AdapterConfig adapterConfig = new AdapterConfig();
adapterConfig.setRealm(config.getRealm());
adapterConfig.setResource(config.getResource());
adapterConfig.setPublicClient(config.getIsPublic());
adapterConfig.setAuthServerUrl(config.getAuthServerUrl());
adapterConfig.setSslRequired(
config.getIsSslRequired() ? "all" : "none"
);
adapterConfig.setUseResourceRoleMappings(
config.getUseResourceRoleMappings()
);
adapterConfig.setTokenStore(config.getTokenStore());
adapterConfig.setBearerOnly(config.getBearerOnly());
KeycloakDeployment keycloakDeployment =
KeycloakDeploymentBuilder.build(adapterConfig);
LOGGER.info("Keycloak Deployment Realm: ", keycloakDeployment.getRealm());
LOGGER.info("Keycloak Deployment Resource: ", keycloakDeployment.getResourceName());
LOGGER.info("Keycloak Deployment URL: ", keycloakDeployment.getAuthUrl());
return keycloakDeployment;
注意 - 这一切似乎都在工作,尽管在一次调用中,此 resolve 方法会被调用数十次:
...
o.k.adapters.KeycloakConfigResolver : Keycloak Deployment Realm: SpringBootKeycloak
o.k.adapters.KeycloakConfigResolver : Keycloak Deployment Resource: SpringBootKeycloak
o.keycloak.adapters.KeycloakDeployment : Loaded URLs from http://auth-service:8080/auth/realms/SpringBootKeycloak/.well-known/openid-configuration
...
o.k.adapters.KeycloakConfigResolver : Keycloak Deployment Realm: SpringBootKeycloak
o.k.adapters.KeycloakConfigResolver : Keycloak Deployment Resource: SpringBootKeycloak
o.keycloak.adapters.KeycloakDeployment : Loaded URLs from http://auth-service:8080/auth/realms/SpringBootKeycloak/.well-known/openid-configuration
...
无论如何,最后,我们有一个有效的 KeycloakDeployment,其中 http://auth-service:8080/auth 作为身份验证登录 URL。
应用的安全配置是:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class SecurityConfiguration
extends KeycloakWebSecurityConfigurerAdapter
@Override
protected void configure(final HttpSecurity http) throws Exception
super.configure(http);
http
.csrf().disable()
.antMatcher("/**")
.authorizeRequests();
...
所以所有请求都被授权。 API 端点是:
...
@RolesAllowed("ROLE_USER")
@GetMapping(
value = "/login",
produces = MediaType.APPLICATION_JSON_VALUE
)
@ResponseBody
public Map<String, String> login()
final Map<String, String> response = new HashMap<String, String>();
response.put("status", "OK");
return response;
...
所以我真的只是想看看用户是否经过身份验证。
现在 - 我期望发生的是用户进来,没有经过身份验证,并被定向到 Keycloak 登录页面。相反,我只得到 403 Forbidden white label 页面。
我认为这是因为在安全配置中我使用了.authorizeRequests()
,即使用户没有经过身份验证,这也会为用户提供“匿名”角色。但在我的一生中,我似乎无法获得正确的调用组合,因此当用户点击该登录端点但实际上并未登录时,他们将被定向到 KeycloakDeployment 的登录页面。
更新:我想我已经解决了这个谜团的一部分。
我在类路径上有一个旧的 AuthenticationEntryPoint 类
@ControllerAdvice
public class CustomAuthenticationEntryPoint
implements AuthenticationEntryPoint
尽管我从未使用 .authenticationEntryPoint() 指定它,但 Spring Boot 神奇的自动配置似乎已经找到并正在使用它。
我已完全禁用它,现在我至少从 /api/v3/login 重定向到 /sso/login。但是 /sso/login 不再使用 CustomKeycloakConfigResolver
,这很重要,因为没有它我们就没有 KeycloakDeployment,这意味着我们会因异常而失败
rest-api_1 | 2021-12-02 21:59:20.871 WARN 12 --- [nio-8080-exec-5] o.keycloak.adapters.KeycloakDeployment : Failed to load URLs from null/realms/null/.well-known/openid-configuration
rest-api_1 |
rest-api_1 | java.lang.IllegalStateException: Target host is null
rest-api_1 | at org.apache.http.util.Asserts.notNull(Asserts.java:52) ~[httpcore-4.4.14.jar!/:4.4.14]
【问题讨论】:
可以做个小测试吗?在 SecurityConfiguration 中显式配置时是否有效:http.authorizeRequests() .antMatchers("/login").hasRole("ROLE_USER") .anyRequest().permitAll();
另外,检查您的 Keycloak 中的角色名称是否相同,并且该角色是否已分配给用户。有时它可能会被忽略。
必须将 hasRole 更改为 .hasRole("USER")
或启动失败(例外,角色不应以“ROLE_”开头,因为它已被假定)。但还是一样的结果。我不认为名字很重要——因为我从来没有被引导到 Keycloak 登录页面,所以我从来没有得到任何角色的 JWT。但我还是仔细检查了。
如果您以同样的方式将角色名称更改为@RolesAllowed("USER")
,会发生什么?
感谢@roccobaroccoSC。注释本身不是问题,但老实说我不确定是什么。春天提供了太多给猫剥皮的方法,但它们似乎并不都能很好地相互配合。无论如何-要使注释正常工作,您只需要安全配置类上的注释@EnableGlobalMethodSecurity(jsr250Enabled = true)
,就可以了。我最终剥离了所有内容,并在其余 API 上重新从头开始,它正在工作,尽管我们会看到当我再次遇到问题时重新添加东西。
【参考方案1】:
根据要求,我正在分享我的配置(匿名)。不同之处在于,授权映射在 SecurityConfigurerAdapter
中,而不是在像您这样的注释中。
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
// https://www.baeldung.com/spring-boot-keycloak#securityconfig
// https://newbedev.com/when-to-use-spring-security-s-antmatcher
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class MyAppWebSecurityConfigurerAdapter extends KeycloakWebSecurityConfigurerAdapter
private static final String COM_MY_COMPANY_MY_APP_SERVER_SECURITY_DISABLED = "com.mycompany.myapp.server.security.DISABLED";
Logger log = LoggerFactory.getLogger(getClass());
// Tasks the SimpleAuthorityMapper to make sure roles are not prefixed with ROLE_.
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
@Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver()
return new KeycloakSpringBootConfigResolver();
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy()
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
@Override
protected void configure(HttpSecurity http) throws Exception
super.configure(http);
if (isDisabled())
// Only for debugging purposes. Keycloak is off!
// Starting server with security off:
// mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Dcom.mycompany.myapp.server.security.DISABLED=true"
log.warn("STARTING WITHOUT WEB-SECURITY. ONLY FOR DEBUGGING!!!");
log.warn("STARTING WITHOUT WEB-SECURITY. ONLY FOR DEBUGGING!!!");
log.warn("STARTING WITHOUT WEB-SECURITY. ONLY FOR DEBUGGING!!!");
http
.csrf().disable()
.authorizeRequests().anyRequest().permitAll()
;
else
// Production
log.info("PRODUCTION MODE. Security is on.");
// Docu: https://docs.spring.io/spring-security/site/docs/current/reference/html5/
// Example: https://***.com/questions/52825679/how-to-use-hasrole-in-spring-security
http
.authorizeRequests()
.antMatchers(
"/resources/**",
"/about",
"/error",
"/sso/login",
"/lobby",
"/sandbox/**"
).permitAll()
.antMatchers("/component/admin").hasRole("myapp-manager")
.antMatchers("/component/**").hasRole("myapp-user")
//.antMatchers("/dbadmin").access("hasRole('myapp-user') and hasRole('myapp-db-admin')") // matching of >1 role
.anyRequest().authenticated()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler())
.logoutUrl("/sso/logout").permitAll()
.logoutSuccessUrl("/lobby")
//.and()
// .exceptionHandling().accessDeniedPage("/403")
;
private boolean isDisabled()
String x = System.getProperty(COM_MY_COMPANY_MY_APP_SERVER_SECURITY_DISABLED);
log.debug("isDisabled: Системно свойство =", COM_MY_COMPANY_MY_APP_SERVER_SECURITY_DISABLED, x);
return "true".equals(x);
我创建了一个 Servlet,用于调试和测试配置。它用于登录/注销并重定向到几个资源以测试授权。
import java.security.Principal;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("")
public class LobbyController
private Logger log = LoggerFactory.getLogger(getClass());
@ResponseStatus( HttpStatus.OK )
@GetMapping(path = "/lobby", produces = "text/html")
public String lobby(Principal principal, HttpServletRequest request, HttpServletResponse response)
log.debug("lobby");
log.trace("lobby: request.getAttributeNames: ", Collections.list(request.getAttributeNames()));
String info = "";
CsrfToken _csrf = (CsrfToken) request.getAttribute("_csrf");
log.debug("_csrf: , ", _csrf.getParameterName(), _csrf.getToken());
KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
boolean isLoggedIn = keycloakSecurityContext != null;
if (isLoggedIn)
KeycloakAuthenticationToken keycloakPrincipal = (KeycloakAuthenticationToken) principal;
SimpleKeycloakAccount details = (SimpleKeycloakAccount) keycloakPrincipal.getDetails();
info
= "<PRE>"
+ "<B>You are logged in.<B><BR/>"
+ "Realm: " + keycloakSecurityContext.getRealm() + "<BR/>"
+ "TokenString : <input value='" + keycloakSecurityContext.getTokenString() + "'/><BR/>"
+ "IdTokenString : <input value='" + keycloakSecurityContext.getIdTokenString() + "'/><BR/>"
+ "IdToken.Type : " + keycloakSecurityContext.getIdToken().getType() + "<BR/>"
+ "IdToken.Subject : " + keycloakSecurityContext.getIdToken().getSubject() + "<BR/>"
+ "IdToken.Id : " + keycloakSecurityContext.getIdToken().getId() + "<BR/>"
+ "IdToken.getAccessTokenHash : " + keycloakSecurityContext.getIdToken().getAccessTokenHash() + "<BR/>"
+ "IdToken.Email : " + keycloakSecurityContext.getIdToken().getEmail() + "<BR/>"
+ "IdToken.Name : " + keycloakSecurityContext.getIdToken().getName() + "<BR/>"
+ "IdToken.PreferredUsername : " + keycloakSecurityContext.getIdToken().getPreferredUsername()+ "<BR/>"
+ "principal : <textarea>" + principal.toString() + "</textarea><BR/>"
+ "principal.details.principal: " + details.getPrincipal() + "<BR/>"
+ "principal.details.roles : " + details.getRoles() + "<BR/>"
+ "</PRE>"
;
AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext();
if (authzContext!=null)
info
+= "<PRE>"
+ "authzContext.hasResourcePermission(/lobby) : " + authzContext.hasResourcePermission("/lobby") + "<BR/>"
+ "authzContext.hasResourcePermission(/component/person): " + authzContext.hasResourcePermission("/component/person") + "<BR/>"
+ "authzContext.hasResourcePermission(/component/admin) : " + authzContext.hasResourcePermission("/component/admin") + "<BR/>"
+ "authzContext.hasScopePermission(email) : " + authzContext.hasScopePermission("email") + "<BR/>"
+ "</PRE>"
;
else
info
= "<PRE>"
+ "<B>You are logged out.<B><BR/>"
+ "</PRE>"
;
info
+= "<A href='/component/person'>Person component</A><BR/>"
+ "<A href='/component/admin'>Admin component</A><BR/>"
+ "<A href='/sandbox/foo'>Sandbox</A><BR/>"
;
if (isLoggedIn)
info += "<FORM method='POST' action='/sso/logout'><input type='hidden' name='" + _csrf.getParameterName() + "' value='" + _csrf.getToken() + "' /><input type='submit' value='Log out'/></FORM><BR/>";
return info;
【讨论】:
以上是关于为啥 Spring Boot 不会重定向到 403 上的 Keycloak 登录页面?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我的 mvc 项目重定向到默认的 403 错误页面,而不是我重定向到的页面,而不是在本地的 IIS 上?
通过 Okta 进行身份验证后,会话 cookie 不会发送到 Spring Boot 应用程序
Spring Boot工程支持HTTP和HTTPS,HTTP重定向HTTPS