最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Extract JWT from Keycloak inside Spring Cloud Gateway filter - Stack Overflow

programmeradmin2浏览0评论

I have this gateway service which is used as a reverse proxy and is built on Spring Cloud Gateway.

It includes a RouteFilter class which uses Spring Cloud Gateway Filter to forward the request either to a custom login-service, to the requested service or proxied to an error-page service. This is based on evaluating a session token, which the login-service creates and stores in a DynamoDB. The RouteFilter also populates some headers with user info, roles for the given service, etc. The RouteFilter also first verifies that the service is healthy. The RouteFilter starts like below (header populating not included).

.
.
.
import .springframework.cloud.gateway.filter.GatewayFilterChain
import .springframework.cloud.gateway.filter.GlobalFilter
import .springframework.core.Ordered
import .springframework.stereotype.Component
import .springframework.web.reactive.function.client.WebClient
import .springframework.web.server.ResponseStatusException
import .springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono

@Component
class RouteFilter(
    private val config: Configuration,
    private val statusService: StatusService,
    private val authService: AuthService,
    private val webClient: WebClient = WebClient.create(config.loginUrl),
) : GlobalFilter,
    Ordered {
    override fun filter(
        exchange: ServerWebExchange,
        chain: GatewayFilterChain,
    ): Mono<Void> {
        val request = exchange.request
        val forwardedUrl = request.uri.toString().replaceFirst("http://", "https://")
        val serviceName = getServiceName(forwardedUrl)
        val sessionId = request.getToken(config.sessionCookieName)

        val verificationResult = authService.authenticate(sessionId)

        if (!statusService.isServiceHealthy(serviceName)) {
            throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE)
        }

        return when {
            verificationResult.isSuccessfulAndUserHasAccess(serviceName) ->
                sendUserToService(
                    serviceName,
                    exchange,
                    verificationResult.user!!,
                    chain,
                )

            verificationResult.sessionExpired() ->
                refreshSessionAndSendUserToService(
                    exchange,
                    forwardedUrl,
                    chain,
                )

            verificationResult.invalidSession() ->
                refreshSessionAndSendUserToService(
                    exchange,
                    forwardedUrl,
                    chain,
                )

            verificationResult.notSuccessful() -> sendUserToLogin(exchange, forwardedUrl, chain)
            else -> accessDenied(serviceName, verificationResult.user!!)
        }
    }
.
.
.

I want to replace the login-service with Keycloak and I looked at the article / and the repository to get the JWT when logging in with Keycloak. This works great.

package pl.piomin.samples.security.gateway;

import .slf4j.Logger;
import .slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

import .springframework.boot.SpringApplication;
import .springframework.boot.autoconfigure.SpringBootApplication;
import .springframework.security.oauth2.client.OAuth2AuthorizedClient;
import .springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import .springframework.web.bind.annotation.GetMapping;
import .springframework.web.bind.annotation.RestController;
import .springframework.web.server.WebSession;

@SpringBootApplication
@RestController
public class GatewayApplication {

