spring cloud重点:gateway

上回解释了一下spring-security,这回讲spring-cloud-gateway,这是所有微服务性能的核心

先说阴谋阳谋,先说结论:阴谋诛心,阳谋论事,阴谋过多则死气沉沉,沆瀣一气,人人自危,阳谋过多就积重难返,争吵不休,不利团结

以昨天对话为例:

其中的阴阳应该很容易分辨,阴话话术不给具体问题,给你扣帽子,自身逍遥事外,
让你自我怀疑,类似PUA,阳话话术老子天下无敌,啥也不惧,打架的架势
可怜现在职场中阴风阵阵,有事不说,先扣帽子,你可以发现哪儿都死气沉沉,这就
是职场现状

著名阴谋例子,阿里,出来的所谓管理是将人分成老牛型,兔子型,疯狗
型,反正都是畜生,而一旦你接受了这种设定,那组织就很稳定了

阳谋例子,三个代表,共产党是这么说的,也是这么在做,所以叫阳谋,而一旦你接
受了这种设定,那党行事快速便捷,同时也有隐患,毕竟并不是每个人都能代表群
众,出事了不利团结

讲这个是为了防止被人pua,远离过阴或过阳的人(否则要么精神上被折磨要么肉体上被折磨)

spring-cloud-gateway没什么可难的,但是在微服务体系内它是作为oauth2客户端存在的

先讲依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-core</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-jose</artifactId>
    </dependency>
</dependencies>

因为微服务种网关的安全性必须依赖认证系统,因此在微服务种作为oauth2客户端

具体做法如下:

认证服务器为https,又使用自己颁发证书的时候,加入信任列表

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Properties;

import javax.annotation.PostConstruct;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.Data;

@Data
@Component
@ConfigurationProperties(prefix = "cert")
@ConditionalOnProperty(prefix = "cert", name = "enabled", havingValue = "true")
public class CertConfig {

    private String trustStore;
    private String trustStorePassword;
    private @Autowired ObjectMapper objectMapper;

    @PostConstruct
    public void init() throws FileNotFoundException, JsonProcessingException {
        if(StringUtils.isEmpty(trustStore)) {
            trustStore = "classpath:trust.jks";
            trustStorePassword = "123456";
        }
        File file = ResourceUtils.getFile(trustStore);
        Properties systemProps = System.getProperties();
        systemProps.put("javax.net.ssl.trustStore", file.getAbsolutePath());
        systemProps.put("javax.net.ssl.trustStorePassword", trustStorePassword);
        System.setProperties(systemProps);
    }

}

当你需要刷新token的时候,过滤器(国外某大神的作品,必须设置reuseToken为false,不然会出错,不过应该是spring security的问题)

import java.time.Duration;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

/**
 * Token Relay Gateway Filter with Token Refresh. This can be removed when issue {@see https://github.com/spring-cloud/spring-cloud-security/issues/175} is closed.
 * Implementierung in Anlehnung an {@link ServerOAuth2AuthorizedClientExchangeFilterFunction}
 */
