Dlaczego OAuth 2.0 to standard dla mikrousług?
W monolicie zarządzanie sesjami jest proste – jeden serwer, jedna baza sesji. W mikrousługach to jak zarządzanie wejściówkami na wielki festiwal – potrzebujesz systemu który pozwala na sprawdzenie uprawnień w każdym namiocie bez dzwonienia do centrali. OAuth 2.0 z JWT tokens rozwiązuje ten problem przez delegated authorization i stateless verification.
Co się nauczysz:
- OAuth 2.0 flows: Authorization Code, Client Credentials, Resource Owner
- JWT tokens vs opaque tokens w mikrousługach
- Spring Security OAuth2 – Authorization Server setup
- Resource Server configuration i token validation
- API Gateway integration z OAuth 2.0
OAuth 2.0 Fundamentals
Kluczowe komponenty OAuth 2.0
Komponent | Odpowiedzialność | W mikrousługach |
---|---|---|
Resource Owner | Właściciel danych (user) | End user aplikacji |
Client | Aplikacja żądająca dostępu | Frontend app, mobile app |
Authorization Server | Wydaje access tokens | Centralny auth service |
Resource Server | Sprawdza tokeny, służy dane | Każdy mikroservice |
OAuth 2.0 Grant Types
1. Authorization Code Flow (Web Applications) Client → User → Authorization Server → Client → Resource Server 2. Client Credentials Flow (Service-to-Service) Service → Authorization Server → Service → Resource Server 3. Resource Owner Password Credentials (Legacy/Internal) App → User credentials → Authorization Server → Resource Server 4. Implicit Flow (SPA - deprecated in OAuth 2.1) SPA → Authorization Server → SPA → Resource Server
Spring Security OAuth2 – Authorization Server
Maven dependencies
org.springframework.boot spring-boot-starter-security org.springframework.security.oauth spring-security-oauth2 2.1.0.RELEASE org.springframework.security spring-security-jwt 1.0.7.RELEASE
Authorization Server Configuration
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // Web Application Client .withClient("web-client") .secret("web-secret") .authorizedGrantTypes("authorization_code", "refresh_token") .scopes("read", "write") .redirectUris("http://localhost:8080/callback") .accessTokenValiditySeconds(3600) // 1 hour .refreshTokenValiditySeconds(86400) // 24 hours .and() // Service-to-Service Client .withClient("microservice-client") .secret("microservice-secret") .authorizedGrantTypes("client_credentials") .scopes("service") .accessTokenValiditySeconds(1800); // 30 minutes } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .tokenStore(tokenStore()) .accessTokenConverter(jwtAccessTokenConverter()); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security .tokenKeyAccess("permitAll()") // /oauth/token_key .checkTokenAccess("isAuthenticated()") // /oauth/check_token .allowFormAuthenticationForClients(); // Client secret in form } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("mySecretKey"); // In production: use proper key management return converter; } }
User Authentication Configuration
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user") .password("{noop}password") .roles("USER") .and() .withUser("admin") .password("{noop}admin") .roles("USER", "ADMIN"); } @Override protected void configure(HttpSecurity http) throws Exception { http .formLogin().permitAll() .and() .authorizeRequests() .antMatchers("/oauth/**").permitAll() .anyRequest().authenticated(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
Resource Server – mikroservices protection
Resource Server Configuration
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/products/**").hasScope("read") .antMatchers(HttpMethod.POST, "/api/products/**").hasScope("write") .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated(); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources .tokenStore(tokenStore()) .resourceId("product-service"); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("mySecretKey"); // Same key as Authorization Server return converter; } }
Protected REST Controller
@RestController @RequestMapping("/api/products") public class ProductController { @Autowired private ProductService productService; @GetMapping @PreAuthorize("hasScope('read')") public ListgetAllProducts() { return productService.findAll(); } @PostMapping @PreAuthorize("hasScope('write')") public Product createProduct(@RequestBody Product product, Authentication auth) { // Extract user info from token String username = auth.getName(); product.setCreatedBy(username); return productService.save(product); } @GetMapping("/my") @PreAuthorize("hasScope('read')") public List getUserProducts(Authentication auth) { String username = auth.getName(); return productService.findByCreatedBy(username); } @DeleteMapping("/{id}") @PreAuthorize("hasScope('write') and hasRole('ADMIN')") public void deleteProduct(@PathVariable Long id) { productService.delete(id); } }
Custom JWT Claims
@Component public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { DefaultOAuth2AccessToken enhancedToken = new DefaultOAuth2AccessToken(accessToken); MapadditionalInfo = new HashMap<>(); // Add custom claims if (authentication.getPrincipal() instanceof UserDetails) { UserDetails user = (UserDetails) authentication.getPrincipal(); additionalInfo.put("username", user.getUsername()); additionalInfo.put("authorities", user.getAuthorities()); additionalInfo.put("issued_at", System.currentTimeMillis() / 1000); } // Add client information additionalInfo.put("client_id", authentication.getOAuth2Request().getClientId()); enhancedToken.setAdditionalInformation(additionalInfo); return enhancedToken; } } // Usage in Authorization Server @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList( customTokenEnhancer, jwtAccessTokenConverter() )); endpoints .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager); }
Service-to-Service Communication
Client Credentials Flow dla mikrousług
@Service public class OrderService { @Autowired private OAuth2RestTemplate oAuth2RestTemplate; @Value("${inventory.service.url}") private String inventoryServiceUrl; public void processOrder(Order order) { // Call inventory service with OAuth2 token for (OrderItem item : order.getItems()) { String url = inventoryServiceUrl + "/api/inventory/reserve"; ReservationRequest request = new ReservationRequest(); request.setProductId(item.getProductId()); request.setQuantity(item.getQuantity()); try { ReservationResponse response = oAuth2RestTemplate.postForObject( url, request, ReservationResponse.class); if (!response.isSuccess()) { throw new InsufficientInventoryException(item.getProductId()); } } catch (HttpClientErrorException e) { if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) { throw new ServiceAuthenticationException("Inventory service auth failed"); } throw e; } } } }
OAuth2RestTemplate Configuration
@Configuration @EnableOAuth2Client public class OAuth2ClientConfig { @Bean @ConfigurationProperties("oauth2.client") public ClientCredentialsResourceDetails clientCredentialsResourceDetails() { return new ClientCredentialsResourceDetails(); } @Bean public RequestInterceptor oauth2FeignRequestInterceptor() { return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails()); } @Bean public OAuth2RestTemplate oauth2RestTemplate() { OAuth2RestTemplate template = new OAuth2RestTemplate( clientCredentialsResourceDetails(), new DefaultOAuth2ClientContext()); template.setRequestFactory(new HttpComponentsClientHttpRequestFactory()); return template; } }
# application.yml oauth2: client: client-id: order-service client-secret: order-secret access-token-uri: http://auth-server:8080/oauth/token grant-type: client_credentials scope: service inventory: service: url: http://inventory-service:8081
API Gateway Integration
Zuul Gateway z OAuth2
@SpringBootApplication @EnableZuulProxy @EnableOAuth2Sso public class GatewayApplication { @Bean public PreFilter tokenRelayFilter() { return new PreFilter() { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 10000; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); // Extract Authorization header String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { // Add token to downstream request ctx.addZuulRequestHeader("Authorization", authHeader); } return null; } }; } }
# application.yml - Gateway configuration zuul: routes: product-service: path: /api/products/** service-id: product-service strip-prefix: false order-service: path: /api/orders/** service-id: order-service strip-prefix: false sensitive-headers: # Don't filter these headers security: oauth2: client: client-id: gateway-client client-secret: gateway-secret resource: jwt: key-uri: http://auth-server:8080/oauth/token_key
Production Considerations
Token Management Best Practices
Aspekt | Development | Production |
---|---|---|
Token Storage | In-memory | Redis/Database |
Key Management | Static symmetric | Rotating asymmetric |
Token Expiration | Long (hours) | Short (15-30 min) |
Refresh Tokens | Optional | Mandatory |
Monitoring | Basic logging | Metrics, alerts |
Security monitoring
@Component public class OAuth2SecurityEventListener { private static final Logger logger = LoggerFactory.getLogger(OAuth2SecurityEventListener.class); @EventListener public void handleFailedAuthentication(AbstractAuthenticationFailureEvent event) { logger.warn("Authentication failed: {}", event.getException().getMessage()); // Send to monitoring system } @EventListener public void handleSuccessfulAuthentication(AuthenticationSuccessEvent event) { logger.info("Successful authentication: {}", event.getAuthentication().getName()); } @EventListener public void handleTokenRevocation(TokenRevocationEvent event) { logger.info("Token revoked: client={}, token_hint={}", event.getClientId(), event.getTokenHint()); } }
JWT dla stateless verification (no database calls), opaque tokens dla better security (revokable). W mikrousługach JWT często wins przez performance, ale używaj short expiration i proper key rotation.
Client (frontend/mobile) handles refresh automatically. Backend services używają client_credentials flow który nie potrzebuje refresh. API Gateway może implement transparent token refresh dla user tokens.
Tak, każdy service powinien validate tokens independently. Shared library lub starter może standardize configuration. Centralny validation service creates single point of failure.
Include roles/permissions w JWT claims lub use external authorization service (like Open Policy Agent). Avoid database calls per request. Cache permissions when possible.
Configure CORS na Authorization Server dla browser-based flows. Resource servers need CORS dla direct SPA calls. API Gateway can handle CORS centrally dla wszystkich services.
🚀 Zadanie dla Ciebie
Zbuduj kompletny OAuth2 ecosystem:
- Authorization Server z JWT tokens i custom claims
- Product Service jako Resource Server z scope-based protection
- Order Service wywołujący Product Service z client_credentials
- Zuul Gateway z token relay dla frontend requests
- Frontend SPA używający authorization_code flow
- Monitoring token usage i failed authentications
Test complete user journey: login → browse products → create order → service-to-service calls.
Przydatne zasoby:
- OAuth 2.0 RFC Specification
- Spring Security OAuth2 Documentation
- JWT.io – JSON Web Tokens
- OAuth 2.0 Security Best Practices
Jak implementujesz autoryzację w swoich mikrousługach? Używasz JWT czy opaque tokens? Jakie największe challenges napotkałeś z OAuth2?