Spring Cloud进阶之路 | 十四:服务网关重构(FactoryBean、动态配置、token解析、user传递)

​前言

前面讲的服务网关Spring Cloud进阶之路 | 七:服务网关(zuul)Spring Cloud进阶之路 | 十:服务网关整合安全框架(zuul+ Spring Cloud Oauth2),其中的代码比较乱。

再者,不能实现相关属性的动态配置,前置身份认证过滤器创建和运行逻辑深度耦合,如后续添加其它内容,必然不易扩展。

所以,今天就这些问题,对网关代码进行重构,以实现创建、运行分离,相关属性动态配置。

准备工作

复用Spring Cloud进阶之路 | 十:服务网关整合安全框架(zuul+ Spring Cloud Oauth2)文章中的网关工程xmall-zuul、商品工程xmall-product、授权工程xmall-auth。

管理Oauth2端点

创建Oauth2端点常量接口。

package com.luas.xmall.gateway.oauth2.endpoint;
​
public interface AuthorizationServerEndpoints {
​
    String AUTHORIZATION_ENDPOINT = "/oauth/authorize";
​
    String TOKEN_ENDPOINT = "/oauth/token";
​
    String CHECK_TOKEN_ENDPOINT = "/oauth/check_token";
​
    String TOKEN_KEY_ENDPOINT = "/oauth/token_key";
​
    String USER_INFO_ENDPOINT = "/oauth/user";
​
    String USER_APPROVAL_ENDPOINT = "/oauth/confirm_access";
​
    String ERROR_ENDPOINT = "/oauth/error";
​
}

改造PreAuthenticationFilter

修改PreAuthenticationFilter,新增token解析、请求匹配、token信息和user信息传递等功能。

package com.luas.xmall.gateway.filter;
​
import com.alibaba.fastjson.JSONObject;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import org.springframework.security.oauth2.provider.authentication.TokenExtractor;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.CollectionUtils;
​
import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
​
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
​
public class PreAuthenticationFilter extends ZuulFilter {
​
    private static final int DEFAULT_FILTER_ORDER = 0;
​
    private Logger logger = LoggerFactory.getLogger(getClass());
​
    private Set<AntPathRequestMatcher> ignoreAntPathRequestMatchers = new HashSet<>();
​
    private int order = DEFAULT_FILTER_ORDER;
​
    private ResourceServerTokenServices tokenServices;
​
    private TokenExtractor tokenExtractor = new BearerTokenExtractor();
​
    @Override
    public String filterType() {
        return PRE_TYPE;
    }
​
    @Override
    public int filterOrder() {
        return order;
    }
​
    @Override
    public boolean shouldFilter() {
        return !isIgnoreRequest();
    }
​
    @Override
    public Object run() throws ZuulException {
​
        RequestContext requestContext = RequestContext.getCurrentContext();
​
        HttpServletRequest request = requestContext.getRequest();
​
        this.logger.debug("processing uri {}", request.getRequestURI());
​
        Authentication authentication = this.tokenExtractor.extract(request);
​
        if (authentication == null || authentication.getPrincipal() == null) {
            //不会继续往下执行 不会调用服务接口了 网关直接响应给客户了
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseBody("Full authentication is required to access this resource");
            requestContext.setResponseStatusCode(401);
            return null;
        }
​
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("tokenServices class is {}", this.tokenServices.getClass().getName());
            this.logger.debug("authentication class is {}", authentication.getClass().getName());
            this.logger.debug("authentication is {}", JSONObject.toJSONString(authentication));
        }
​
        String accessToken = (String) authentication.getPrincipal();
​
        /* 解析token,将user信息放入request,传递给下游微服务 */
​
        // 解析token
        OAuth2Authentication oAuth2Authentication = this.tokenServices.loadAuthentication(accessToken);
​
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("OAuth2Authentication class is {}", oAuth2Authentication.getClass().getName());
            this.logger.debug("OAuth2Authentication is {}", JSONObject.toJSONString(oAuth2Authentication));
        }
​
        // 根据token获取user信息
​
        // 将user信息放入request,传递给下游微服务
        // requestContext.addZuulRequestHeader("", "");
        return null;
    }
​
    private boolean isIgnoreRequest() {
        RequestContext requestContext = RequestContext.getCurrentContext();
​
        HttpServletRequest request = requestContext.getRequest();
​
        for (AntPathRequestMatcher matcher : ignoreAntPathRequestMatchers) {
            if (matcher.matches(request)) {
                return true;
            }
        }
​
        return false;
    }
