Java与OpenID Connect/OAuth 2.1:实现安全的微服务间认证授权

好的,以下是一篇关于Java与OpenID Connect/OAuth 2.1:实现安全的微服务间认证授权的技术讲座文章。

Java与OpenID Connect/OAuth 2.1:实现安全的微服务间认证授权

大家好!今天我们来探讨一个在微服务架构中至关重要的话题:如何使用Java与OpenID Connect (OIDC) 和 OAuth 2.1 实现安全的微服务间认证和授权。

一、微服务安全挑战与解决方案

在单体应用时代,安全性通常由应用服务器统一管理。但在微服务架构中,服务之间相互独立,直接暴露API给外部世界。这带来了以下挑战:

  • 身份验证(Authentication): 如何验证访问服务的用户或服务的身份?
  • 授权(Authorization): 如何确定已验证的用户或服务是否有权访问特定的资源?
  • 凭证管理: 如何安全地存储和传递用户或服务的凭证?
  • 安全审计: 如何追踪和审计对服务的访问行为?

OpenID Connect (OIDC) 和 OAuth 2.1 提供了解决这些问题的标准方法。

  • OAuth 2.1: 是一个授权框架,允许第三方应用代表用户访问资源,无需共享用户的凭证。
  • OpenID Connect (OIDC): 构建在 OAuth 2.1 之上,提供身份验证功能。它可以验证用户的身份,并获取关于用户的基本信息,例如姓名、电子邮件地址等。

二、OAuth 2.1 核心概念

理解 OAuth 2.1 的核心概念是至关重要的:

概念 描述
Resource Owner 拥有受保护资源的实体,通常是用户。
Client 代表资源所有者访问受保护资源的应用程序。
Authorization Server 负责验证资源所有者的身份,并颁发访问令牌。
Resource Server 托管受保护资源的服务器,它使用访问令牌来验证客户端的访问权限。
Access Token 客户端用来访问受保护资源的凭证。它代表资源所有者授予客户端的访问权限。
Refresh Token 客户端用来获取新的访问令牌的凭证,无需资源所有者再次授权。
Scopes 定义客户端可以访问的资源范围。
Grant Types 定义客户端如何获取访问令牌的方式。常见的授权类型包括授权码模式、密码模式、客户端凭据模式和隐式模式。OAuth2.1已移除隐式模式和密码模式,推荐使用授权码模式和客户端凭据模式。

三、OIDC 流程

OIDC流程主要包括以下步骤:

  1. 客户端发起授权请求: 客户端将用户重定向到授权服务器,请求授权。
  2. 用户认证和授权: 用户在授权服务器上进行身份验证,并授权客户端访问其资源。
  3. 授权服务器颁发授权码: 授权服务器颁发一个授权码给客户端。
  4. 客户端使用授权码获取访问令牌和ID令牌: 客户端将授权码发送到授权服务器,并获得访问令牌和ID令牌。
  5. 客户端使用访问令牌访问资源服务器: 客户端使用访问令牌访问资源服务器上的受保护资源。
  6. 资源服务器验证访问令牌: 资源服务器验证访问令牌的有效性,并授予客户端访问权限。
  7. 客户端使用ID令牌获取用户信息: 客户端可以使用ID令牌来获取有关已验证用户的基本信息。

四、Java 实现 OAuth 2.1 和 OIDC

现在我们来看如何使用 Java 实现 OAuth 2.1 和 OIDC。我们将使用 Spring Security OAuth2 框架来简化开发。

1. 引入依赖

首先,在 pom.xml 文件中添加 Spring Security OAuth2 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. 配置 Resource Server

我们需要配置 Resource Server 来保护我们的微服务。这涉及到验证传入的访问令牌。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/public").permitAll() // 允许匿名访问
                .anyRequest().authenticated() // 其他请求需要认证
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    // 这里可以配置 JWT 验证器,例如设置 JWKS URI
                )
            );
        return http.build();
    }
}

