dubbo
dubbo copied to clipboard
[Feature][3.3] CORS support for triple rest protocol
Pre-check
- [X] I am sure that all the content I provide is in English.
Search before asking
- [X] I had searched in the issues and found no similar feature requirement.
Apache Dubbo Component
Java SDK (apache/dubbo)
Descriptions
Supports CORS negotiation in triple rest and supports options configuration similar to Spring MVC.
Related issues
No response
Are you willing to submit a pull request to fix on your own?
- [ ] Yes I am willing to submit a pull request on my own!
Code of Conduct
- [X] I agree to follow this project's Code of Conduct
Can it be assigned to me ,i can complete it
Triple Rest Cors Plan
1. Cors related implementation
Under the rest/cors package of dubbo-rpc-triple module
CorsMeta configuration class currently supports the following modules and methods related to inspection and processing of some attributes.
public class CorsMeta {
.........
private List<String> allowedOrigins;
private List<OriginPattern> allowedOriginPatterns;
private List<String> allowedMethods;
private List<HttpMethods> resolvedMethods = DEFAULT_METHODS;
private List<String> allowedHeaders;
private List<String> exposedHeaders;
private Boolean allowCredentials;
private Boolean allowPrivateNetwork;
private Long maxAge;
.........
CorsProcessor, used to process requests. The following are the main methods of DefaultCorsProcesso
public class DefaultCorsProcessor implements CorsProcessor {
public boolean process(CorsMeta config, HttpRequest request, HttpResponse response) {
// set vary header
setVaryHeaders(response);
// skip if is not a cors request
if (!isCorsRequest(request)) {
return true;
}
// skip if origin already contains in Access-Control-Allow-Origin header
if (response.header(RestConstants.ACCESS_CONTROL_ALLOW_ORIGIN) != null)...
if (config == null) {
// if no cors config and is a preflight request
if (isPreFlight(request)) {
reject(response);
return false;
}
return true;
}
// handle cors request
return handleInternal(request, response, config, isPreFlight(request));
}
private boolean isPreFlight(HttpRequest request) {
// preflight request is a OPTIONS request with
// Access-Control-Request-Method header
return request.method().equals(HttpMethods.OPTIONS.name())
&& request.header(RestConstants.ACCESS_CONTROL_REQUEST_METHOD) != null;
}
private boolean isCorsRequest(HttpRequest request) {
// skip if request has no origin header
String origin = request.header(RestConstants.ORIGIN);
if (origin == null) {
return false;
}
try {
URI uri = new URI(origin);
// return true if origin is not the same as request's scheme, host and port
return !(Objects.equals(uri.getScheme(), request.scheme())
&& uri.getHost().equals(request.serverName())
//getPortByScheme
&& getPort(uri.getScheme(), uri.getPort()) == getPort(request.scheme(), request.serverPort()));
} catch (URISyntaxException e) {
// skip if origin is not a valid URI
return false;
}
}
protected boolean handleInternal(HttpRequest request, HttpResponse response, CorsMeta config, boolean isPreLight) {
//check origin,method,header are allowed
String allowOrigin = config.checkOrigin(request.header(RestConstants.ORIGIN));
........
List<HttpMethods> allowHttpMethods = config.checkHttpMethods(getHttpMethods(request, isPreLight));
.......
List<String> allowHeaders = config.checkHeaders(getHttpHeaders(request, isPreLight));
......
response.setHeader(RestConstants.ACCESS_CONTROL_ALLOW_ORIGIN, allowOrigin);
//set allow method if is prelight
if (isPreLight) {
response.setHeader(
RestConstants.ACCESS_CONTROL_ALLOW_METHODS,
......
if (isPreLight && !allowHeaders.isEmpty()) ......
if (isPreLight && config.getMaxAge() != null)....
// set related config
if (!CollectionUtils.isEmpty(config.getExposedHeaders()))...
if (Boolean.TRUE.equals(config.getAllowCredentials())) .....
if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) .....
return true;
}
}
2.Initialize Cors configuration
Global configuration
Add RestCorsConfig under org.apache.dubbo.config.RestConfig
Add the CorsMeta attribute under org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping
public class RestConfig implements Serializable {
.......
/**
* The config is used to set the Global CORS configuration properties.
*/
private CorsConfig corsConfig;
.......
}
public final class RequestMapping implements Condition<RequestMapping, HttpRequest> {
.........
private final CorsMeta corsMeta;
}
Configuration parsing will combine global, class, and method configurations in org.apache.dubbo.rpc.protocol.tri.rest.mapping.DefaultRequestMappingRegistry
public final class DefaultRequestMappingRegistry implements RequestMappingRegistry {
private CorsMeta globalCorsMeta;
.....
@Override
public void register(Invoker<?> invoker) {
new MethodWalker().walk(service.getClass(), (classes, consumer) -> {
for (RequestMappingResolver resolver : resolvers) {
.....
// combine gloabl config in class
classMapping.setCorsMeta(classMapping.getCorsMeta().combine(getGlobalCorsMeta()));
consumer.accept((methods) -> {
.....
//combine class in method
methodMapping = classMapping.combine(methodMapping);
....
});
}
});
}
private CorsMeta getGlobalCorsMeta() {
if (globalCorsMeta == null) {
Configuration globalConfiguration =
ConfigurationUtils.getGlobalConfiguration(ApplicationModel.defaultModel());
globalCorsMeta = CorsUtil.resolveGlobalMeta(globalConfiguration);
}
return globalCorsMeta;
}
spring@CrossOrigin annotation configuration
@Activate(onClass = "org.springframework.web.bind.annotation.RequestMapping")
public class SpringMvcRequestMappingResolver implements RequestMappingResolver {
public RequestMapping resolve(ServiceMeta serviceMeta) {
......
AnnotationMeta<?> crossOrigin = serviceMeta.findMergedAnnotation(Annotations.CrossOrigin);
return builder(requestMapping, responseStatus)
....
.cors(createCorsMeta(crossOrigin))
.build();
}
public RequestMapping resolve(MethodMeta methodMeta)
AnnotationMeta<?> crossOrigin = methodMeta.findMergedAnnotation(Annotations.CrossOrigin);
return builder(requestMapping, responseStatus)
......
.cors(createCorsMeta(crossOrigin))
.build();
}
private CorsMeta createCorsMeta(AnnotationMeta<?> crossOrigin) {
CorsMeta meta = new CorsMeta();
if (crossOrigin == null) {
return meta;
}
meta.setAllowCredentials(Boolean.valueOf(crossOrigin.getString("allowCredentials")));
meta.setAllowedHeaders(Arrays.asList(crossOrigin.getStringArray("allowedHeaders")));
.....
return meta;
}
}
3.Perform request interception processing
Add the CorsProcessor member variable under org.apache.dubbo.rpc.protocol.tri.rest.mapping.DefaultRequestMappingRegistry, and add the following processing code in the lookup method
public final class DefaultRequestMappingRegistry implements RequestMappingRegistry {
.......
private CorsMeta globalCorsMeta;
private final CorsProcessor corsProcessor;
public DefaultRequestMappingRegistry(FrameworkModel frameworkModel){
corsProcessor = frameworkModel.getBeanFactory().getOrRegisterBean(CorsProcessor.class);
}
public HandlerMeta lookup(HttpRequest request, HttpResponse response) {
.......
List<Candidate> candidates = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Match<Registration> match = matches.get(i);
RequestMapping mapping = match.getValue().mapping.match(request, match.getExpression());
if (mapping != null) {
Candidate candidate = new Candidate();
candidate.mapping = mapping;
candidate.meta = match.getValue().meta;
candidate.expression = match.getExpression();
candidate.variableMap = match.getVariableMap();
candidates.add(candidate);
}
}
RequestMapping mapping = winner.mapping;
//Handle request
processCors(method, mapping, request, response);
....
}
private void processCors(RequestMapping mapping, HttpRequest request, HttpResponse response) {
if (!corsProcessor.process(mapping.getCorsMeta(), request, response)) {
throw new HttpResultPayloadException(HttpResult.builder()
.......
}
}
............
Thanks for you contribution @Rawven