​
    private Set<String> obtainAuthorities(OAuth2Authentication authentication) {
        if (CollectionUtils.isEmpty(authentication.getAuthorities())) {
            return null;
        }
​
        return authentication.getAuthorities().stream().map(authority -> authority.toString()).collect(Collectors.toSet());
    }
​
    public Set<AntPathRequestMatcher> getIgnoreAntPathRequestMatchers() {
        return ignoreAntPathRequestMatchers;
    }
​
    public void setIgnoreAntPathRequestMatchers(Set<AntPathRequestMatcher> ignoreAntPathRequestMatchers) {
        this.ignoreAntPathRequestMatchers = ignoreAntPathRequestMatchers;
    }
​
    public int getOrder() {
        return order;
    }
​
    public void setOrder(int order) {
        this.order = order;
    }
​
    public ResourceServerTokenServices getTokenServices() {
        return tokenServices;
    }
​
    public void setTokenServices(ResourceServerTokenServices tokenServices) {
        this.tokenServices = tokenServices;
    }
​
    public TokenExtractor getTokenExtractor() {
        return tokenExtractor;
    }
​
    public void setTokenExtractor(TokenExtractor tokenExtractor) {
        this.tokenExtractor = tokenExtractor;
    }
​
}

其它内容,如解析token、根据token获取用户名、根据用户名获取用户信息(基本信息、权限信息、机构信息)等,可自行根据情况实现。然后通过header方式,将用户等信息传递给下游微服务,下游微服务再通过request获取。

创建PreAuthenticationFilterFactoryBean


创建PreAuthenticationFilterFactoryBean,用以实现过滤器执行和创建逻辑分离,降低耦合。PreAuthenticationFilter只负责执行相关业务逻辑,PreAuthenticationFilterFactoryBean则负责创建PreAuthenticationFilter的创建。

package com.luas.xmall.gateway.filter;
​
import com.luas.xmall.gateway.oauth2.endpoint.AuthorizationServerEndpoints;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import org.springframework.security.oauth2.provider.authentication.TokenExtractor;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
​
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
​
public class PreAuthenticationFilterFactoryBean implements FactoryBean<PreAuthenticationFilter>, InitializingBean {
​
    private static final int DEFAULT_FILTER_ORDER = 0;
​
    private String zuulPrefix;
​
    private Set<String> ignoreAntPatterns = new HashSet<>();
​
    private transient Set<AntPathRequestMatcher> ignoreAntPathRequestMatchers = new HashSet<>();
​
    private int order = DEFAULT_FILTER_ORDER;
​
    private ResourceServerTokenServices tokenServices;
​
    private TokenExtractor tokenExtractor = new BearerTokenExtractor();
​
    @Nullable
    private PreAuthenticationFilter preAuthenticationFilter;
​
    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(this.tokenServices, "ResourceServerTokenServices bean must not be null.");
        Assert.notNull(this.tokenExtractor, "TokenExtractor bean must not be null.");
​
        buildIgnorePatterns();
​
        build();
    }
​
    public void build() {
        PreAuthenticationFilter preAuthenticationFilter = new PreAuthenticationFilter();
        preAuthenticationFilter.setOrder(this.order);
        preAuthenticationFilter.setTokenServices(this.tokenServices);
        preAuthenticationFilter.setTokenExtractor(this.tokenExtractor);
        preAuthenticationFilter.setIgnoreAntPathRequestMatchers(this.ignoreAntPathRequestMatchers);
​
        this.preAuthenticationFilter = preAuthenticationFilter;
    }
​
    private void buildIgnorePatterns() {
        for (String pattern : this.ignoreAntPatterns) {
            this.ignoreAntPathRequestMatchers.add(new AntPathRequestMatcher(pattern));
        }
​
        // 添加系统默认不拦截路径
        String prefixAntPattern = StringUtils.isBlank(this.zuulPrefix) ? "/*" : this.zuulPrefix + "/*";
​
        this.ignoreAntPathRequestMatchers.add(new AntPathRequestMatcher(prefixAntPattern + AuthorizationServerEndpoints.TOKEN_ENDPOINT));
        this.ignoreAntPathRequestMatchers.add(new AntPathRequestMatcher(prefixAntPattern + AuthorizationServerEndpoints.CHECK_TOKEN_ENDPOINT));
        this.ignoreAntPathRequestMatchers.add(new AntPathRequestMatcher(prefixAntPattern + AuthorizationServerEndpoints.TOKEN_KEY_ENDPOINT));
    }
