当我通过以下方式直接命中时:
http://localhost:8081/dictionary/api/dictionary/items
与Headers
类似:
Authorization
,其值例如:
Bearer {token}
并带有自定义标头:
X-Company
的值为X
我得到输出:"Test"
没关系-此 dictionary
模块中的代码为:
控制器:
@RestController
@RequestMapping("/api/dictionary/")
@PreAuthorize("isAuthenticated()")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class DictionaryController {
@RequestMapping(value = "items", method = GET)
public String getAllDictionaryItems() {
return "Test";
}
}
拦截器:
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
class CompanyFeignInterceptor implements RequestInterceptor {
private static final String COMPANY_HEADER = "X-Company";
private final MultitenancyHelper multitenancyHelper;
@Override
public void apply(RequestTemplate template) {
String selectedCompany = null;
if (!template.url().contains("/login")) {
selectedCompany = multitenancyHelper.getSelectedCompany();
}
if (selectedCompany != null) {
template.header(COMPANY_HEADER, selectedCompany);
}
}
}
另一个拦截器:
@Slf4j
class CompanyHandlerInterceptor extends HandlerInterceptorAdapter {
private static final String COMPANY_HEADER = "X-Company";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
String headerContent = decodeInsuranceCompany(request.getHeader(COMPANY_HEADER));
Optional<Authentication> authenticationOpt = Optional
.ofNullable(SecurityContextHolder.getContext().getAuthentication());
checkState(authenticationOpt.isPresent(),
"No authentication information available in securityContext!");
//noinspection OptionalGetWithoutIsPresent
Object principal = authenticationOpt.get().getPrincipal();
if (!(principal instanceof UserDetailsImpl)) {
return true;
}
UserDetailsImpl userDetails = (UserDetailsImpl) principal;
boolean hasAccess = false;
if (headerContent != null) {
Set<String> selectedCompanies = Sets.newHashSet(headerContent.split(";"));
Set<String> intersect = userDetails.getCompanies().stream()
.filter(selectedCompanies::contains).collect(Collectors.toSet());
//set only these companies for which user has access
userDetails.setSelectedCompanies(intersect);
//if user hasn't got access to any company, throw exception
hasAccess = !intersect.isEmpty();
}
if (!hasAccess) {
log.error("User [{}] is not assigned to company [{}]", userDetails.getUsername(),
headerContent);
throw new NoPermissionToCompanyException(userDetails.getUsername(), headerContent);
}
return true;
}
private String decodeInsuranceCompany(String insuranceCompany) {
if (insuranceCompany != null) {
return CodingUtil.decodeStringInUTF8(insuranceCompany);
}
return null;
}
}
配置:
@Configuration
@EnableWebMvc
@EnableSwagger2
@Import(WebSecurityConfiguration.class)
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Bean
public LoggingFilter loggingFilter() {
return new LoggingFilter();
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jackson2Converter());
converters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
converters.add(new ByteArrayHttpMessageConverter());
}
@Bean
public HttpMessageConverters messageConverters() {
return new HttpMessageConverters(new ArrayList<>()) {
@Override
protected List<HttpMessageConverter<?>> postProcessConverters(
List<HttpMessageConverter<?>> converters) {
if (converters == null) {
converters = new ArrayList<>();
}
converters.add(0, jackson2Converter());
return converters;
}
};
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new InsuranceCompanyHandlerInterceptor());
}
private MappingJackson2HttpMessageConverter jackson2Converter() {
return new MappingJackson2HttpMessageConverter(new Jackson2ObjectMapperBuilder()
.timeZone(TimeZone.getTimeZone("GMT+1:00"))
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modules(new JavaTimeModule(), new Jdk8Module())
.failOnUnknownProperties(false)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build()
);
}
@Bean
@ConditionalOnProperty(prefix = "timeMachine.time.changing", name = "enabled")
public TimeMachineController timeMachineController(TimeMachineSender sender) {
return new TimeMachineController(sender);
}
@Bean
@ConditionalOnMissingBean(RequestContextFilter.class)
public RequestContextFilter requestContextFilter() {
return new RequestContextFilter();
}
@Bean
public FilterRegistrationBean requestContextFilterChainRegistration(
@Qualifier("requestContextFilter") Filter securityFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(securityFilter);
registration.setName("requestContextFilter");
return registration;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("GET", "HEAD", "PUT", "POST", "DELETE");
}
}
我还有另一个模块-Api
我在apiGateway
作为contextPath的tomcat上运行了它。
输出为401-我想找出原因。令牌与上面的令牌相同,因此是有效令牌。
当我将SpringBoot从1.5.x更改为2.0.3时出现了问题。
zuul-routes-ext.properties
:
zuul.routes.dictionary.path=/dictionary/**
zuul.routes.dictionary.url=http://localhost:8081/dictionary/
zuul.routes.dictionary.sensitiveHeaders=Cookie,Set-Cookie
Ouath
配置:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XOAuth2AuthenticationEntryPoint extends AbstractOAuth2SecurityExceptionHandler implements AuthenticationEntryPoint {
private String typeName = "Bearer";
private String realmName = "oauth";
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
doHandle(request, response, authException);
}
@Override
protected ResponseEntity<OAuth2Exception> enhanceResponse(ResponseEntity<OAuth2Exception> response, Exception exception) {
HttpHeaders headers = response.getHeaders();
String existing = null;
if (headers.containsKey("WWW-Authenticate")) {
existing = extractTypePrefix(headers.getFirst("WWW-Authenticate"));
}
StringBuilder builder = new StringBuilder();
builder.append(this.typeName + " ");
builder.append("realm=\"" + this.realmName + "\"");
if (existing != null) {
builder.append(", " + existing);
}
HttpHeaders update = new HttpHeaders();
update.putAll(response.getHeaders());
update.set("WWW-Authenticate", builder.toString());
update.set("Access-Control-Allow-Origin", "*");
return new ResponseEntity(response.getBody(), update, response.getStatusCode());
}
private String extractTypePrefix(String header) {
String existing = header;
String[] tokens = header.split(" +");
if (tokens.length > 1 && !tokens[0].endsWith(",")) {
existing = StringUtils.arrayToDelimitedString(tokens, " ").substring(header.indexOf(" ") + 1);
}
return existing;
}
}
WebConfig
:
@Configuration
@EnableZuulProxy
@PropertySource(value = {"classpath:x-gateway.properties",
"classpath:x-nodes.properties",
"classpath:zuul-routes.properties",
"classpath:x-gateway-ext.properties",
"classpath:zuul-routes-ext.properties"},
ignoreResourceNotFound = true)
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("GET", "HEAD", "PUT", "POST", "DELETE");
}
}
ResourceServerConfiguration
:
@Configuration
@EnableResourceServer
@Order(Integer.MAX_VALUE)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new XOAuth2AuthenticationEntryPoint());
http.httpBasic().authenticationEntryPoint(new XOAuth2AuthenticationEntryPoint());
http.authorizeRequests()
.antMatchers("/index.html", "/home.html").permitAll()
.anyRequest().authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(new XOAuth2AuthenticationEntryPoint());
}
}
RequestContextFilterConfiguration
:
@Configuration
public class RequestContextFilterConfiguration {
@Bean
@ConditionalOnMissingBean(RequestContextFilter.class)
public RequestContextFilter requestContextFilter() {
return new RequestContextFilter();
}
@Bean
public FilterRegistrationBean requestContextFilterChainRegistration(
@Qualifier("requestContextFilter") Filter securityFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(securityFilter);
registration.setName("requestContextFilter");
return registration;
}
}
CorsRequestIdFilter
:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class CorsRequestIdFilter extends OncePerRequestFilter {
private static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (isCorsRequest(request) && isPreFlightRequest(request)) {
Set<String> headers = new HashSet<>();
headers.add("Authorization");
headers.add("X-Company");
String requestHeader = request.getHeader("Access-Control-Request-Headers");
if (requestHeader != null) {
headers.addAll(Arrays.asList(requestHeader.split(",")));
}
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
response.addHeader("Access-Control-Max-Age", "3600");
response.addHeader("Access-Control-Allow-Headers",
headers.stream().collect(Collectors.joining(",")));
return;
}
filterChain.doFilter(request, response);
}
private boolean isCorsRequest(HttpServletRequest request) {
return (request.getHeader(HttpHeaders.ORIGIN) != null);
}
private boolean isPreFlightRequest(HttpServletRequest request) {
return (isCorsRequest(request) && request.getMethod().equals(HttpMethod.OPTIONS.name()) &&
request.getHeader(ACCESS_CONTROL_REQUEST_METHOD) != null);
}
}
此模块pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>api</artifactId>
<version>4.6.0-SNAPSHOT</version>
<packaging>war</packaging>
<parent>
<groupId>Y</groupId>
<artifactId>Z-parent</artifactId>
<version>4.6.0-SNAPSHOT</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons-collections4.version}</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>B</groupId>
<artifactId>A-common-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- SECURITY -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${spring-security-oauth2.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring-security-jwt.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>A</groupId>
<artifactId>B</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>${maven-war-plugin-version}</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<archive>
<manifestEntries>
<Sys-Version>${project.version}</Sys-Version>
<!-- Variables set by Hudson -->
<Build-Number>${BUILD_NUMBER}</Build-Number>
<Build-Date>${BUILD_TIMESTAMP}</Build-Date>
<!-- Next two for potential usage in the next AppInfo versions -->
<Job-Name>${JOB_NAME}</Job-Name>
<Git-Branch>${GIT_BRANCH}</Git-Branch>
<Git-Commit>${GIT_COMMIT}</Git-Commit>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.0.3.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<!-- Enable this profile to run in IntelliJ. IntelliJ excludes provided dependencies from compile by default. -->
<id>intellij</id>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</profile>
</profiles>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>http://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>http://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>http://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>