OAuth 2.0 – autoryzacja w mikrousługach

TL;DR: OAuth 2.0 w mikrousługach pozwala na centralized authentication z distributed authorization. Kluczowe komponenty to Authorization Server (wydaje tokeny), Resource Server (sprawdza tokeny) i Client (aplikacja). JWT tokens umożliwiają stateless authorization bez shared database.

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.

Spring Security OAuth2 (wersja 2.x w 2017) zapewnia ready-to-use components dla Authorization Server i Resource Server. JWT support został dodany w Spring Security 4.2.

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
Wymagania wstępne: Znajomość Spring Security, podstawy mikrousług, pojęcie authentication vs authorization, doświadczenie z REST API.

OAuth 2.0 Fundamentals

Kluczowe komponenty OAuth 2.0

KomponentOdpowiedzialnośćW mikrousługach
Resource OwnerWłaściciel danych (user)End user aplikacji
ClientAplikacja żądająca dostępuFrontend app, mobile app
Authorization ServerWydaje access tokensCentralny auth service
Resource ServerSprawdza tokeny, służy daneKażdy mikroservice
Access Token – credential używany do dostępu do protected resources. W mikrousługach często implementowany jako JWT dla stateless verification.

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();
    }
}
Pro tip: W produkcji używaj external user store (database, LDAP) i proper key management (rotate keys, use asymmetric signatures). In-memory setup jest tylko dla development.

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 List getAllProducts() {
        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);
        Map additionalInfo = 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);
}
Pułapka: JWT tokens są stateless ale nie mogą być revoked przed expiration. Dla critical operations używaj shorter expiration times i refresh token rotation.

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

AspektDevelopmentProduction
Token StorageIn-memoryRedis/Database
Key ManagementStatic symmetricRotating asymmetric
Token ExpirationLong (hours)Short (15-30 min)
Refresh TokensOptionalMandatory
MonitoringBasic loggingMetrics, 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 vs Opaque tokens – które wybrać w mikrousługach?

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.

Jak obsłużyć token refresh w mikrousługach?

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.

Czy każdy mikroservice potrzebuje własny Resource Server?

Tak, każdy service powinien validate tokens independently. Shared library lub starter może standardize configuration. Centralny validation service creates single point of failure.

Jak zarządzać permissions w distributed system?

Include roles/permissions w JWT claims lub use external authorization service (like Open Policy Agent). Avoid database calls per request. Cache permissions when possible.

Co z CORS w OAuth2 setup?

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:

  1. Authorization Server z JWT tokens i custom claims
  2. Product Service jako Resource Server z scope-based protection
  3. Order Service wywołujący Product Service z client_credentials
  4. Zuul Gateway z token relay dla frontend requests
  5. Frontend SPA używający authorization_code flow
  6. Monitoring token usage i failed authentications

Test complete user journey: login → browse products → create order → service-to-service calls.

Przydatne zasoby:

Jak implementujesz autoryzację w swoich mikrousługach? Używasz JWT czy opaque tokens? Jakie największe challenges napotkałeś z OAuth2?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry