Overview
Spring Cloud Gateway is an API Gateway built on top of the Spring ecosystem. It is built on top of Spring Boot, Spring WebFlux, and Project Reactor.
In this section, you will use Spring Cloud Gateway to route requests to a Servlet API service.
What you will learn in this article:
- OpenID Connect authentication - used for user authentication.
- Token relay - Spring Cloud Gateway API gateway acts as a client and forwards tokens to resource requests.
Prerequisites:
- Java 8+
- MySQL
- Redis
OpenID Connect authentication
OpenID Connect defines a user authentication mechanism based on the OAuth2 authorization code flow. The following diagram shows the complete process of authentication between Spring Cloud Gateway and the authorization service. For clarity, some parameters have been omitted.
Build authorization service
In this section, we will use Spring Authorization Server to build an authorization service that supports the OAuth2 and OpenID Connect protocols. At the same time, we will use the RBAC0 basic permission model to control access rights. This authorization service also supports Github third-party login as an OAuth2 client.
Relevant database table structure
We have created a basic RBAC0 permission model for this article and provided the table structures required for persistence storage of OAuth2 authorization services and OAuth2 clients. The oauth2_client_role table defines the external system role and the mapping relationship with the local platform role. The SQL statements related to the creation of related tables and initialization data can be obtained here.
Role description
By default, the authorization service in this section provides two roles, with the following role attributes and access permissions:
read | write | |
---|---|---|
ROLE_ADMIN | ✅ | ✅ |
ROLE_OPERATION | ✅ | ❎ |
Maven dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
Configuration
First, let's start with the application.yml
configuration, where we specify the port number and MySQL connection configuration:
server:
port: 8080
spring:
datasource:
druid:
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth2server?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: <<username>> # Modify username
password: <<password>> # Modify password
Next, we will create AuthorizationServerConfig
to configure the required beans for OAuth2 and OIDC. First, we will add OAuth2 client information and persist it to the database:
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId("relive-messaging-oidc")
.clientId("relive-client")
.clientSecret("{noop}relive-client")
.clientAuthenticationMethods(s -> {
s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
})
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-gateway-oidc")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope(OidcScopes.EMAIL)
.scope("read")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false)
.requireProofKey(false)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
.accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
.refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
.reuseRefreshTokens(true)
.build())
.build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
Second, we will create the persistence container class required during the authorization process:
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
The authorization server needs a signing key for its tokens, so let's generate a 2048-byte RSA key:
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
static class Jwks {
private Jwks() {
}
public static RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
}
static class KeyGeneratorUtils {
private KeyGeneratorUtils() {
}
static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
Next, we will create the SecurityFilterChain
used for OAuth2 authorization. SecurityFilterChain
is a filter chain provided by Spring Security.
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
//配置OIDC
authorizationServerConfigurer.oidc(Customizer.withDefaults());
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
return http.requestMatcher(endpointsMatcher)
.authorizeRequests((authorizeRequests) -> {
((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl) authorizeRequests.anyRequest()).authenticated();
}).csrf((csrf) -> {
csrf.ignoringRequestMatchers(new RequestMatcher[]{endpointsMatcher});
}).apply(authorizationServerConfigurer)
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.exceptionHandling(exceptions -> exceptions.
authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
.apply(authorizationServerConfigurer)
.and()
.build();
}
Above, we configured the default configuration for OAuth2 and OpenID Connect, and redirected authentication requests to the login page. At the same time, we also enable the OAuth2 resource service configuration provided by Spring Security. This configuration is used to protect the OpenID Connect /userinfo user information endpoint.
When enabling the Spring Security OAuth2 resource service configuration, we specify JWT verification, so we need to specify the jwk-set-uri
in application.yml
or add JwtDecoder
declaratively. Here, we use declarative configuration:
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
Next, we will customize the access token. In this example, we use the RBAC0 permission model, so we add the authorities
for the permission code of the current user's role to the access_token
:
@Configuration(proxyBeanMethods = false)
public class AccessTokenCustomizerConfig {
@Autowired
RoleRepository roleRepository;
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
return (context) -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getClaims().claims(claim -> {
claim.put("authorities", roleRepository.findByRoleCode(context.getPrincipal().getAuthorities().stream()
.map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION"))
.getPermissions().stream().map(Permission::getPermissionCode).collect(Collectors.toSet()));
});
}
};
}
}
RoleRepository is the persistence layer object of the role table. In this example, we use the JPA framework and the relevant code will not be shown in this article. If you are not familiar with JPA, you can use Mybatis instead.
Below we will configure the authorization service Form authentication method.
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(withDefaults())
...
return http.build();
}
Next, we will create a JdbcUserDetailsService
that implements UserDetailsService
, which is used to look up the password and permission information of the logged-in user during the authentication process. If you are interested in why UserDetailsService
needs to be implemented, you can check the source code of UsernamePasswordAuthenticationFilter
-> ProviderManager
-> DaoAuthenticationProvider
. In DaoAuthenticationProvider
, the user information is obtained by calling UserDetailsService#loadUserByUsername(String username).
@RequiredArgsConstructor
public class JdbcUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.relive.entity.User user = userRepository.findUserByUsername(username);
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("user is not found");
}
if (CollectionUtils.isEmpty(user.getRoleList())) {
throw new UsernameNotFoundException("role is not found");
}
Set<SimpleGrantedAuthority> authorities = user.getRoleList().stream().map(Role::getRoleCode)
.map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
And we will inject it into Spring:
@Bean
UserDetailsService userDetailsService(UserRepository userRepository) {
return new JdbcUserDetailsService(userRepository);
}
When attempting to access an unauthenticated interface, the user will be directed to the login page and prompted to enter their username and password, as shown below:
Users usually need to use multiple platforms provided and hosted by different organizations. These users may need to use specific (and different) credentials for each platform. When users have many different credentials, they often forget their login credentials.
Federated authentication is used to authenticate users using external systems. This can be used with Google, Github or any other identity provider. Here, I will use Github for user authentication and data synchronization management.
Github Identity Authentication
First, we will configure the Github client information, and you only need to change the clientId and clientSecret. Secondly, we will use the JdbcClientRegistrationRepository
persistence layer container class introduced in Spring Security Persistence OAuth2 Client to store the GitHub client information in the database.
@Bean
ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
JdbcClientRegistrationRepository jdbcClientRegistrationRepository = new JdbcClientRegistrationRepository(jdbcTemplate);
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("github")
.clientId("123456")
.clientSecret("123456")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
.scope(new String[]{"read:user"})
.authorizationUri("https://github.com/login/oauth/authorize")
.tokenUri("https://github.com/login/oauth/access_token")
.userInfoUri("https://api.github.com/user")
.userNameAttributeName("login")
.clientName("GitHub").build();
jdbcClientRegistrationRepository.save(clientRegistration);
return jdbcClientRegistrationRepository;
}
Next, we will instantiate OAuth2AuthorizedClientService
and OAuth2AuthorizedClientRepository
:
- OAuth2AuthorizedClientService: responsible for persisting OAuth2AuthorizedClient between web requests.
- OAuth2AuthorizedClientRepository: used to save and persist authorization clients between requests.
@Bean
OAuth2AuthorizedClientService authorizedClientService(
JdbcTemplate jdbcTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
}
@Bean
OAuth2AuthorizedClientRepository authorizedClientRepository(
OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
For each user logging in with Github, we need to assign platform roles to control which resources they can access. Here, we will create the AuthorityMappingOAuth2UserService class to grant user roles:
@RequiredArgsConstructor
public class AuthorityMappingOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
private final OAuth2ClientRoleRepository oAuth2ClientRoleRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) delegate.loadUser(userRequest);
Map<String, Object> additionalParameters = userRequest.getAdditionalParameters();
Set<String> role = new HashSet<>();
if (additionalParameters.containsKey("authority")) {
role.addAll((Collection<? extends String>) additionalParameters.get("authority"));
}
if (additionalParameters.containsKey("role")) {
role.addAll((Collection<? extends String>) additionalParameters.get("role"));
}
Set<SimpleGrantedAuthority> mappedAuthorities = role.stream()
.map(r -> oAuth2ClientRoleRepository.findByClientRegistrationIdAndRoleCode(userRequest.getClientRegistration().getRegistrationId(), r))
.map(OAuth2ClientRole::getRole).map(Role::getRoleCode).map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
//When no client role is specified, the least privilege ROLE_OPERATION is given by default
if (CollectionUtils.isEmpty(mappedAuthorities)) {
mappedAuthorities = new HashSet<>(
Collections.singletonList(new SimpleGrantedAuthority("ROLE_OPERATION")));
}
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
return new DefaultOAuth2User(mappedAuthorities, oAuth2User.getAttributes(), userNameAttributeName);
}
}
We can see that the permission information is obtained from the authority and role attributes, and the role attributes mapped to this platform are searched through OAuth2ClientRoleRepository.
Note:
authority
androle
are custom attributes of the platform, unrelated to the OAuth2 protocol and Open ID Connect protocol. In a production environment, you can negotiate with an external system to agree on an attribute to transmit permission information.
OAuth2ClientRoleRepository
is the persistence layer container class for theoauth2_client_role
table, implemented by JPA.
For the mapped role information that has not been obtained in advance, we will assign the default ROLE_OPERATION
minimum permission role. In this example, users who log in with Github will also be assigned the ROLE_OPERATION
role.
For users who successfully authenticate with Github and log in for the first time, we will obtain their user information and persist it to the user
table. Here, we will implement AuthenticationSuccessHandler
and add user persistence logic.
public final class SavedUserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler();
private Consumer<OAuth2User> oauth2UserHandler = (user) -> {
};
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication instanceof OAuth2AuthenticationToken) {
if (authentication.getPrincipal() instanceof OAuth2User) {
this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal());
}
}
this.delegate.onAuthenticationSuccess(request, response, authentication);
}
public void setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) {
this.oauth2UserHandler = oauth2UserHandler;
}
}
We will inject UserRepositoryOAuth2UserHandler
into SavedUserAuthenticationSuccessHandler
through the setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler)
method. UserRepositoryOAuth2UserHandler
defines specific persistence layer operations:
@Component
@RequiredArgsConstructor
public final class UserRepositoryOAuth2UserHandler implements Consumer<OAuth2User> {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Override
public void accept(OAuth2User oAuth2User) {
DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) oAuth2User;
if (this.userRepository.findUserByUsername(oAuth2User.getName()) == null) {
User user = new User();
user.setUsername(defaultOAuth2User.getName());
Role role = roleRepository.findByRoleCode(defaultOAuth2User.getAuthorities()
.stream().map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION"));
user.setRoleList(Arrays.asList(role));
userRepository.save(user);
}
}
}
We obtain the role information mapped by defaultOAuth2User.getAuthorities()
and store it in the database with the user information.
UserRepository and RoleRepository are persistence container classes.
Finally, we add OAuth2 Login configuration to SecurityFilterChain:
@Autowired
UserRepositoryOAuth2UserHandler userHandler;
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.oauth2Login(oauth2login -> {
SavedUserAuthenticationSuccessHandler successHandler = new SavedUserAuthenticationSuccessHandler();
successHandler.setOauth2UserHandler(userHandler);
oauth2login.successHandler(successHandler);
});
...
return http.build();
}
Create a Spring Cloud Gateway application
In this section, we will use Spring Security OAuth2 Login in Spring Cloud Gateway to enable OpenID Connect authentication and relay Access Token to downstream services.
Maven dependencies
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.76.Final</version>
</dependency>
Configuration
First, we add the following properties to application.yml
:
server:
port: 8070
servlet:
session:
cookie:
name: GATEWAY-CLIENT
Here, the cookie name is specified as GATEWAY-CLIENT
to avoid conflicts with the authorization service JSESSIONID
.
Route to the resource server through Spring Cloud Gateway:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: resource-server
uri: http://127.0.0.1:8090
predicates:
Path=/resource/**
filters:
- TokenRelay
The TokenRelay filter extracts the access token stored in the user session and adds it as an Authorization header to the outgoing request. This allows downstream services to authenticate requests.
We will add OAuth2 client information in application.yml
:
spring:
security:
oauth2:
client:
registration:
messaging-gateway-oidc:
provider: gateway-client-provider
client-id: relive-client
client-secret: relive-client
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- openid
- profile
client-name: messaging-gateway-oidc
provider:
gateway-client-provider:
authorization-uri: http://127.0.0.1:8080/oauth2/authorize
token-uri: http://127.0.0.1:8080/oauth2/token
jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
user-info-uri: http://127.0.0.1:8080/userinfo
user-name-attribute: sub
OpenID Connect uses a special scope value openid
to control access to the UserInfo endpoint, and other information is consistent with the client registration information parameters of the authorization service in the previous section.
Spring Security intercepts unauthenticated requests and performs authentication on the authorization server. For simplicity, CSRF is disabled.
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(authorize -> authorize
.anyExchange().authenticated()
)
.oauth2Login(withDefaults())
.cors().disable();
return http.build();
}
}
After completing OpenID Connect authentication in Spring Cloud Gateway, the user information and token are stored in the session, so we add spring-session-data-redis
to provide Redis-supported distributed session functionality and add the following configuration in application.yml
:
spring:
session:
store-type: redis
redis:
flush-mode: on_save
namespace: gateway:session
redis:
host: localhost
port: 6379
password: 123456
Based on the above example, we use Spring Cloud Gateway to drive authentication and know how to authenticate users, obtain tokens (after user consent), but do not authenticate/authorize requests through Gateway (Spring Gateway Cloud is not the audience target of Access Token). The reason behind this approach is that some services are protected while others are public. Even in a single service, sometimes only a few endpoints need to be protected, not every endpoint. That's why I leave the request authentication/authorization to specific services.
Of course, from the implementation perspective, it does not prevent us from performing authentication/authorization in Spring Cloud Gateway, it is just a matter of choice.
Resource server
In this section, we use Spring Boot to set up a simple resource server. The example provides two API interfaces for the resource server and protects them through Spring Security OAuth2 resource server configuration.
Maven dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.7</version>
</dependency>
Configuration
Add the jwk-set-uri
property in application.yml
:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://127.0.0.1:8080
jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
server:
port: 8090
Create the ResourceServerConfig
class to configure the Spring Security security module, and use the @EnableMethodSecurity
annotation to enable annotation-based security.
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer()
.jwt();
return http.build();
}
}
Spring Security resource server uses the scope
and scp
attributes in the claim by default to verify the token and extract permission information.
Spring Security
JwtAuthenticationProvider
extracts permission information and other information through theJwtAuthenticationConverter
auxiliary converter.
However, in this example, the internal permissions use the authorities attribute, so we use JwtAuthenticationConverter
to manually extract permissions.
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
Here, we specify the permission attribute as authorities
and completely remove the permission prefix.
Finally, we will create API interfaces for testing in the example and use @PreAuthorize
to protect the interface, which can only be accessed by corresponding permissions:
@RestController
public class ArticleController {
List<String> article = new ArrayList<String>() {{
add("article1");
add("article2");
}};
@PreAuthorize("hasAuthority('read')")
@GetMapping("/resource/article/read")
public Map<String, Object> read(@AuthenticationPrincipal Jwt jwt) {
Map<String, Object> result = new HashMap<>(2);
result.put("principal", jwt.getClaims());
result.put("article", article);
return result;
}
@PreAuthorize("hasAuthority('write')")
@GetMapping("/resource/article/write")
public String write(@RequestParam String name) {
article.add(name);
return "success";
}
}
Testing our application
After we have started the service, we access http://127.0.0.1:8070/resource/article/read in the browser and we will be redirected to the authorization service login page as shown below:
After we enter the username and password (admin/password), we will get the request response information:
The role of the admin user is ROLE_ADMIN
, so we try to request http://127.0.0.1:8070/resource/article/write?name=article3
After logging out, we also access http://127.0.0.1:8070/resource/article/read, but this time we use Github login, and the response information is as shown below:
We can see that the response information shows that the user has switched to your Github username.
Users who log in with Github are assigned the ROLE_OPERATION
role by default, and ROLE_OPERATION
does not have access to http://127.0.0.1:8070/resource/article/write?name=article3. Let's try to test it:
The result shows that our request is rejected, with a 403 status code indicating that we do not have access permissions.
Conclusion
In this article, you learned how to use Spring Cloud Gateway to protect microservices with OAuth2. In the example, the browser cookie only stores the session ID, and the JWT access token is not exposed to the browser but flows internally in the service. This way, we experience the advantages of JWT and also use the cookie-session to compensate for the shortcomings of JWT, such as when we need to implement a forced user logout function.
As always, the source code used in this article is available on GitHub.
Thanks for reading! 😊
Top comments (2)
Hey great article on adding OAuth2 authentication to Spring - learned a lot. Recently, I was digging deeper into Spring + WebAuthn / passkey authentication. Have you ever implemented that, too?
Thank u so much. I have researched bff pattern and got many troubles with it, this is exactly what I want