    private static final Logger LOGGER = LoggerFactory.getLogger(GatewayApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

    @GetMapping(value = "/token")
    public Mono<String> getHome(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
        return Mono.just(authorizedClient.getAccessToken().getTokenValue());
    }

    @GetMapping("/")
    public Mono<String> index(WebSession session) {
        return Mono.just(session.getId());
    }
}

The JWT is accessible at /token with the roles the user has for the various clients. However I am not sure how I can apply this to the RouteFilter class in my other project. I am not sure if I need to get a hold of the JWT either. What I really need is to verify user has roles for the given service, populate these roles in a header, before sending request to service.mydomain (where the user originally came from). This is unless the user does not have any roles for the client, in which case it should be proxied to error-pages service.

How can I use Keycloak to log in and access the user token (JWT) inside the RouteFilter, so I can analyse it there and route to the app or proxy to the error-pages service as required?

Below is a sequence diagram trying to explain what I need, when a user has a role for the given client.

I have this gateway service which is used as a reverse proxy and is built on Spring Cloud Gateway.

It includes a RouteFilter class which uses Spring Cloud Gateway Filter to forward the request either to a custom login-service, to the requested service or proxied to an error-page service. This is based on evaluating a session token, which the login-service creates and stores in a DynamoDB. The RouteFilter also populates some headers with user info, roles for the given service, etc. The RouteFilter also first verifies that the service is healthy. The RouteFilter starts like below (header populating not included).

.
.
.
import .springframework.cloud.gateway.filter.GatewayFilterChain
import .springframework.cloud.gateway.filter.GlobalFilter
import .springframework.core.Ordered
import .springframework.stereotype.Component
import .springframework.web.reactive.function.client.WebClient
import .springframework.web.server.ResponseStatusException
import .springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono

@Component
class RouteFilter(
    private val config: Configuration,
    private val statusService: StatusService,
    private val authService: AuthService,
    private val webClient: WebClient = WebClient.create(config.loginUrl),
) : GlobalFilter,
    Ordered {
    override fun filter(
        exchange: ServerWebExchange,
        chain: GatewayFilterChain,
    ): Mono<Void> {
        val request = exchange.request
        val forwardedUrl = request.uri.toString().replaceFirst("http://", "https://")
        val serviceName = getServiceName(forwardedUrl)
        val sessionId = request.getToken(config.sessionCookieName)

        val verificationResult = authService.authenticate(sessionId)

        if (!statusService.isServiceHealthy(serviceName)) {
            throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE)
        }

        return when {
            verificationResult.isSuccessfulAndUserHasAccess(serviceName) ->
                sendUserToService(
                    serviceName,
                    exchange,
                    verificationResult.user!!,
                    chain,
                )

            verificationResult.sessionExpired() ->
                refreshSessionAndSendUserToService(
                    exchange,
                    forwardedUrl,
                    chain,
                )

            verificationResult.invalidSession() ->
                refreshSessionAndSendUserToService(
                    exchange,
                    forwardedUrl,
                    chain,
                )

            verificationResult.notSuccessful() -> sendUserToLogin(exchange, forwardedUrl, chain)
            else -> accessDenied(serviceName, verificationResult.user!!)
        }
    }
.
.
.

I want to replace the login-service with Keycloak and I looked at the article https://piotrminkowski/2020/10/09/spring-cloud-gateway-oauth2-with-keycloak/ and the repository https://github/piomin/sample-spring-security-microservices to get the JWT when logging in with Keycloak. This works great.

package pl.piomin.samples.security.gateway;

import .slf4j.Logger;
import .slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

import .springframework.boot.SpringApplication;
import .springframework.boot.autoconfigure.SpringBootApplication;
import .springframework.security.oauth2.client.OAuth2AuthorizedClient;
import .springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import .springframework.web.bind.annotation.GetMapping;
import .springframework.web.bind.annotation.RestController;
import .springframework.web.server.WebSession;

@SpringBootApplication
@RestController
public class GatewayApplication {

    private static final Logger LOGGER = LoggerFactory.getLogger(GatewayApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

    @GetMapping(value = "/token")
    public Mono<String> getHome(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
        return Mono.just(authorizedClient.getAccessToken().getTokenValue());
    }

    @GetMapping("/")
    public Mono<String> index(WebSession session) {
        return Mono.just(session.getId());
    }
}

The JWT is accessible at /token with the roles the user has for the various clients. However I am not sure how I can apply this to the RouteFilter class in my other project. I am not sure if I need to get a hold of the JWT either. What I really need is to verify user has roles for the given service, populate these roles in a header, before sending request to service.mydomain (where the user originally came from). This is unless the user does not have any roles for the client, in which case it should be proxied to error-pages service.

How can I use Keycloak to log in and access the user token (JWT) inside the RouteFilter, so I can analyse it there and route to the app or proxy to the error-pages service as required?

Below is a sequence diagram trying to explain what I need, when a user has a role for the given client.

Share Improve this question edited Mar 29 at 9:51 Cornelius asked Mar 28 at 15:59 CorneliusCornelius 3612 gold badges7 silver badges14 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

How can I use Keycloak to log in

with oauth2Login from spring-security-oauth2-client. The Boot starter for that is spring-boot-starter-oauth2-client.

access the user token (JWT)

In the token response at you get within the authorization code flow, you don't get one, but usually 3 tokens: ID, access, and refresh.

ID tokens are always JWTs. Their audience are OAuth2 clients (application with oauth2Login are clients).

Access tokens may be JWTs (it is with Keycloak). Their audience are OAuth2 resource servers and clients should not try to read it. The only Use of a client for an access token should be for authorizing requests to resource servers.

a RouteFilter class which uses Spring Cloud Gateway Filter to forward the request either to a custom login-service, to the requested service or proxied to an error-page service.

Instead of rewriting such a custom filter, you should consider using:

  • oauth2Login to delegate authentication to Keycloak and store tokens in session
  • the tokenRelay= filter to automatically replace session-based authorization with bearer-based authorization when routing a request from the front end to an oauth2ResourceServer: get the JWT access token in session and set it as Authorization header
  • configure downstream services with oauth2ResourceServer: decode the JWT access token in the Authorization header and take access dontrol decision based on the claims it contains and accessed resource

I wrote a detailed tutorial for that on Baeldung.

发布评论

评论列表(0)

  1. 暂无评论