Summary
I've successfully integrated spring cloud gateway with spring security OAuth2 login using Hydra as the OAuth2 authorization server, and configured gateway using redis to store session. I can get the access token after authorized, but it'll lost after gateway restarted. The full source code can be found at Spring Cloud in Practice.
Actual Behavior
Pom.xml
<?xml version="1.0" ?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.jaggerwang</groupId>
<artifactId>spring-cloud-in-practice</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>net.jaggerwang</groupId>
<artifactId>spring-cloud-in-practice-gateway</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>spring-cloud-in-practice-gateway</name>
<description>Spring cloud in practice gateway</description>
<dependencies>
<dependency>
<groupId>net.jaggerwang</groupId>
<artifactId>spring-cloud-in-practice-common</artifactId>
<version>${scip-common.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-kickstart-spring-boot-starter-tools</artifactId>
<version>${graphql-starter.version}</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-kickstart-spring-boot-starter-webflux</artifactId>
<version>${graphql-starter.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Configure OAuth2 login for gateway
package net.jaggerwang.scip.gateway.api.config;
...
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.csrf(csrf -> csrf.disable())
.oauth2Client(oauth2Client -> {})
.oauth2Login(oauth2Login -> {})
.authorizeExchange(exchanges -> exchanges
.anyExchange()
.permitAll()
)
.build();
}
}
Configure redis session
Auto configured by spring boot, and I can see the saved session in redis.
127.0.0.1:6379> HGETALL scip:session:sessions:cc6cf08d-6f49-4903-a393-63406fcae52c
1) "lastAccessedTime"
2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01pF\xd6\x88\xf9"
3) "creationTime"
4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01pF\xd6Zp"
5) "sessionAttr:org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository.AUTHORIZATION_REQUEST"
6) ""
7) "maxInactiveInterval"
8) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\t:\x80"
9) "sessionAttr:SPRING_SECURITY_CONTEXT"
10) "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl..."
Authorize with Hydra OAuth2 server
Open http://localhost:9080/login to commence a authorization code flow if run application by docker compose, or open http://localhost:8080/login if run by local environment. After successfully authorized, it will auto redirect to index page / which will show the issued access token.
package net.jaggerwang.scip.gateway.adapter.controller;
...
@RestController
@RequestMapping("/")
public class IndexController {
private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
public IndexController(ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
this.authorizedClientRepository = authorizedClientRepository;
}
@GetMapping("/")
public Mono<RootDto> index(ServerWebExchange exchange) {
return ReactiveSecurityContextHolder
.getContext()
.map(SecurityContext::getAuthentication)
.filter(authentication -> authentication instanceof OAuth2AuthenticationToken)
.cast(OAuth2AuthenticationToken.class)
.flatMap(token -> authorizedClientRepository
.loadAuthorizedClient(token.getAuthorizedClientRegistrationId(), token, exchange)
.cast(OAuth2AuthorizedClient.class))
.map(OAuth2AuthorizedClient::getAccessToken)
.map((accessToken) -> new RootDto().addDataEntry("accessToken", accessToken))
.defaultIfEmpty(new RootDto().addDataEntry("accessToken", null));
}
}
{"code":"ok","message":"","data":{"accessToken":{"tokenValue":"...","issuedAt":"2020-02-15T09:58:44.849028Z","expiresAt":"2020-02-15T10:58:44.849028Z","tokenType":{"value":"Bearer"},"scopes":["offline","user","post","file","stat"]}}}
But after gateway restarted, the accessToken will lost.
{"code":"ok","message":"","data":{"accessToken":null}}
Expected Behavior
The access token will remain after gateway restarted.
Version
Spring Boot: v2.2.2 Spring Cloud: Hoxton.SR1
Comment From: jzheaux
Thanks for reaching out, @jaggerwang. Spring Security stores tokens in-memory by default. If you want to store tokens in the session, please use WebSessionServerOAuth2AuthorizedClientRepository:
@Bean
ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
return new WebSessionServerOAuth2AuthorizedClientRepository();
}
I'll close this since I believe this will resolve your issue, but please feel free to reopen if I've misunderstood.
Comment From: jaggerwang
Thanks a lot!