将JWT与Spring Security OAuth2一起使用
#java #jwt #oauth2 #springsecurity

概述

OAUTH 2.0是行业标准授权协议。 OAuth 2.0专注于客户开发人员的简单性,同时为Web应用程序,桌面应用程序,手机和客厅设备提供特定的授权流。

OAuth授权服务器负责认证用户并发出包含用户数据和适当访问策略的访问令牌。

下面我们将使用Spring Authorization Server构建简单的授权服务器。

OAuth2授权服务器实现

让我们从OAuth2授权服务器配置实现开始。

Maven依赖性

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
   <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-authorization-server</artifactId>
  <version>0.3.1</version>
</dependency>

配置

首先让我们通过application.yml配置数据库连接信息。

spring:
  application:
    name: auth-server
  datasource:
    druid:
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/integrated_oauth?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: <<username>> # modify username
      password: <<password>> # change Password

然后我们创建一个AuthorizationServerConfig配置类,在此类中,我们将创建OAuth2授权服务器所需的特定bean。第一个将是客户端服务存储库,我们在其中使用注册的构建器类型创建客户端,并将其持续到数据库。

 @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("relive-client")
                .clientSecret("{noop}relive-client")
                .clientAuthenticationMethods(s -> {
                    s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
                    s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
                })
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-client-authorization-code")
                .scope("message.read")
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(true)
                        .requireProofKey(false)
                        .build())
                .tokenSettings(TokenSettings.builder()
                        .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
                        .accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
                        .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
                        .reuseRefreshTokens(true)
                        .build())
                .build();

        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        registeredClientRepository.save(registeredClient);

        return registeredClientRepository;
    }

我们配置的属性是:

  • id- registeredClient唯一ID
  • clientId-客户标识符
  • clients cretecret--client Secret
  • clientauthenticationmethods-客户端可能使用的身份验证方法。支持的值是client_secret_basicclient_secret_postprivate_key_jwtclient_secret_jwtnone
  • 授权granttypes - 客户可以使用的赠款类型。支持的值是authorization_codeclient_credentialsrefresh_token
  • redirecturis--委托人已注册重定向URI
  • 范围 - 允许客户请求
  • 的范围
  • 客户端 - 委托自定义设置

  • tokensettings-发给客户的OAuth2代币的custom设置

接下来,让我们配置中央组件oauth2authorizationservice,以存储新的授权和查询现有的授权。

   @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

对于OAuth2授权请求的授权“同意”,Spring提供了 oauth2authorizationConsentservice 组件用于存储新授权同意并查询现有授权同意的组件。

   @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

接下来,让我们创建一个bean,使用其他默认配置配置OAuth2授权服务,然后使用它将请求重定向到登录页面,以获取未经验证的授权请求。

   @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.exceptionHandling(exceptions -> exceptions.
                authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))).build();
    }

每个授权服务器都需要代币的签名密钥,让我们生成一个RSA密钥:

final class KeyGeneratorUtils {

    private KeyGeneratorUtils() {
    }

    static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }
}
public final class Jwks {

    private Jwks() {
    }

    public static RSAKey generateRsa() {
        KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }
}
 @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = Jwks.generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

处理令牌的签名密钥后,授权服务器还需要一个发行器URL,我们可以通过 providerSettings
创建它。

    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder()
                .issuer("http://127.0.0.1:8080")
                .build();
    }

最后,我们将启用Spring Security Security配置类以保护我们的服务。

@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {


    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .formLogin(withDefaults())
        return http.build();
    }

  //...
}

there oterizerequests anyrequest() authenticated ()使所有请求都需要身份验证并提供基于表单的身份验证。

我们还需要定义测试使用的用户信息,以下是创建基于内存的用户信息存储库。

    @Bean
    UserDetailsService users() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

资源服务器实现

现在我们将创建一个资源服务器,服务中的API接口仅允许由OAuth2授权服务器进行身份验证。

Maven依赖性

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

配置

首先让我们通过application.yml。
配置服务端口

server:
  port: 8090

下一

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:8080

资源服务器将使用此URI进一步配置自己,发现授权服务器的公共密钥,并传递用于验证JWT的JWTDECODER。此过程的结果是,授权服务器必须启动并接收资源服务器成功启动的请求。

