SpringCloudGateway针对白名单接口携带Token,网关直接认证失败问题解决
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;
}
}
3、携带错误token测试
**************************************************** 林深时见鹿,海蓝时见鲸 ****************************************************