The spring cloud gateway carries a Token for the whitelist interface, and the direct authentication of the gateway fails. The problem is solved

Posted by Michael Lasky on Fri, 21 Jan 2022 13:36:53 +0100

The spring cloud gateway carries a Token for the whitelist interface, and the direct authentication of the gateway fails. The problem is solved

1. Problem description

Previously, when using SpringCloudGateway to integrate SpringSecurity for authentication and authorization of Oauth2, because the white list needs to be set in the gateway, the URL of the white list does not need authentication and authorization and is released directly. During the project development process, it is found that there is a problem that when the path interface of the white list does not carry a token for access, it can be accessed normally and released by the gateway, However, when the interface of the white list path carries an incorrect token in the request header for access, the gateway will directly report authentication failure.

Note: the following figure is only used to describe the problem and does not need to be tangled.

Figure 1: the white list does not carry token access, and the gateway is released normally

Figure 2: the white list request carries incorrect token access, and the gateway authentication fails

2. Problem solving

Since the white list request does not carry token access, and the gateway can release normally, can you directly remove the Authorization information in the request header and rewrite the request access during the white list access? Later, after consulting the relevant materials, it is found that this scheme is feasible. The specific operations are as follows for reference only!

2.1. Add a custom filter in the spring cloud Gateway project

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * The request header authentication information needs to be removed when accessing the whitelist path
 *
 * @author Starry sky fleeting year
 */
@Component
public class WhiteListAuthorizationFilter implements WebFilter {

    @Resource
    private WhiteListProperties properties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        PathMatcher pathMatcher = new AntPathMatcher();
        //Whitelist path remove request header authentication information
        List<String> urls = properties.getUrls();
        for (String url : urls) {
            if (pathMatcher.match(url, path)) {
                request = exchange.getRequest().mutate().header(HttpHeaders.AUTHORIZATION, "").build();
                exchange = exchange.mutate().request(request).build();
                return chain.filter(exchange);
            }
        }
        return chain.filter(exchange);
    }
}

Note: the whitelist configuration information class is as follows

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * White list release path
 *
 * @author Starry fleeting year
 */
@ConfigurationProperties(prefix = "whitelist")
@Component
@RefreshScope
public class WhiteListProperties {
    private List<String> urls;

    public List<String> getUrls() {
        return urls;
    }

    public void setUrls(List<String> urls) {
        this.urls = urls;
    }

    @Override
    public String toString() {
        return "WhiteListProperties{" +
                "urls=" + urls +
                '}';
    }
}

2.2. Add a custom filter before the default authentication filter

Before configuring the custom filter to the default authentication filter, configure it in ResourceServerConfig.

The work of ResourceServerConfig here is to configure the authentication manager AuthorizationManager to the resource server to request white list release, unauthorized access and user-defined exception response of invalid token.

Note: where to add a custom filter

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;

/**
 * Resource server configuration
 *
 * @author Starry fleeting year
 */
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {

    @Resource
    private AuthorizationManager authorizationManager;

    @Resource
    private WhiteListProperties properties;

    @Resource
    private WhiteListAuthorizationFilter authenticationFilter;

    private static final Logger log = LoggerFactory.getLogger(ResourceServerConfig.class);

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
        http.oauth2ResourceServer().authenticationEntryPoint(authenticationEntryPoint());
        http.addFilterBefore(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);

        http.authorizeExchange().pathMatchers(ArrayUtil.toArray(properties.getUrls(), String.class)).permitAll().anyExchange().access(authorizationManager)
            .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler()).authenticationEntryPoint(authenticationEntryPoint())
            .and().csrf().disable();

        return http.build();
    }

    /**
     * Unauthorized
     *
     * @return
     */
    @Bean
    ServerAccessDeniedHandler accessDeniedHandler() {
        return (exchange, denied) -> Mono.defer(() -> Mono.just(exchange.getResponse()))
                                         .flatMap(response -> {
                                             response = responseInfo(response);
                                             String body = JSONUtil.toJsonStr(Result.fail(RestStatus.INVALID_TOKEN.getCode(), "Access not authorized, Please confirm the validity of the token!"));
                                             log.error("Access not authorized, The response information is: {}", body);
                                             DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
                                             return response.writeWith(Mono.just(buffer)).doOnError(error -> DataBufferUtils.release(buffer));
                                         });
    }

    /**
     * token Invalid or expired custom response
     *
     * @return
     */
    @Bean
    ServerAuthenticationEntryPoint authenticationEntryPoint() {
        return (exchange, e) -> Mono.defer(() -> Mono.just(exchange.getResponse()))
                                    .flatMap(response -> {
                                        response = responseInfo(response);
                                        String body = JSONUtil.toJsonStr(Result.fail(RestStatus.INVALID_TOKEN.getCode(), "The token is missing, invalid or expired, please confirm!"));
                                        log.error("The token is missing or invalid or has expired, header:{},The response information is: {}", exchange.getRequest().getHeaders(), body);
                                        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
                                        return response.writeWith(Mono.just(buffer)).doOnError(error -> DataBufferUtils.release(buffer));
                                    });
    }

    /**
     * Redefine R rights manager
     * <p>
     * explain:
     * ServerHttpSecurity The payload part of the authorities in jwt is not treated as Authentication
     * You need to add the authorities in jwt's Claim
     * Scheme: redefine the R permission manager, and the default converter is JwtGrantedAuthoritiesConverter
     *
     * @return
     */
    @Bean
    public Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstants.AUTHORITY_PREFIX);
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstants.AUTHORITY_CLAIM_NAME);

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }

    /**
     * Set response information
     *
     * @param response
     * @return
     */
    private ServerHttpResponse responseInfo(ServerHttpResponse response) {
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getHeaders().set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        response.getHeaders().set(HttpHeaders.CACHE_CONTROL, "no-cache");
        return response;
    }
}

3. Carry error token Test

reference material:

1,https://www.cnblogs.com/summerday152/p/13635948.html

2,https://juejin.cn/post/7036297405326688287

Topics: Spring Boot