​
    @Override
    public PreAuthenticationFilter getObject() throws Exception {
        return this.preAuthenticationFilter;
    }
​
    @Override
    public Class<?> getObjectType() {
        return PreAuthenticationFilter.class;
    }
​
    @Override
    public boolean isSingleton() {
        return true;
    }
​
    public void ignoreAntPattern(String[] patterns) {
        Assert.notEmpty(patterns, "patterns must be not null.");
        this.ignoreAntPatterns.addAll(Arrays.asList(patterns));
    }
​
    public void ignoreAntPattern(Set<String> patterns) {
        Assert.notEmpty(patterns, "patterns must be not null.");
        this.ignoreAntPatterns.addAll(patterns);
    }
​
    public void ignoreAntPattern(String pattern) {
        Assert.hasLength(pattern, "pattern must be not null.");
        this.ignoreAntPatterns.add(pattern);
    }
​
    public String getZuulPrefix() {
        return zuulPrefix;
    }
​
    public void setZuulPrefix(String zuulPrefix) {
        this.zuulPrefix = zuulPrefix;
    }
​
    public Set<String> getIgnoreAntPatterns() {
        return ignoreAntPatterns;
    }
​
    public void setIgnoreAntPatterns(Set<String> ignoreAntPatterns) {
        this.ignoreAntPatterns = ignoreAntPatterns;
    }
​
    public Set<AntPathRequestMatcher> getIgnoreAntPathRequestMatchers() {
        return ignoreAntPathRequestMatchers;
    }
​
    public void setIgnoreAntPathRequestMatchers(Set<AntPathRequestMatcher> ignoreAntPathRequestMatchers) {
        this.ignoreAntPathRequestMatchers = ignoreAntPathRequestMatchers;
    }
​
    public int getOrder() {
        return order;
    }
​
    public void setOrder(int order) {
        this.order = order;
    }
​
    public ResourceServerTokenServices getTokenServices() {
        return tokenServices;
    }
​
    public void setTokenServices(ResourceServerTokenServices tokenServices) {
        this.tokenServices = tokenServices;
    }
​
    public TokenExtractor getTokenExtractor() {
        return tokenExtractor;
    }
​
    public void setTokenExtractor(TokenExtractor tokenExtractor) {
        this.tokenExtractor = tokenExtractor;
    }
​
}


 

系统配置项管理

