前言
前面讲的服务网关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/
本文系【银河架构师】原创,如需转载请在文章明显处注明作者及出处。
微信搜索【银河架构师】,发现更多精彩内容。