@Component
public class TokenRelayWithTokenRefreshGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
	
    private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager;

    private static final Duration accessTokenExpiresSkew = Duration.ofSeconds(6);

    public TokenRelayWithTokenRefreshGatewayFilterFactory(ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
                                                          ReactiveClientRegistrationRepository clientRegistrationRepository) {
        super(Object.class);
        this.authorizedClientManager = createDefaultAuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
    }

    private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

        final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .clientCredentials(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .password(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .build();
        final DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

    public GatewayFilter apply() {
        return apply((Object) null);
    }

    @Override
    public GatewayFilter apply(Object config) {
    	
        return (exchange,chain) -> exchange.getPrincipal()
        		//.log("token-relay-filter")
        		.filter(principal->principal instanceof OAuth2AuthenticationToken)
        		.cast(OAuth2AuthenticationToken.class)
        		.flatMap(this::authorizeClient)
        		.map(OAuth2AuthorizedClient::getAccessToken)
        		.map(token -> withBearerAuth(exchange, token))
        		// TODO: adjustable behavior if empty
        		.defaultIfEmpty(exchange).flatMap(chain::filter);
    }

    private ServerWebExchange withBearerAuth(ServerWebExchange exchange, OAuth2AccessToken accessToken) {
        return exchange.mutate().request(r -> r.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue()))).build();
    }

    private Mono<OAuth2AuthorizedClient> authorizeClient(OAuth2AuthenticationToken oAuth2AuthenticationToken) {
        final String clientRegistrationId = oAuth2AuthenticationToken.getAuthorizedClientRegistrationId();
        return Mono.defer(() -> authorizedClientManager.authorize(createOAuth2AuthorizeRequest(clientRegistrationId, oAuth2AuthenticationToken)));
    }

    private OAuth2AuthorizeRequest createOAuth2AuthorizeRequest(String clientRegistrationId, Authentication principal) {
        return OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId).principal(principal).build();
    }
}

需要在网关登出时候,必备

import java.net.URI;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;

@EnableWebFluxSecurity
public class WebFluxSecurityConfig {
	
	@Value("${auth.logout}")
	private String logout;
	@Bean
	SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {

		RedirectServerLogoutSuccessHandler handler = new RedirectServerLogoutSuccessHandler();
		handler.setLogoutSuccessUrl(URI.create(logout));
		
		http.authorizeExchange((exchanges) -> exchanges
				.pathMatchers("/actuator/**", "/oauth/**").permitAll()
				.anyExchange().authenticated()).csrf().disable().oauth2Login().and()
				.cors().and().logout().logoutSuccessHandler(handler);
		return http.build();
	}

}

开发时若需要显示服务列表时,可自己写一个页面列出服务,@Profile(“dev”)表示只在dev环境存在,route是自己写的类,懂的自己自然会写

import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.Setter;
import sample.domain.Route;

@Controller
@ConfigurationProperties(prefix = "spring.cloud.gateway")
@Setter
@Profile("dev")
public class GatewayIndexDevController {

	private List<Route> routes;
	
	@GetMapping("/")
	public String demo(Model model) {
		for (Route route : routes) {
			if ("auth-server".equals(route.getId())) {
				continue;
			}
			String[] paths = route.getPredicates();
			String path = paths[0];

			route.setPath(path.substring(path.indexOf("/")).replace("**", "swagger-ui/index.html"));
		}
		model.addAttribute("routes", routes);
		return "index";
	}

}

由于gateway作为oauth2客户端,配置稍微复杂点,如下,意思是这样(auth-server和gateway安装在k8s)

auth:
   server: https://auth.server
   server-k8s: http://gateway-auth:8080
   logout: ${auth.server}/oauth/exit
server:
   http2:
      enabled: true
   ssl:
      enabled: true
      key-store: classpath:gateway.sample.p12
      key-store-password: 123456
      keyStoreType: PKCS12
spring:
   thymeleaf:
      cache: false
   security.oauth2.client:
      registration:
         auth-server:
            client_id: gateway-sample
            client_secret: 1234
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: all
      provider:
         auth-server:
            authorization-uri: ${auth.server}/oauth/authorize
            token-uri: ${auth.server-k8s}/oauth/token
            jwk-set-uri: ${auth.server-k8s}/oauth/token_key
            user-info-uri: ${auth.server-k8s}/oauth/userInfo
            user-name-attribute: name
   cloud.gateway.routes:
   -  id: websocket
      uri: http://192.168.108.1:8010
      predicates:
      -  Path=/websocket/**
      filters:
      -  StripPrefix=1
      -  TokenRelayWithTokenRefresh=
      -  RemoveRequestHeader=Cookie

最后的效果如图示:

输入地址:
img.png
跳转至auth-server

登陆之后,是swagger列表,当打开相应的连接,就是资源服务器api的swagger文档