代码解释:

  • @EnableWebSecurity: 启用 Spring Security 的 Web 安全功能。
  • SecurityFilterChain: 定义安全过滤器链。
  • authorizeHttpRequests: 配置授权规则。
    • requestMatchers("/public").permitAll(): 允许匿名访问 /public 路径。
    • anyRequest().authenticated(): 所有其他请求都需要认证。
  • oauth2ResourceServer: 配置 OAuth 2.0 资源服务器。
    • jwt: 配置 JWT (JSON Web Token) 支持。
      • jwtAuthenticationConverter(): 可以自定义 JWT 认证转换器,将 JWT 声明转换为 Spring Security 的 Authentication 对象。
      • jwkSetUri(): 设置 JWKS (JSON Web Key Set) URI,用于验证 JWT 签名。

3. 配置 Authorization Server (示例使用 Spring Authorization Server)

Spring Authorization Server 是一个开源的 OAuth 2.1 和 OIDC 授权服务器实现。

引入依赖:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>1.2.0</version> <!-- 替换为最新版本 -->
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

配置:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;

import java.util.UUID;

@Configuration
public class AuthorizationServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("messaging-client")
                .clientSecret("{noop}secret") // 生产环境替换为安全的存储方式
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc") // 替换为你的客户端回调地址
                .redirectUri("http://127.0.0.1:8080/authorized")
                .scope(OidcScopes.OPENID)
                .scope("message.read")
                .scope("message.write")
                .clientSettings(clientSettings -> clientSettings.requireUserConsent(false)) // 是否需要用户确认授权
                .build();

        // Save registered client in db as if in-memory
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        registeredClientRepository.save(registeredClient);

        return registeredClientRepository;
    }

    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder().issuer("http://auth-server:9000").build();
    }

}

代码解释:

  • authorizationServerSecurityFilterChain: 配置授权服务器的安全过滤器链。
  • RegisteredClientRepository: 用于管理已注册的客户端。这里使用 JdbcRegisteredClientRepository 将客户端信息存储在数据库中。
    • clientId: 客户端的 ID。
    • clientSecret: 客户端的密钥。注意:在生产环境中,不要使用 {noop} 前缀,应该使用加密存储的密钥。
    • clientAuthenticationMethod: 客户端的身份验证方法。CLIENT_SECRET_BASIC 表示使用 HTTP Basic 认证。
    • authorizationGrantType: 客户端支持的授权类型。
    • redirectUri: 授权服务器将用户重定向回客户端的 URI。
    • scope: 客户端请求的权限范围。
    • clientSettings: 客户端设置。
      • requireUserConsent(false): 是否需要用户确认授权。
  • ProviderSettings: 配置授权服务器的提供者设置。
    • issuer: 授权服务器的 issuer URI。

4. 配置客户端

客户端需要配置 OAuth2 客户端属性,以便与授权服务器进行交互。可以使用 Spring Boot 的 application.ymlapplication.properties 文件进行配置。

spring:
  security:
    oauth2:
      client:
        registration:
          messaging-client-oidc:
            client-id: messaging-client
            client-secret: secret
            client-authentication-method: client_secret_basic
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc"
            scope: openid, message.read, message.write
        provider:
          oidc-provider:
            issuer-uri: http://auth-server:9000

server:
  port: 8080

代码解释:

  • spring.security.oauth2.client.registration: 配置客户端注册信息。
    • client-id: 客户端的 ID,与授权服务器注册的客户端 ID 匹配。
    • client-secret: 客户端的密钥,与授权服务器注册的客户端密钥匹配。
    • client-authentication-method: 客户端的身份验证方法。
    • authorization-grant-type: 客户端使用的授权类型。
    • redirect-uri: 授权服务器将用户重定向回客户端的 URI。
    • scope: 客户端请求的权限范围。
  • spring.security.oauth2.client.provider: 配置 OAuth2 提供者信息。
    • issuer-uri: 授权服务器的 issuer URI。

