天天看點

SpringCloudGateway針對白名單接口攜帶Token,網關直接認證失敗問題解決

SpringCloudGateway針對白名單接口攜帶Token,網關直接認證失敗問題解決

1、問題描述

  之前使用SpringCloudGateway整合SpringSecurity進行Oauth2的認證授權操作時,由于需要在網關設定白名單,進而針對白名單的URL不需要進行認證授權,直接放行,在項目開發過程中,發現存在一個問題,就是白名單的路徑接口不攜帶token通路時,可以正常通路,網關放行,但是當白名單路徑的接口在請求頭中攜帶不正确的token進行通路時,網關會直接報認證失敗。

說明:下圖隻是為了描述問題所用,不必糾結。

圖一:白名單不攜帶token通路,網關正常放行

SpringCloudGateway針對白名單接口攜帶Token,網關直接認證失敗問題解決

圖二:白名單請求攜帶不正确的token通路,網關認證失敗

SpringCloudGateway針對白名單接口攜帶Token,網關直接認證失敗問題解決

 2、問題解決

  由于白名單請求不攜帶token通路,網關可以正常放行,那麼可不可以在白名單通路時,直接移除請求頭中的Authorization資訊,重寫請求通路呢,後來查閱相關資料,發現此方案可行,具體操作如下,僅供參考!

2.1、在SpringCloudGateway網關項目添加自定義過濾器

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;

/**
 * 白名單路徑通路時需要移除請求頭認證資訊
 *
 * @author 星空流年
 */
@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();
        //白名單路徑移除請求頭認證資訊
        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);
    }
}      

備注:擷取白名單配置資訊類如下

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

import java.util.List;

/**
 * 白名單放行路徑
 *
 * @author 星空流年
 */
@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、在預設的認證過濾器之前添加自定義的過濾器

  把自定義的過濾器配置到預設的認證過濾器之前,在ResourceServerConfig中進行配置。

  ResourceServerConfig這裡做的工作是将鑒權管理器AuthorizationManager配置到資源伺服器,進行請求白名單放行、無權通路和無效token的自定義異常響應等操作。

  注意:自定義過濾器添加位置

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;

/**
 * 資源伺服器配置
 *
 * @author 星空流年
 */
@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();
    }

    /**
     * 未授權
     *
     * @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(), "通路未授權, 請确認令牌有效性!"));
                                             log.error("通路未授權, 響應資訊為: {}", body);
                                             DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
                                             return response.writeWith(Mono.just(buffer)).doOnError(error -> DataBufferUtils.release(buffer));
                                         });
    }

    /**
     * token無效或者已過期自定義響應
     *
     * @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(), "令牌缺失或者無效或者已過期,請确認!"));
                                        log.error("令牌缺失或者無效或者已過期, header:{},響應資訊為: {}", 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));
                                    });
    }

    /**
     * 重新定義R 權限管理器
     * <p>
     * 說明:
     * ServerHttpSecurity沒有将jwt中authorities的負載部分當做Authentication
     * 需要把jwt的Claim中的authorities加入
     * 方案:重新定義R 權限管理器,預設轉換器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);
    }

    /**
     * 設定響應資訊
     *
     * @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、攜帶錯誤token測試

SpringCloudGateway針對白名單接口攜帶Token,網關直接認證失敗問題解決

參考資料:

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

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

遨遊在代碼世界裡的一條不知名的小船。。。。。。

上一篇: 硬體斷點hook