SpringCloudGateway針對白名單接口攜帶Token,網關直接認證失敗問題解決
1、問題描述
之前使用SpringCloudGateway整合SpringSecurity進行Oauth2的認證授權操作時,由於需要在網關設置白名單,從而針對白名單的URL不需要進行認證授權,直接放行,在項目開發過程中,發現存在一個問題,就是白名單的路徑接口不攜帶token訪問時,可以正常訪問,網關放行,但是當白名單路徑的接口在請求頭中攜帶不正確的token進行訪問時,網關會直接報認證失敗。
說明:下圖只是為了描述問題所用,不必糾結。
圖一:白名單不攜帶token訪問,網關正常放行
圖二:白名單請求攜帶不正確的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; } }