5. 创建Controller

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageController {

    @GetMapping("/messages")
    public String getMessage(Authentication authentication) {
        return "Message from resource server. User: " + authentication.getName();
    }

    @GetMapping("/public")
    public String getPublicMessage() {
        return "Public message from resource server.";
    }
}

代码解释:

  • /messages:受保护的API,只有经过身份验证的用户才能访问。
  • /public:公共API,允许匿名访问。

五、微服务间的认证和授权

以上展示了用户如何通过 OIDC 进行认证,以及客户端如何使用 OAuth 2.1 访问资源。现在让我们看看如何实现微服务间的认证和授权。

1. 使用 Client Credentials Grant

当一个微服务需要代表自身而不是用户访问另一个微服务时,可以使用 Client Credentials Grant。

  • 客户端微服务: 使用其客户端 ID 和密钥向授权服务器请求访问令牌。
  • 授权服务器: 验证客户端的身份,并颁发访问令牌。
  • 客户端微服务: 使用访问令牌访问目标微服务。

2. 配置客户端微服务

在客户端微服务的 application.ymlapplication.properties 中配置 Client Credentials Grant:

spring:
  security:
    oauth2:
      client:
        registration:
          internal-client: # 内部客户端的名称
            client-id: internal-service-client # 客户端ID
            client-secret: internal-service-secret # 客户端密钥
            client-authentication-method: client_secret_basic
            authorization-grant-type: client_credentials
            scope: message.read # 客户端请求的权限范围
        provider:
          internal-provider:
            issuer-uri: http://auth-server:9000 # 授权服务器的issuer URI

3. 获取访问令牌

使用 OAuth2AuthorizedClientManager 获取访问令牌。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;

@Service
public class InternalServiceClient {

    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Autowired
    private OAuth2AuthorizedClientManager authorizedClientManager;

    public String getMessageFromResourceServer() {
        ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId("internal-client");
        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("internal-client")
                .principal("internal-service") // 设置principal
                .build();

        OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);

        if (authorizedClient == null) {
            throw new RuntimeException("Failed to authorize client.");
        }

        return WebClient.builder()
                .baseUrl("http://resource-server:8080") // 替换为资源服务器的地址
                .defaultHeader("Authorization", "Bearer " + authorizedClient.getAccessToken().getTokenValue())
                .build()
                .get()
                .uri("/messages")
                .retrieve()
                .bodyToMono(String.class)
                .block();
    }
}

代码解释:

  • ClientRegistrationRepository: 用于获取客户端注册信息。
  • OAuth2AuthorizedClientService: 用于管理已授权的客户端。
  • OAuth2AuthorizedClientManager: 用于授权客户端并获取访问令牌。
  • authorizeRequest: 创建 OAuth2 授权请求,指定客户端注册 ID。
  • authorizedClientManager.authorize(authorizeRequest): 授权客户端并获取访问令牌。
  • WebClient: 用于发送 HTTP 请求。
    • defaultHeader("Authorization", "Bearer " + authorizedClient.getAccessToken().getTokenValue()): 设置授权头,包含访问令牌。

4. 目标微服务

目标微服务需要像之前一样配置为 Resource Server,验证传入的访问令牌。

六、安全注意事项

  • 密钥管理: 安全地存储和管理客户端密钥,避免硬编码在代码中。使用 Vault、AWS Secrets Manager 等密钥管理工具。
  • 传输安全: 使用 HTTPS 协议进行所有通信,确保数据在传输过程中得到加密。
  • 输入验证: 验证所有输入数据,防止注入攻击。
  • 最小权限原则: 授予客户端最小必要的权限范围。
  • 定期审计: 定期审计安全配置和访问日志,及时发现和解决安全问题。
  • Token过期时间: 设置合理的Token过期时间,避免Token被长期滥用。

七、总结

本次讲座我们深入探讨了如何使用Java与OpenID Connect和OAuth 2.1实现微服务间的安全认证和授权。正确实施这些技术能够确保你的微服务架构安全可靠,为用户提供更安全的体验。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注