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.
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.jksImplement 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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
