How to Build a Unified OAuth2 Authentication & Authorization System with Spring Cloud Gateway

This guide walks through building a unified microservice authentication and authorization solution using Spring Boot 2.2+, Spring Cloud Hoxton, Nacos, Spring Cloud Gateway, and JWT, detailing the architecture, service division, and step‑by‑step implementation of auth, gateway, and API services with code examples.

macrozheng
macrozheng
macrozheng
How to Build a Unified OAuth2 Authentication & Authorization System with Spring Cloud Gateway
Recently a solid microservice permission solution was discovered, which uses a centralized authentication service and a gateway for unified authentication and authorization. This solution works with Spring Boot 2.2.0, Spring Cloud Hoxton and later versions.

Prerequisite Knowledge

We will use Nacos as the service registry, Spring Cloud Gateway as the gateway, and the nimbus-jose-jwt library for JWT handling. For those unfamiliar with these technologies, see the referenced articles.

Spring Cloud Gateway: Next‑generation API gateway service

Spring Cloud Alibaba: Using Nacos as a registry and configuration center

Recommended JWT library

Application Architecture

The ideal solution is: the authentication service handles authentication, the gateway handles authentication verification and authorization, and other API services focus on business logic. Security logic resides only in the authentication and gateway services.

Related service division:

micro‑oauth2‑gateway: gateway service for request forwarding and authorization, integrates Spring Security + OAuth2

micro‑oauth2‑auth: OAuth2 authentication service, integrates Spring Security + OAuth2

micro‑oauth2‑api: protected API service, accessed after successful authorization, does not integrate Spring Security + OAuth2

Solution Implementation

Below is the detailed implementation, building the authentication service, gateway service, and API service in order.

micro‑oauth2‑auth

First we set up the authentication service, which will serve as the OAuth2 authentication server and also provide the gateway's authorization capabilities.

Add the necessary dependencies (Spring Security, OAuth2, JWT, Redis) to pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>com.nimbusds</groupId>
        <artifactId>nimbus-jose-jwt</artifactId>
        <version>8.16</version>
    </dependency>
    <!-- redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

Add Nacos and Redis configuration to application.yml:

server:
  port: 9401
spring:
  profiles:
    active: dev
  application:
    name: micro-oauth2-auth
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
  redis:
    database: 0
    port: 6379
    host: localhost
    password:
management:
  endpoints:
    web:
      exposure:
        include: "*"

Generate an RSA key pair for JWT using keytool and place jwt.jks under resource:

keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

Implement UserServiceImpl that implements UserDetailsService to load user information:

/**
 * User management service
 */
@Service
public class UserServiceImpl implements UserDetailsService {
    private List<UserDTO> userList;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    public void initData() {
        String password = passwordEncoder.encode("123456");
        userList = new ArrayList<>();
        userList.add(new UserDTO(1L, "macro", password, 1, CollUtil.toList("ADMIN")));
        userList.add(new UserDTO(2L, "andy", password, 1, CollUtil.toList("TEST")));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<UserDTO> findUserList = userList.stream()
            .filter(item -> item.getUsername().equals(username))
            .collect(Collectors.toList());
        if (CollUtil.isEmpty(findUserList)) {
            throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
        }
        SecurityUser securityUser = new SecurityUser(findUserList.get(0));
        if (!securityUser.isEnabled()) {
            throw new DisabledException(MessageConstant.ACCOUNT_DISABLED);
        } else if (!securityUser.isAccountNonLocked()) {
            throw new LockedException(MessageConstant.ACCOUNT_LOCKED);
        } else if (!securityUser.isAccountNonExpired()) {
            throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);
        } else if (!securityUser.isCredentialsNonExpired()) {
            throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);
        }
        return securityUser;
    }
}

Configure the authentication server ( Oauth2ServerConfig) to load user details and RSA key pair:

/**
 * Authentication server configuration
 */
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
    private final PasswordEncoder passwordEncoder;
    private final UserServiceImpl userDetailsService;
    private final AuthenticationManager authenticationManager;
    private final JwtTokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            .withClient("client-app")
            .secret(passwordEncoder.encode("123456"))
            .scopes("all")
            .authorizedGrantTypes("password", "refresh_token")
            .accessTokenValiditySeconds(3600)
            .refreshTokenValiditySeconds(86400);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(accessTokenConverter());
        enhancerChain.setTokenEnhancers(delegates);
        endpoints.authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService)
            .accessTokenConverter(accessTokenConverter())
            .tokenEnhancer(enhancerChain);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }

    @Bean
    public KeyPair keyPair() {
        // Load key pair from classpath keystore
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        return factory.getKeyPair("jwt", "123456".toCharArray());
    }
}

If you need custom information (e.g., user ID) in the JWT, implement TokenEnhancer:

/**
 * JWT content enhancer
 */
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
        Map<String, Object> info = new HashMap<>();
        info.put("id", securityUser.getId()); // add user ID to JWT
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}

Expose the RSA public key via an endpoint so the gateway can verify signatures:

/**
 * RSA public key endpoint
 */
@RestController
public class KeyPairController {
    @Autowired
    private KeyPair keyPair;

