尽管设置了承载令牌,但 Spring Cloud Gateway 重定向到 Keycloak 登录页面

Posted

技术标签:

【中文标题】尽管设置了承载令牌,但 Spring Cloud Gateway 重定向到 Keycloak 登录页面【英文标题】:Spring Cloud Gateway redirects to Keycloak login page although Bearer token is set 【发布时间】:2020-10-13 06:10:58 【问题描述】:

我正在使用 Keycloak 作为身份提供者、Spring Cloud Gateway 作为 API 网关和多个微服务的设置。 我可以通过http://localhost:8050/auth/realms/dev/protocol/openid-connect/token 的网关(重定向到 Keycloak)接收 JWT。

我可以使用 JWT 访问直接位于 Keycloak 服务器上的资源(例如 http://localhost:8080/auth/admin/realms/dev/users)。 但是当我想使用网关将我中继到相同的资源 (http://localhost:8050/auth/admin/realms/dev/users) 时,我会收到 Keycloak 登录表单作为响应。

我的结论是我的 Spring Cloud Gateway 应用程序中一定存在错误配置。

这是网关中的安全配置:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration 

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) 

        // Authenticate through configured OpenID Provider
        http.oauth2Login();

        // Also logout at the OpenID Connect provider
        http.logout(logout -> logout.logoutSuccessHandler(
                new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));

        //Exclude /auth from authentication
        http.authorizeExchange().pathMatchers("/auth/realms/ahearo/protocol/openid-connect/token").permitAll();

        // Require authentication for all requests
        http.authorizeExchange().anyExchange().authenticated();

        // Allow showing /home within a frame
        http.headers().frameOptions().mode(Mode.SAMEORIGIN);

        // Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
        http.csrf().disable();
        return http.build();
    

这是我在网关中的 application.yaml:

spring:
  application:
    name: gw-service
  cloud:
    gateway:
      default-filters:
        - TokenRelay
      discovery:
        locator:
          lower-case-service-id: true
          enabled: true
      routes:
        - id: auth
          uri: http://localhost:8080
          predicates:
            - Path=/auth/**

  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: 'api-gw'
            client-secret: 'not-relevant-but-correct'
            authorizationGrantType: authorization_code
            redirect-uri: 'baseUrl/login/oauth2/code/registrationId'
            scope: openid,profile,email,resource.read
        provider:
          keycloak:
            issuerUri: http://localhost:8080/auth/realms/dev
            user-name-attribute: preferred_username

server:
  port: 8050
eureka:
  client:
    service-url:
      default-zone: http://localhost:8761/eureka
    register-with-eureka: true
    fetch-registry: true

如何让网关知道用户已通过身份验证(使用 JWT)而不会将我重定向到登录页面?

【问题讨论】:

能否请您告知您是如何绕过登录页面实现的? 把这个改成:authorization-grant-type: authorization_code 你能绕过登录页面吗?如果是,请与我们分享如何? 【参考方案1】:

如果您想使用访问令牌向 Spring Gateway 发出请求,您需要将其设为资源服务器。添加以下内容:

pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

application.yml

  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://.../auth/realms/...

SecurityConfiguration.java

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                        ReactiveClientRegistrationRepository clientRegistrationRepository) 
    // Authenticate through configured OpenID Provider
    http.oauth2Login();
    // Also logout at the OpenID Connect provider
    http.logout(logout -> logout.logoutSuccessHandler(
            new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
    // Require authentication for all requests
    http.authorizeExchange().anyExchange().authenticated();

    http.oauth2ResourceServer().jwt();

    // Allow showing /home within a frame
    http.headers().frameOptions().mode(Mode.SAMEORIGIN);
    // Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
    http.csrf().disable();
    return http.build();

【讨论】:

能否请您在这里指导我 - github.com/SaiUpadhyayula/springboot-microservices-project/…?请查看最后的 cmets【参考方案2】:

我绕过了这个问题,直接与 Keycloak 通信,而不通过 Spring Cloud Gateway 将请求转发给它。

这实际上不是一种解决方法,但实际上是最佳实践/据我所知完全可以。

【讨论】:

无需代码,直接寻址您的 Keycloak 服务器即可。所以不要使用api.domain.com(让API将您的请求重定向到Keycloak)使用keycloak.domain.com(直接将请求发送到Keycloak实例)。【参考方案3】:

此代码用于Client_credentialsgrant_type。如果您使用其他授权类型,则需要在请求参数中添加client_idclient_secret

public class MyFilter2 extends OncePerRequestFilter 

    private final ObjectMapper mapper = new ObjectMapper();

    @Value("$auth.server.uri")
    private String authServerUri;

    @Value("$client_id")
    private String clientId;
    @Value("$client_secret")
    private String clientSecret;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
            FilterChain filterChain) throws IOException 
        try 
            String token = httpServletRequest.getHeader("Authorization");

            HttpHeaders headers = new HttpHeaders();
            headers.set("Content-Type","application/x-www-form-urlencoded");
            headers.set("Authorization",token);

            final HttpEntity finalRequest = new HttpEntity("", headers);
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> response = restTemplate.postForEntity(authServerUri,finalRequest,String.class);
            if (!HttpStatus.OK.equals(response.getStatusCode())) 
                Map<String, Object> errorDetails = new HashMap<>();
                errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
                errorDetails.put("message", "Invalid or empty token");

                httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
                httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

                mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
             else 
                    filterChain.doFilter(httpServletRequest, httpServletResponse);
            
        catch(HttpClientErrorException he) 
            Map<String, Object> errorDetails = new HashMap<>();
            errorDetails.put("status", HttpStatus.UNAUTHORIZED.value());
            errorDetails.put("message", "Invalid or empty token");

            httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

            mapper.writeValue(httpServletResponse.getWriter(), errorDetails);
        catch (Exception exception) 
    

【讨论】:

以上是关于尽管设置了承载令牌,但 Spring Cloud Gateway 重定向到 Keycloak 登录页面的主要内容,如果未能解决你的问题,请参考以下文章

这样的 Spring Cloud 微服务项目太牛了!

忽略 Spring Boot 中特定 Url 的承载令牌验证

即使端点设置为 permitAll,也会检查承载令牌

Spring Boot Keycloak - 如何验证承载令牌?

spring cloud Feign oauth2令牌传递

如何使用 jwt 公钥在 Spring Boot 中验证承载访问令牌