1.Spring Cloud Gateway集成Spring Security 网关只进行服务级别的认证,不做授权逻辑判断
1.1 Spring Secuty核心配置 核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @EnableWebFluxSecurity public class WebSecurityConfiguration { @Autowired private JwtTokenAuthenticationManager jwtTokenAuthenticationManager; @Bean public SecurityWebFilterChain springSecurityFilterChain (ServerHttpSecurity http) { AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter (jwtTokenAuthenticationManager); authenticationWebFilter.setServerAuthenticationConverter(new JwtTokenAuthenticationConverter ()); authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository ()); http.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); http.addFilterBefore(new ClearExchangeSecurityContextWebFilter (), SecurityWebFiltersOrder.REACTOR_CONTEXT); http.cors().configurationSource(corsConfigurationSource()); http.csrf().disable(); http.authorizeExchange() .pathMatchers("/**/login" ).permitAll() .pathMatchers("/**/logout" ).authenticated() .pathMatchers("/upms/**" ).authenticated() .pathMatchers("/product/**" ).authenticated() .anyExchange().denyAll(); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource () { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (); CorsConfiguration config = new CorsConfiguration (); config.addAllowedHeader("*" ); config.addAllowedMethod("*" ); config.addAllowedOrigin("*" ); config.setAllowCredentials(true ); config.setMaxAge(3600L ); source.registerCorsConfiguration("/**" , config); return source; } }
上述鉴权配置使用Spring Security自带的AuthenticationWebFilter类,对其进行定制
1.2 定制ServerAuthenticationConverter,用于匹配并生成所需Authentication(即JwtTokenAuthenticationToken) 自定义JwtTokenAuthenticationConverter核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class JwtTokenAuthenticationConverter implements ServerAuthenticationConverter { @Override public Mono<Authentication> convert (ServerWebExchange exchange) { return apply(exchange); } public Mono<Authentication> apply (ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); String token = request.getHeaders().getFirst(HeaderNames.X_TOKEN); if (StringUtils.isBlank(token)) { return Mono.empty(); } return Mono.just(new JwtTokenAuthenticationToken (token)); } }
自定义JwtTokenAuthenticationToken核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class JwtTokenAuthenticationToken extends AbstractAuthenticationToken { private String token; private Long userId; public JwtTokenAuthenticationToken (Collection<? extends GrantedAuthority> authorities) { super (authorities); } public JwtTokenAuthenticationToken (String token) { super (null ); this .token = token; } public void setUserId (Long userId) { this .userId = userId; } public Long getUserId () { return this .userId; } @Override public Object getCredentials () { return token; } @Override public Object getPrincipal () { return null ; } @Override public boolean implies (Subject subject) { return false ; }
1.3 定制ReactiveAuthenticationManager,用于执行鉴权逻辑 自定义JwtTokenAuthenticationManager核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Slf4j @Component public class JwtTokenAuthenticationManager implements ReactiveAuthenticationManager { @Autowired private AuthFeignService authFeignService; @Override public Mono<Authentication> authenticate (Authentication authentication) throws AuthenticationException { JwtTokenAuthenticationToken requestToken = (JwtTokenAuthenticationToken) authentication; String token = (String) requestToken.getCredentials(); AuthDto authDto = new AuthDto (); authDto.setToken(token); authDto.setOnlyAuthenticate(true ); ResultVo<AuthVo> resultVo = authFeignService.auth(authDto); if (resultVo.getCode() != ErrorCodeEnum.SUCCESS.getCode() || !resultVo.getData().getAuthenticated()) { log.error("调用auth服务鉴权失败!" ); return Mono.error(new BadCredentialsException ("调用auth服务鉴权失败!" )); } List<GrantedAuthority> authorities = Collections.emptyList(); JwtTokenAuthenticationToken resultToken = new JwtTokenAuthenticationToken (authorities); resultToken.setAuthenticated(true ); resultToken.setUserId(resultVo.getData().getUserId()); return Mono.just(resultToken); } }
1.4 定制ServerSecurityContextRepository,用于存取SecurityContext(安全上下文) 由于Spring Cloud Gateway基于WebFlux构建,有别于javax servlet模型,不可使用基于ThreadLocal的SecurityContextHolder存取SecurityContext 可以使用Spring Security的基于session的WebSessionServerSecurityContextRepository类 代码见源码:WebSessionServerSecurityContextRepository
1.5 清除安全上下文 由于访问凭证使用的是jwt token方案,而Spring Security的WebSessionServerSecurityContextRepository会使用到session 出现一个问题,当执行一次请求,触发鉴权操作,会生成一个session,即使jwt token失效后或者不带上jwt token,浏览器会自动带上sessionid,而服务端的session可能还未失效,这时该次请求会从session中获取缓存的已认证通过的Authentication 解决方案之一:使session和jwt token的过期时间保持一致,配置Spring Security的logout以使调用登出接口时清除相应SecurityContext 解决方案之二:自定义ServerSecurityContextRepository,避开session 解决方案之三:每次请求结束前清除相应的SecurityContext
此处使用方案三,核心代码如下:
1 2 3 4 5 6 7 8 9 public class ClearExchangeSecurityContextWebFilter implements WebFilter { private ServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository (); @Override public Mono<Void> filter (ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange).then(securityContextRepository.save(exchange, null )); } }
网关做完鉴权操作后,要向下游服务传递所需的(Header)参数,比如userId 使用自定义的Spring Cloud Gateway的GlobalFilter
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class GatewayWebContextGlobalFilter implements GlobalFilter { @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { return ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) .filter(authentication -> authentication instanceof JwtTokenAuthenticationToken) .map(authentication -> (JwtTokenAuthenticationToken) authentication) .flatMap(authentication -> { String headerName = HeaderNames.X_USER_ID; String headerValue = String.valueOf(authentication.getUserId()); ServerHttpRequest request = exchange.getRequest().mutate().header(headerName, headerValue).build(); return chain.filter(exchange.mutate().request(request).build()); }) .switchIfEmpty(chain.filter(exchange)); } }
配置自定义GlobalFilter,核心代码如下:
1 2 3 4 5 6 7 8 @Configuration public class GatewayConfiguration { @Bean public GatewayWebContextGlobalFilter gatewayWebContextGlobalFilter () { return new GatewayWebContextGlobalFilter (); } }
2.下游服务集成Spring Security 下游服务做接口方法级别的鉴权 需要开启@EnableGlobalMethodSecurity 需要鉴权的接口方法添加@PreAuthorize注解
2.1 Spring Security核心配置 核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableWebSecurity public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private JwtTokenAuthenticationProvider jwtTokenAuthenticationProvider; @Override protected void configure (HttpSecurity http) throws Exception { http.csrf().disable(); http.cors().disable(); http.addFilterAfter(new JwtTokenAuthenticationFilter (authenticationManagerBean()), LogoutFilter.class); } @Override protected void configure (AuthenticationManagerBuilder auth) { auth.authenticationProvider(jwtTokenAuthenticationProvider); } @Bean @Override public AuthenticationManager authenticationManagerBean () throws Exception { return super .authenticationManagerBean(); } }
2.2 自定义Authentication 核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class JwtTokenAuthenticationToken extends AbstractAuthenticationToken { private String token; public JwtTokenAuthenticationToken (Collection<? extends GrantedAuthority> authorities) { super (authorities); } public JwtTokenAuthenticationToken (String token) { super (null ); this .token = token; } @Override public Object getCredentials () { return token; } @Override public Object getPrincipal () { return null ; } @Override public boolean implies (Subject subject) { return false ; } }
2.3 自定义Filter 核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Slf4j public class JwtTokenAuthenticationFilter extends OncePerRequestFilter { private AuthenticationManager authenticationManager; public JwtTokenAuthenticationFilter (AuthenticationManager authenticationManager) { this .authenticationManager = authenticationManager; } @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { logger.debug("doFilterInternal()" ); authenticateIfRequired(request); filterChain.doFilter(request, response); WebContextUtils.clear(); } private void authenticateIfRequired (HttpServletRequest request) { String token = request.getHeader(Common.TOKEN_HEADER_KEY); if (StringUtils.isBlank(token)) { log.debug("no token, as anonymous in later AnonymousAuthenticationFilter" ); SecurityContextHolder.clearContext(); return ; } Authentication authRequest = new JwtTokenAuthenticationToken (token); Authentication authResult = authenticationManager.authenticate(authRequest); SecurityContextHolder.getContext().setAuthentication(authResult); } }
2.4 自定义AuthenticationProvider 在网关已经做了认证,此处主要是做授权
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Slf4j @Component public class JwtTokenAuthenticationProvider implements AuthenticationProvider { @Autowired private AuthFeignService authFeignService; @Override public Authentication authenticate (Authentication authentication) throws AuthenticationException { JwtTokenAuthenticationToken requestToken = (JwtTokenAuthenticationToken) authentication; String token = (String) requestToken.getCredentials(); AuthDto authDto = new AuthDto (); authDto.setToken(token); ResultVo<AuthVo> resultVo = authFeignService.auth(authDto); if (resultVo.getCode() != ErrorCodeEnum.SUCCESS.getCode() || !resultVo.getData().getAuthenticated()) { log.error("调用auth服务鉴权失败!" ); throw new BaseException (ErrorCodeEnum.AUTH_FAIL, "调用auth服务鉴权失败!" ); } WebContextUtils.setToken(token); WebContextUtils.setUserId(resultVo.getData().getUserId()); AuthVo authVo = resultVo.getData(); List<String> listAuthority = authVo.getRoles().stream().map(s -> "ROLE_" + s).collect(Collectors.toList()); listAuthority.addAll(authVo.getResources()); String commaSeparatedRoles = String.join("," , listAuthority); List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(commaSeparatedRoles); JwtTokenAuthenticationToken resultToken = new JwtTokenAuthenticationToken (authorities); resultToken.setAuthenticated(true ); return resultToken; } @Override public boolean supports (Class<?> authentication) { return JwtTokenAuthenticationToken.class.isAssignableFrom(authentication); } }
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Slf4j public class ServiceWebContextFilter extends OncePerRequestFilter { @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader(HeaderNames.X_TOKEN); String userId = request.getHeader(HeaderNames.X_USER_ID); log.debug("WebContext.token: {}" , token); log.debug("WebContext.userId: {}" , userId); if (token != null ) { WebContextUtils.setToken(token); } if (userId != null ) { WebContextUtils.setUserId(Long.parseLong(userId)); } filterChain.doFilter(request, response); } }
配置提取Header参数的Filter,核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class ContextConfiguration { @Bean public FilterRegistrationBean<ServiceWebContextFilter> serviceWebContextFilter () { FilterRegistrationBean<ServiceWebContextFilter> registrationBean = new FilterRegistrationBean <>(); registrationBean.setFilter(new ServiceWebContextFilter ()); registrationBean.setOrder(Ordered.LOWEST_PRECEDENCE); return registrationBean; } }
3.网关跨域的问题 Spring Cloud Gateway集成Spring Security后出现跨域(Gateway配置中已经开启跨域配置并允许所有)
解决方案: Spring Security也添加跨域配置
1 2 3 4 5 6 7 8 9 10 11 12 @Bean public CorsConfigurationSource corsConfigurationSource () { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (); CorsConfiguration config = new CorsConfiguration (); config.addAllowedHeader("*" ); config.addAllowedMethod("*" ); config.addAllowedOrigin("*" ); config.setAllowCredentials(true ); config.setMaxAge(3600L ); source.registerCorsConfiguration("/**" , config); return source; }
仅添加以上配置即可(Spring Security会获取到),也可以额外显式设置一下,在 SecurityWebFilterChain 配置中: http.cors().configurationSource(corsConfigurationSource());
以上解决方案的原因: 按照网上的说法,Gateway的跨域处理较Spring Security靠后,Spring Security发现跨域问题后直接响应了 但是对Spring Security的cors进行关闭也无效:http.cors().disable(); 具体细节暂不清楚
4.网关OpenFeign调用错误的问题 错误信息:feign.codec.EncodeException: No qualifying bean of type ‘org.springframework.boot.autoconfigure.http.HttpMessageConverters’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)} 原因:OpenFeign与基于WebFlux的Gateway不能完美适配导致报错 解决方案:
1 2 3 4 5 @Bean @ConditionalOnMissingBean public HttpMessageConverters messageConverters (ObjectProvider<HttpMessageConverter<?>> converters) { return new HttpMessageConverters (converters.orderedStream().collect(Collectors.toList())); }
按照官方说法,以上解决方案有缺陷,因为HttpMessageConvert会阻塞,可能会造成WebFlux应用中断https://github.com/spring-cloud/spring-cloud-openfeign/issues/235
Spring官方目前推荐的做法是使用第三方的替代方案(feign-reactive https://github.com/Playtika/feign-reactive )https://cloud.spring.io/spring-cloud-openfeign/reference/html/#reactive-support
由于不熟悉后者的reactive编程,所以暂时仍然使用前者
5.版本
Spring Cloud Gateway 2.2.5
Spring Security 5.3.5