    @GetMapping("/rsa/publicKey")
    public Map<String, Object> getKey() {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }
}

Configure Spring Security to permit access to the public‑key endpoint:

/**
 * Spring Security configuration
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
            .antMatchers("/rsa/publicKey").permitAll()
            .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Cache resource‑role mappings in Redis for fast authorization checks:

/**
 * Resource‑role management service
 */
@Service
public class ResourceServiceImpl {
    private Map<String, List<String>> resourceRolesMap;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostConstruct
    public void initData() {
        resourceRolesMap = new TreeMap<>();
        resourceRolesMap.put("/api/hello", CollUtil.toList("ADMIN"));
        resourceRolesMap.put("/api/user/currentUser", CollUtil.toList("ADMIN", "TEST"));
        redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap);
    }
}

micro‑oauth2‑gateway

Next we set up the gateway service, which acts as the OAuth2 resource server and client, performing unified authentication and authorization for all microservice requests.

Add gateway‑related dependencies to pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-resource-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-jose</artifactId>
    </dependency>
    <dependency>
        <groupId>com.nimbusds</groupId>
        <artifactId>nimbus-jose-jwt</artifactId>
        <version>8.16</version>
    </dependency>
</dependencies>

Configure routing, JWT public‑key location, and whitelist URLs in application.yml:

server:
  port: 9201
spring:
  profiles:
    active: dev
  application:
    name: micro-oauth2-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes:
        - id: oauth2-api-route
          uri: lb://micro-oauth2-api
          predicates:
            - Path=/api/**
          filters:
            - StripPrefix=1
        - id: oauth2-auth-route
          uri: lb://micro-oauth2-auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: 'http://localhost:9401/rsa/publicKey'
  redis:
    database: 0
    port: 6379
    host: localhost
    password:
secure:
  ignore:
    urls:
      - "/actuator/**"
      - "/auth/oauth/token"

Resource server configuration using WebFlux security:

/**
 * Resource server configuration
 */
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
    private final AuthorizationManager authorizationManager;
    private final IgnoreUrlsConfig ignoreUrlsConfig;
    private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
        http.authorizeExchange()
            .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll()
            .anyExchange().access(authorizationManager)
            .and().exceptionHandling()
            .accessDeniedHandler(restfulAccessDeniedHandler)
            .authenticationEntryPoint(restAuthenticationEntryPoint)
            .and().csrf().disable();
        return http.build();
    }

    @Bean
    public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter granted = new JwtGrantedAuthoritiesConverter();
        granted.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
        granted.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(granted);
        return new ReactiveJwtAuthenticationConverterAdapter(converter);
    }
}

Global filter that extracts user information from JWT and puts it into request headers:

/**
 * Convert JWT to user info header
 */
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StrUtil.isEmpty(token)) {
            return chain.filter(exchange);
        }
        try {
            String realToken = token.replace("Bearer ", "");
            JWSObject jwsObject = JWSObject.parse(realToken);
            String userStr = jwsObject.getPayload().toString();
            LOGGER.info("AuthGlobalFilter.filter() user:{}", userStr);
            ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build();
            exchange = exchange.mutate().request(request).build();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

micro‑oauth2‑api

The API service contains no security logic; it relies entirely on the gateway for protection.

Add a simple web starter dependency to pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Basic service configuration in application.yml:

server:
  port: 9501
spring:
  profiles:
    active: dev
  application:
    name: micro-oauth2-api
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
management:
  endpoints:
    web:
      exposure:
        include: "*"

Test controller:

/**
 * Test endpoint
 */
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello World.";
    }
}

Component to retrieve the logged‑in user from request headers:

/**
 * Retrieve login user information
 */
@Component
public class LoginUserHolder {
    public UserDTO getCurrentUser() {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attrs.getRequest();
        String userStr = request.getHeader("user");
        JSONObject json = new JSONObject(userStr);
        UserDTO user = new UserDTO();
        user.setUsername(json.getStr("user_name"));
        user.setId(Convert.toLong(json.get("id")));
        user.setRoles(Convert.toList(String.class, json.get("authorities")));
        return user;
    }
}

User controller exposing the current user information:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private LoginUserHolder loginUserHolder;

    @GetMapping("/currentUser")
    public UserDTO currentUser() {
        return loginUserHolder.getCurrentUser();
    }
}

Function Demonstration

The following demonstrates the unified authentication and authorization flow; all requests go through the gateway.

Start Nacos and Redis, then sequentially start micro-oauth2-auth, micro-oauth2-gateway and micro-oauth2-api services.

Obtain a JWT token using password grant at http://localhost:9201/auth/oauth/token.

Access a protected endpoint with the token at http://localhost:9201/api/hello.

Get the current logged‑in user info at http://localhost:9201/api/user/currentUser.

When the JWT expires, obtain a new token using refresh_token at http://localhost:9201/auth/oauth/token.

Using an account without sufficient permissions (e.g., andy) results in an access‑denied response when calling http://localhost:9201/api/hello.

Project Source Code

https://github.com/macrozheng/springcloud-learning/tree/master/micro-oauth2

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

MicroservicesgatewaySpring CloudJWTOAuth2spring-security
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.