In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we show how to build an API Gateway to control the authentication and access to the backend resources using Spring Cloud. This is the fourth in a series of sections, and you can catch up on the basic building blocks of the application or build it from scratch by reading the first section, or you can just go straight to the source code in Github. In the last section we built a simple distributed application that used Spring Session to authenticate the backend resources. In this one we make the UI server into a reverse proxy to the backend resource server, fixing the issues with the last implementation (technical complexity introduced by custom token authentication), and giving us a lot of new options for controlling access from the browser client.
Reminder: if you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that for a single server is to open a new incognito window.
An API Gateway is a single point of entry (and control) for front end clients, which could be browser based (like the examples in this section) or mobile. The client only has to know the URL of one server, and the backend can be refactored at will with no change, which is a significant advantage. There are other advantages in terms of centralization and control: rate limiting, authentication, auditing and logging. And implementing a simple reverse proxy is really simple with Spring Cloud.
If you were following along in the code, you will know that the application implementation at the end of the last section was a bit complicated, so it’s not a great place to iterate away from. There was, however, a halfway point which we could start from more easily, where the backend resource wasn’t yet secured with Spring Security. The source code for this is a separate project in Github so we are going to start from there. It has a UI server and a resource server and they are talking to each other. The resource server doesn’t have Spring Security yet so we can get the system working first and then add that layer.
To turn it into an API Gateway, the UI server needs one small tweak. We need to add Spring Cloud Gateway to the classpath and configure a route. First, we add the dependency to our Maven POM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webmvc</artifactId>
</dependency>
...
</dependencies>Note the use of the "spring-cloud-starter-gateway-server-webmvc" - it’s a starter POM just like the Spring Boot ones, but it governs the dependencies we need for the Spring Cloud Gateway MVC proxy. We are also using <dependencyManagement> because we want to be able to depend on all the versions of transitive dependencies being correct.
Then in an external configuration file we need to map a local resource in the UI server to a remote one in the external configuration ("application.yml"):
link:ui/src/main/resources/application.yml[role=include]This says "map paths with the pattern /resource/** in this server to the same paths in the remote server at localhost:9000". The StripPrefix=1 filter removes the /resource prefix before forwarding the request, so a request to /resource/ becomes a request to / on the backend. Simple and yet effective!
With those changes in place our application still works, but we haven’t actually used the new proxy yet until we modify the client. Fortunately that’s trivial. We just need to revert the change we made going from the "single" to the "vanilla" samples in the last section:
constructor(private app: AppService, private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}Now when we fire up the servers everything is working and the requests are being proxied through the UI (API Gateway) to the resource server.
Even better: we don’t need the CORS filter any more in the resource server. We threw that one together pretty quickly anyway, and it should have been a red light that we had to do anything as technically focused by hand (especially where it concerns security). Fortunately it is now redundant, so we can just throw it away, and go back to sleeping at night!
You might remember in the intermediate state that we started from there is no security in place for the resource server.
Aside: Lack of software security might not even be a problem if your network architecture mirrors the application architecture (you can just make the resource server physically inaccessible to anyone but the UI server). As a simple demonstration of that we can make the resource server only accessible on localhost. Just add this to
application.propertiesin the resource server:
server.address: 127.0.0.1Wow, that was easy! Do that with a network address that’s only visible in your data center and you have a security solution that works for all resource servers and all user desktops.
Suppose that we decide we do need security at the software level (quite likely for a number of reasons). That’s not going to be a problem, because all we need to do is add Spring Security as a dependency (in the resource server POM):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>That’s enough to get us a secure resource server, but it won’t get us a working application yet, for the same reason that it didn’t in Part III: there is no shared authentication state between the two servers.
We can use the same mechanism to share authentication (and CSRF) state as we did in the last, i.e. Spring Session. We add the dependency to both servers as before:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>but this time the configuration is much simpler because Spring Cloud Gateway forwards all headers by default.
Then we can move on to the resource server. There are two small changes to make: one is to explicitly disable HTTP Basic in the resource server (to prevent the browser from popping up authentication dialogs), and the other is to set the session creation policy to NEVER so the resource server won’t create sessions but will use existing ones:
link:resource/src/main/java/demo/ResourceApplication.java[role=include]Aside: an alternative, which would also prevent the authentication dialog, would be to keep HTTP Basic but change the 401 challenge to something other than "Basic". You can do that with a one-line implementation of
AuthenticationEntryPointin theHttpSecurityconfiguration callback.
The UI server also needs to be configured to use session-based security context storage with HTTP Basic authentication:
link:ui/src/main/java/demo/UiApplication.java[role=include]The key configuration here is the httpBasic customizer that sets the securityContextRepository to HttpSessionSecurityContextRepository. This ensures that authentication state is properly stored in the session and shared via Redis with the resource server.
As long as redis is still running in the background (use the docker-compose.yml if you like to start it) then the system will work. Load the homepage for the UI at http://localhost:8080 and login and you will see the message from the backend rendered on the homepage.
What is going on behind the scenes now? First we can look at the HTTP requests in the UI server (and API Gateway):
| Verb | Path | Status | Response |
|---|---|---|---|
GET |
/ |
200 |
index.html |
GET |
/*.js |
200 |
Assets from angular |
GET |
/user |
401 |
Unauthorized (ignored) |
GET |
/resource |
401 |
Unauthenticated access to resource |
GET |
/user |
200 |
JSON authenticated user |
GET |
/resource |
200 |
(Proxied) JSON greeting |
That’s identical to the sequence at the end of Part II except for the fact that the cookie names are slightly different ("SESSION" instead of "JSESSIONID") because we are using Spring Session. But the architecture is different and that last request to "/resource" is special because it was proxied to the resource server.
We can see the reverse proxy in action by enabling debug logging for Spring Cloud Gateway. Add the following to your application.yml:
logging:
level:
org.springframework.cloud.gateway: DEBUG|
Note
|
Try to use a different browser so that there is no chance of authentication crossover (e.g. use Firefox if you used Chrome for testing the UI) - it won’t stop the app from working, but it will make the logs harder to read if they contain a mixture of authentication from the same browser. |
When you make a request to /resource, you can observe both the incoming request and the proxied request. Using your browser’s developer tools (Network tab), you’ll see the client’s request:
GET /resource/ HTTP/1.1
Host: localhost:8080
Accept: application/json, text/plain, */*
X-Requested-With: XMLHttpRequest
X-XSRF-TOKEN: 542c7005-309c-4f50-8a1d-d6c74afe8260
Cookie: SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260
Response:
Content-Type: application/json;charset=UTF-8
Status: 200The gateway then proxies this request to the resource server. In the debug logs, you’ll see the routing decision and the forwarded request:
DEBUG o.s.c.g.s.m.HandlerFunctions : Matched route: resource
DEBUG o.s.c.g.s.m.HandlerFunctions : Mapping [/resource/] to http://localhost:9000
Forwarded Request:
method: GET
path: /
headers:
accept: application/json, text/plain, */*
x-xsrf-token: 542c7005-309c-4f50-8a1d-d6c74afe8260
cookie: SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260
x-forwarded-prefix: /resource
x-forwarded-host: localhost:8080
Response:
Content-Type: application/json;charset=UTF-8
Status: 200Notice a few important things in the proxied request:
-
The path changed from
/resource/to/(theStripPrefix=1filter removed the prefix) -
The
x-forwarded-prefixandx-forwarded-hostheaders were added so the backend knows the original request context -
The cookies (
SESSION,XSRF-TOKEN) and the CSRF header (x-xsrf-token) were all forwarded
Without Spring Session these cookies would be meaningless to the resource server, but the way we have set it up it can now use those headers to re-constitute a session with authentication and CSRF token data. The resource server looks up the session in Redis using the SESSION cookie value, finds the authenticated principal, and permits the request. So the request is allowed and we are in business!
We covered quite a lot in this section but we got to a really nice place where there is a minimal amount of boilerplate code in our two servers, they are both nicely secure and the user experience isn’t compromised. That alone would be a reason to use the API Gateway pattern, but really we have only scratched the surface of what that might be used for. Read up on Spring Cloud to find out more on how to make it easy to add more features to the gateway. The next section in this series will extend the application architecture a bit by extracting the authentication responsibilities to a separate server (the Single Sign On pattern).