创建SysProperties类,管理系统相关配置项。其中,ignoreAntPatterns为ant风格的路径匹配规则,如/a/**、/b/*等,配置之后,所有满足匹配规则的请求,均不进行身份认证

package com.luas.xmall.gateway.configuration;
​
import org.springframework.boot.context.properties.ConfigurationProperties;
​
import java.util.Set;
​
@ConfigurationProperties(prefix = "sys.zuul")
public class SysProperties {
​
    private PreAuthenticationFilter preAuthenticationFilter = new PreAuthenticationFilter();
​
    public PreAuthenticationFilter getPreAuthenticationFilter() {
        return preAuthenticationFilter;
    }
​
    public void setPreAuthenticationFilter(PreAuthenticationFilter preAuthenticationFilter) {
        this.preAuthenticationFilter = preAuthenticationFilter;
    }
​
    public static class PreAuthenticationFilter {
​
        private boolean enabled = true;
​
        private int order = 0;
​
        private Set<String> ignoreAntPatterns;
​
        public boolean isEnabled() {
            return enabled;
        }
​
        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
​
        public int getOrder() {
            return order;
        }
​
        public void setOrder(int order) {
            this.order = order;
        }
​
        public Set<String> getIgnoreAntPatterns() {
            return ignoreAntPatterns;
        }
​
        public void setIgnoreAntPatterns(Set<String> ignoreAntPatterns) {
            this.ignoreAntPatterns = ignoreAntPatterns;
        }
    }
​
}

执行mvn clean compile之后,在yml配置中,即可提示这些配置项,如图。

注意,如需自动提示,还需添加spring-boot-configuration-processor依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

系统配置

创建SysConfiguration,进行系统层面的相关配置,如PreAuthenticationFilter的创建等。

package com.luas.xmall.gateway.configuration;
​
import com.luas.xmall.gateway.filter.PreAuthenticationFilter;
import com.luas.xmall.gateway.filter.PreAuthenticationFilterFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.util.CollectionUtils;
​
@Configuration
@EnableConfigurationProperties(SysProperties.class)
public class SysConfiguration {
​
    private SysProperties sysProperties;
​
    private ZuulProperties zuulProperties;
​
    public SysConfiguration(SysProperties sysProperties, ZuulProperties zuulProperties) {
        this.sysProperties = sysProperties;
        this.zuulProperties = zuulProperties;
    }
​
    @Bean
    @ConditionalOnProperty(prefix = "sys.zuul", name = "pre-authentication-filter.enabled", havingValue = "true", matchIfMissing = true)
    @ConditionalOnMissingBean(PreAuthenticationFilter.class)
    public PreAuthenticationFilterFactoryBean preAuthenticationFilter(@Qualifier("userInfoTokenServices") ResourceServerTokenServices tokenServices) {
        SysProperties.PreAuthenticationFilter preAuthFilter = this.sysProperties.getPreAuthenticationFilter();
​
        PreAuthenticationFilterFactoryBean preAuthenticationFilterFactoryBean = new PreAuthenticationFilterFactoryBean();
        preAuthenticationFilterFactoryBean.setZuulPrefix(this.zuulProperties.getPrefix());
        preAuthenticationFilterFactoryBean.setTokenServices(tokenServices);
        preAuthenticationFilterFactoryBean.setOrder(preAuthFilter.getOrder());
​
        if (!CollectionUtils.isEmpty(preAuthFilter.getIgnoreAntPatterns())) {
            preAuthenticationFilterFactoryBean.ignoreAntPattern(preAuthFilter.getIgnoreAntPatterns());
        }
​
        return preAuthenticationFilterFactoryBean;
    }
​
}

注意,如配置sys.zuul.pre-authentication-filter.enabled=false,则PreAuthenticationFilter将不会创建,即不再进行前置身份认证。

改造资源服务器配置类

修改资源服务器配置类ResourceServerConfiguration,引入端点管理接口,以根据系统配置,实现相关端点不拦截。

package com.luas.xmall.gateway.configuration;
​
import com.luas.xmall.gateway.oauth2.endpoint.AuthorizationServerEndpoints;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
​
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
​
    private String prefixAntPattern;
​
    public ResourceServerConfiguration(ZuulProperties zuulProperties) {
        this.prefixAntPattern = StringUtils.isBlank(zuulProperties.getPrefix()) ? "/*" : zuulProperties.getPrefix() + "/*";
    }
​
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers(this.prefixAntPattern + AuthorizationServerEndpoints.TOKEN_ENDPOINT).permitAll()
                .antMatchers(this.prefixAntPattern + AuthorizationServerEndpoints.TOKEN_KEY_ENDPOINT).permitAll()
                .antMatchers(this.prefixAntPattern + AuthorizationServerEndpoints.CHECK_TOKEN_ENDPOINT).permitAll()
                .anyRequest().authenticated()
        ;
    }
}

此处有两个点,比较重要。

第一个,同PreAuthenticationFilterFactoryBean类中一样,为什么会加上/*或者/zuulPrefix/*?前置身份认证过滤器为pre类型过滤器,此时request请求为全路径,包含zuul整体prefix、各路由规则的prefix,所以,需要精确匹配。

第二个点,为啥不是/**?/*和/**均为ant类型匹配规则范畴,含义截然不同。/**可任意匹配,即任意级别。而/*只能匹配一级。如/**/**/oauth/token,可匹配/gateway/auth/a/oauth/token,也可匹配/a/b/c/gateway/auth/a/b/c/oauth/token。而/*/*/oauth/token,只能匹配/gateway/auth/oauth/token。所有,严谨起见,配置为/*为最优。

验证

依次启动xmall-product、xmall-auth、xmall-zuul,端口分别为8080,、7777、5566。

通过网关调用授权服务,进行授权,地址为http://localhost:5566/gateway/auth/oauth/token?client_id=client_1&client_secret=123456&username=user_1&password=123456&scope=server&grant_type=password。

请求之后,返回授权结果。

header方式携带授权,访问http://localhost:8080/sku/1122,正常访问。

工程重构完成,可以发现,代码比之原来,干练了一些,职责更清晰,逻辑更明了。同时,也更易于后续扩展。后续,再有服务网关相关文章,皆以此重构版本为基础,进行进一步的改造。

源码


github

https://github.com/liuminglei/SpringCloudLearning/tree/master/14/

gitee

https://gitee.com/xbd521/SpringCloudLearning/tree/master/14/

本文系【银河架构师】原创,如需转载请在文章明显处注明作者及出处。

微信搜索【银河架构师】,发现更多精彩内容。

发布了29 篇原创文章 · 获赞 1 · 访问量 2224

猜你喜欢

转载自blog.csdn.net/liuminglei1987/article/details/104199450