如果资源服务器必须能够独立于授权服务器启动,则可以提供jwk-set-uri。这将是我们在OAuth2安全配置中添加的进一步属性:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:8080
          jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks

现在我们可以设置Spring Security Security配置,应授权到服务资源的每个请求并具有适当的权限:

@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.requestMatchers()
                .antMatchers("/resource/test/**")
                .and()
                .authorizeRequests()
                .mvcMatchers("/resource/test/**")
                .access("hasAuthority('SCOPE_message.read')")
                .and()
                .oauth2ResourceServer()
                .jwt();
        return http.build();
    }
}

最后,我们将创建一个REST控制器,该控制器将返回JWT索赔信息。

@RestController
public class ResourceServerTestController {

    @GetMapping("/resource/test")
    public Map<String, Object> getArticles(@AuthenticationPrincipal Jwt jwt) {
        return jwt.getClaims();
    }
}

OAuth2客户端

现在,我们要创建一个客户端,该客户端首先请求授权服务器授权以获取访问令牌,然后访问资源服务器上的相应资源。

Maven依赖性

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.6.7</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  <version>2.6.7</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
  <version>2.6.7</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webflux</artifactId>
  <version>5.3.9</version>
</dependency>

配置

首先,我们将在application.yml中配置客户端的访问端口8070。

server:
  port: 8070

接下来,我们将定义OAuth2客户端的配置属性:

spring:
  security:
    oauth2:
      client:
        registration:
          messaging-client-authorization-code:
            provider: client-provider
            client-id: relive-client
            client-secret: relive-client
            authorization-grant-type: authorization_code
            redirect-uri: "http://127.0.0.1:8070/login/oauth2/code/{registrationId}"
            scope: message.read
            client-name: messaging-client-authorization-code
        provider:
          client-provider:
            authorization-uri: http://127.0.0.1:8080/oauth2/authorize
            token-uri: http://127.0.0.1:8080/oauth2/token

现在,让我们创建一个WebClient实例,以对资源服务器执行HTTP请求:

  @Bean
    WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        return WebClient.builder()
                .apply(oauth2Client.oauth2Configuration())
                .build();
    }

WebClient添加了一个OAuth2授权过滤器,该过滤器需要OAuth2AuthorizedClientManager作为依赖项。此处仅配置了授权代码和刷新令牌,如有必要,可以添加其他模式:

@Bean
    OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
                                                          OAuth2AuthorizedClientRepository authorizedClientRepository) {

        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
                .builder()
                .authorizationCode()
                .refreshToken()
                .build();
        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

最后,我们将配置Spring Security Security配置:

 @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        //Easy to test, open permissions
                        authorizeRequests.anyRequest().permitAll()
                )
                .oauth2Client(withDefaults());
        return http.build();
    }

在这里,我们发布了所有客户端API权限,但是在实际情况下,客户服务需要身份验证。 OAUTH2协议本身是一个授权协议,不在乎特定的身份验证形式。您可以添加简单的表单身份验证。

访问资源列表

最后,我们创建一个控制器,我们将使用先前配置的WebClient将HTTP请求发送到我们的资源服务器:

@RestController
public class ClientTestController {
    @Autowired
    private WebClient webClient;

    @GetMapping(value = "/client/test")
    public Map<String, Object> getArticles(@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code") OAuth2AuthorizedClient authorizedClient) {
        return this.webClient
                .get()
                .uri("http://127.0.0.1:8090/resource/test")
                .attributes(oauth2AuthorizedClient(authorizedClient))
                .retrieve()
                .bodyToMono(Map.class)
                .block();
    }
}

在上面的示例中,我们使用 @ nocenteDoAuth2authorizedClient 注释来绑定 oauth2authorizedClient ,并触发OAuth2授权代码模式来获得访问令牌。

结论

此示例主要在使用OAUTH2协议之间证明了两项服务之间的安全通信,尤其是在复杂的Internet方案中,在该方案中,客户服务和资源服务由不同的平台提供。 OAuth2非常擅长获得用户的委托决定,从许多方面来说,它比其他解决方案更简单,更安全。

本文中使用的源代码可在GitHub中获得。