Recall that your Helidon SE application constructs rules for Helidon to use in routing HTTP methods and paths to your application endpoints. You use the same approach to add CORS support to the HTTP methods and paths in your application that need it.
To use the Helidon CORS support, your application:
- creates one or more instances of
CorsSupport
, each set up according to how you want your application endpoints to work with CORS, and - uses the
CorsSupport
instances in preparing the routing rules.
In this way, you associate specific CORS behavior with each endpoint/HTTP method combination in your application. You prescribe this CORS behavior in one or more Helidon CrossOriginConfig
objects.
Your application can create a CrossOriginConfig
object using a Config
object containing one or more of these settings:
Helidon config keys | Default | CORS header name |
---|---|---|
allow-credentials |
false |
Access-Control-Allow-Credentials |
allow-headers |
["*"] | Access-Control-Allow-Headers |
allow-methods |
["*"] | Access-Control-Allow-Methods |
allow-origins |
["*"] | Access-Control-Allow-Origins |
expose-headers |
none | Access-Control-Expose-Headers |
max-age |
3600 | Access-Control-Max-Age |
enabled |
true |
n/a |
All of these except enabled
correspond to the headers used in the CORS protocol. If the configuration sets enabled
to false
, then the Helidon CORS implementation ignores that cross-origin configuration entry.
Both the Helidon routing mechanism and the Helidon CORS support are very flexible. You can combine them in many ways to add CORS support to your endpoints. Here we describe a few of the likely use cases and how to implement them. These approaches use a combination of code in your application and configuration.
Some applications might not be susceptible to the dangers of unmanaged cross-origin resource sharing. For example, perhaps your application supports only GET
HTTP requests and the responses never contain sensitive information. You might want your endpoints to honor the CORS protocol but without restricting resource sharing.
Use a single CrossOriginConfig
instance with default settings and apply that to all incoming traffic. Using the GreetService
from the Helidon SE quickstart example, the following code creates a Routing
instance you can use to start your Helidon SE server with all endpoints providing unrestricted CORS support:
Routing routing = Routing.builder()
.register(JsonSupport.create())
.register(CorsSupport.create())
.register("/greet", new GreetService())
.build();
In this approach:
- Choose what different categories of CORS support you want to support in your application. For example, you might want to allow unrestricted sharing in certain cases and stricter access in others.
- Prepare a configuration source that contains the CORS set-up for each of the categories.
- Add code to your application to:
- read the configuration, creating a
CrossOriginConfig
object for each category, then - apply the correct
CrossOriginConfig
instance to each HTTP method/path combination.
- read the configuration, creating a
Helidon configuration lets you use multiple configuration sources. These examples use the default application.yaml
config file to hold the CORS configuration for the different categories of access but you are not limited to that; any config source your application uses will work.
The purpose of CORS is to let you carefully relax the restrictions imposed by the same-origin policy https://en.wikipedia.org/wiki/Same-origin_policy. For example, suppose you want to allow unrestricted access for GET
, HEAD
, and POST
requests (what CORS refers to as "simple" requests), but permit other types of requests only from the two origins foo.com
and bar.com
. This means you have two categories of CORS configuration: relaxed for the simple requests and stricter for others. In practice, you can use as many categories as makes sense for your application.
Create or update your application's default configuration file:
application.yaml
restrictive-cors:
allow-origins: ["foo.com", "bar.com"]
allow-methods: ["PUT", "DELETE"]
open-cors: # defaults to "*" for all the "allows" values
max-age: -1
You can choose any key values (restrictive-cors
, open-cors
in this example) as long as your application code uses the same keys to retrieve each set of configuration.
import io.helidon.config.Config;
...
Config appConfig = Config.create(); // loads config from the default sources, including application.yaml
CrossOriginConfig restrictiveConfig = CrossOriginConfig.create(appConfig.get("restrictive-cors"));
CrossOriginConfig openConfig = CrossOriginConfig.create(appConfig.get("open-cors"));
The following code sets up restrictive CORS access for PUT
and DELETE
requests and
CorsSupport restrictiveCors = CorsSupport.create(restrictiveConfig);
Routing routing = Routing.builder()
.register(JsonSupport.create())
--> wrong; need to fix .any(CorsSupport.builder().addCrossOriginConfig(openConfig).build()
.put(restrictiveCors)
.delete(restrictiveCors)
.register("/greet", new GreetService())
.build();
A few notes:
- We created the open
CorsSupport
instance using theconfig
convenience method; we did not need a local variable to hold it because we use it only once. - We reuse the
restrictiveCors
CrossOriginConfig
instance in setting up the routing forPUT
andDELETE
requests. - We have not changed any of the
GreetService
code itself that implements the business logic of the application. - We invoke
any
,put
, anddelete
with the CORS set-up before invokingregister
for theGreetService
. This makes sure that the Helidon CORS logic runs first -- particularly forPUT
andDELETE
requests -- so unwanted requests are prevented from reaching the application's endpoints. - This code applies CORS based only on the HTTP method, not on the endpoints' paths.
- Note that the greeting application does not implement a
DELETE
endpoint. We set up restrictive CORS enforcement forDELETE
requests anyway in this example because: - In general, if you are protecting puts you would probably also want to protect deletes and we wanted to illustrate that.
- Future enhancements to your greeting service might add and endpoint for
DELETE
and this way the routing code is already in place to apply resctrictive CORS access toDELETE
requests.
Because the application reads the CORS set-up for the categories from configuration, you or others who deploy your application can modify that CORS set-up by revising or overriding the configuration, without needing any changes to the application code. By default, Helidon configuration allows multiple sources for the default application config (such as system properties in addition to the application.yaml
file). Different deployments of your application can use different CORS rules without editing applicaiton.yaml
or repackaging the application.
By writing your application to load CORS information from configuration, you give yourself and deployers of your application all the flexibility of the Helidon config system to assign and override the CORS settings.
Suppose your application includes some administrative features that should permit only very restricted resource sharing. Add a new section to your application config file and modify your application to read the CORS set-up for the admin functions from the config and set up routing using the CORS constraints.
application.yaml
...
admin-cors:
allow-origins: ["mycompany.com"]
...
Assume you have added a new Service
implementation to your application called GreetAdminService
. To the earlier example code add this:
CorsSupport adminCors = CorsSupport.mappedCreate(appConfig.get("admin-cors"));
Routing routing = Routing.builder()
.register(JsonSupport.create())
.any(CorsSupport.builder().addCrossOriginConfig(openConfig).build())
.put(restrictiveCors)
.delete(restrictiveCors)
.register("/greet", new GreetService())
.register("/greet/admin", adminCors, new GreetAdminService()) // <-- this is new
.build();
All admin operations will have the /greet/admin
prefix and enforce the resource sharing constraints your application loaded from the configuration.
You can allow deployers of your application to override completely the CORS configuration in your application, based on path matching and HTTP method.
MappedCrossOriginConfig mapped = MappedCrossOriginConfig.create(appConfig.get("cors"));
Routing routing = Routing.builder()
.register(JsonSupport.create())
.any(CorsSupport.builder().addCrossOriginConfig(openConfig).build())
.put(restrictiveCors)
.delete(restrictiveCors)
.register("/greet", new GreetService())
.register(mapped) // <--- this is new
.build();
The mapped cross-origin config can take path mapping information as well as the CORS set-up itself. The configuration can have a new section containing overrides:
application.yaml
...
cors:
- path-prefix: /greet
allow-origins: ["foo.com"]
allow-methods: ["PUT", "DELETE"]
- path-prefix: /other
allow-origins: ["bar.com"]
By adding just two lines to your application you allow deployers of your application to override any CORS setting for any endpoint. The overriding configuration specifies endpoint path expressions which Helidon CORS uses to match actual endpoint paths in the application.
We do not recommend using this approach as the primary way to assign CORS behavior to endpoints. If you decide to change the path or subpaths for the endpoints you need to change the Java code and the configuration. Instead, rely on mapped CORS configuration for overriding only and primarily use the CORS categories approach above to prepare the normal CORS set-up for your application.
Original table:
Helidon config keys | CrossOriginConfig.Builder method |
Default | CORS header name |
---|---|---|---|
allow-credentials |
allowCredentials |
false |
Access-Control-Allow-Credentials |
allow-headers |
allowHeaders |
"*" | Access-Control-Allow-Headers |
allow-methods |
allowMethods |
"*" | Access-Control-Allow-Methods |
allow-origins |
allowOrigins |
"*" | Access-Control-Allow-Origins |
expose-headers |
exposeHeaders |
none | Access-Control-Expose-Headers |
max-age |
maxAgeSeconds |
3600 | Access-Control-Max-Age |
enabled |
enabled |
true |
n/a |
CrossOriginConfig crossOriginConfig = CrossOriginConfig.builder()
.allowCredentials(true)
.allowHeaders(hdr1, hdr2, ...)
.allowMethods(mth1, mth2, ...)
.allowOrigins(or1, or2, ...)
.exposeHeaders(hdrA, hdrB, ...)
.maxAgeSeconds(age)
.enabled(true)
.build();
You can omit any or all of the attribute-setting method invocations, in which case defaults or previous settings on the same CrossOriginConfig.Builder
object prevail.
Use the CrossOriginConfig.Builder
config
method to partly or completely set up CORS information from a Helidon Config
object. You can mix calls to the config
method and the attribute-setting methods; the last one invoked overrides any prior settings.
To load just from configuration, use the CrossOriginConfig.create(Config)
method (which simply functions as CrossOriginConfig.builder().config(myConfig).build()
).
You can create a CrossOriginConfig
instance using configuration and the attribute-level methods together. Think of the CrossOriginConfig.Builder.config
method as a sequence of attribute-level method invocations using the contents of the config object. You can invoke the builder's config
method multiple times, and you can invoke config
and the attribute-level methods in any order. Remember that the last assignment to a setting wins.
Your application needs to link each CrossOriginConfig
object to the endpoint/HTTP method combinations it should affect. You create routing rules for CORS very similarly to how you do with your application endpoints.
Recall that the Helidon SE routing rules approach lets you define rules in six ways. For each request, Helidon SE routing uses the first entry in this table that matches. The "Case" column refers to the CorsSupport
cases below.
HTTP method-specific | Path-specific | Routing.Builder Java method |
Case | Notes |
---|---|---|---|---|
X | X | get(pathExpr, handler...) |
1 | Same for put , post , etc. Routing.Builder methods. Associates the rule with one HTTP method and one path expression. |
_ | X | any(pathExpr, handler...) |
1 | Applies to any HTTP method. |
X | _ | get(handler...) |
2 | Applies to any HTTP GET request. |
_ | X | register(pathExpr, service...) |
3 | Applies to any request matching the path expression. |
_ | _ | any(handler...) |
2 | Applies to all requests. |
_ | _ | register(service...) |
4 | Applies to all requests. |
Your application passes a CorsSupport
object as the handler or service to the methods above. You associate one or more CrossOriginConfig
objects with each CorsSupport
object you create. Exactly how you construct the CorsSupport
instance depends on the case, detailed next. Note that you can (but in some cases will not) give a path-matching expression when you link the CrossOriginConfig
object with the CorsSupport
object.
The CorsSupport
object wraps a single CrossOriginConfig' object. You associate it with a path expression as a parameter to the
Routing.Buildermethod along with the
Handler` that implements that operation.
CorsSupport corsSupport = CorsSupport.builder().addCrossOrigin(restrictive).build();
Routing.Builder builder = Routing.builder()
.get("/greet", corsSupport); // or put, post, options, any, etc.
The CorsSupport
object holds one ore more CrossOriginConfig
instances, each potentially mapped to different path expressions.
CorsSupport corsSupport = CorsSupport.builder().addCrossOrigin("/greet", restrictive).build();
Routing.Builder builder = Routing.builder()
.get(corsSupport); // or put, post, options, any, etc.
The CorsSupport
object wraps one CrossOriginConfig
object. You associate it with a path expression as a parameter to the Router.Builder.register
method.
CorsSupport corsSupport = CorsSupport.builder().addCrossOrigin(restrictive).build();
Routing.Builder builder = Routing.builder()
.register("/greet", corsSupport);
The CorsSupport
object holds one ore more CrossOriginConfig
instances, each potentially mapped to different path expressions.
CorsSupport corsSupport = CorsSupport.builder().addCrossOrigin("/greet", restrictive).build();
Routing.Builder builder = Routing.builder()
.register(corsSupport);
It depends. If you can, avoid case
Think of each CorsSupport
object as a collection of one or more CrossOriginConfig
objects.