使用资源服务器保护API

资源服务器实际上就是API前的一个过滤器,它确保对需要授权的资源的请求中包括一个有效的访问令牌和所需scope。Spring Security提供了OAuth2资源服务器的实现,我们可以将其添加到现有的API中。这需要在项目的构建文件中添加如下的依赖:

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

也可以在创建项目时,通过在Initializr中选择OAuth2 Resource Server将资源服务器依赖添加进来。

依赖准备就绪之后,接下来,我们就是声明对“/ingredients”的POST请求需要“writeIngredients”scope,并且对“/ingredients”的DELETE请求需要“deleteIngredients”scope。如下所示是项目中SecurityConfig类的代码片段,展示了如何实现这一点:

@Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
       ...
        .antMatchers(HttpMethod.POST, "/api/ingredients")
            .hasAuthority("SCOPE_writeIngredients")
        .antMatchers(HttpMethod.DELETE, "/api//ingredients")
            .hasAuthority("SCOPE_deleteIngredients")
     ...
  }

对于每个端点,我们都可以调用“.hasAuthority()”方法来指定所需的scope。请注意,这里scope的前缀是“SCOPE_”,表明它们应该与请求这些资源时所携带的访问令牌中的OAuth 2 scope进行匹配。

在相同的配置类中,我们还需要启用资源服务器:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
     ...
      .and()
        .oauth2ResourceServer(oauth2 -> oauth2.jwt())
  ...
}

这里的oauth2ResourceServer()方法被赋予了一个lambda表达式,借助它,我们可以配置资源服务器。在这里,我们只启用了JWT令牌(与之相对的是opaque令牌),所以资源服务器会探查令牌的内容来确定它包含哪些安全要求(claim)。具体来讲,对于我们保护的两个端点,它将会查看令牌中是否包含“writeIngredients”或“deleteIngredients”scope。

不过,它不会相信令牌中表面上的值。为了确信令牌是由受信任的授权服务器代表用户的身份创建的,它会使用一个公钥校验令牌的签名,这个公钥是与创建令牌签名时所使用私钥配对的。不过,我们需要配置资源服务器从何处获取这个公钥。如下的属性会指定授权服务器上JWK集的URL,资源服务器会从这里获取公钥:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://localhost:9000/oauth2/jwks

现在,我们的资源服务器已经准备就绪了!构建Taco Cloud应用并将其启动,使用curl进行如下的尝试:

$ curl localhost:8080/ingredients \
        -H"Content-type: application/json" \
        -d'{"id":"CRKT", "name":"Legless Crickets", "type":"PROTEIN"}'

请求将会失败,并且带有HTTP 401响应码。这是因为我们配置了该端点需要“writeIngredients”scope,但是在请求中,我们并没有提供包含该scope的合法访问令牌。

为了让请求成功并将这个新的配料添加进去,我们需要使用8.2节介绍的流程获取一个访问令牌,确保浏览器在重定向到授权服务器时请求获取“writeIngredients”和“deleteIngredients”的许可。然后,使用curl命令时,在“Authorization”头信息中提供访问令牌(请将“$token”替换为实际的访问令牌):

$ curl localhost:8080/ingredients \
    -H"Content-type: application/json" \
    -d'{"id":"SHMP", "name":"Coconut Shrimp", "type":"PROTEIN"}' \
    -H"Authorization: Bearer $token"

这一次,新的配料就能创建了。可以使用curl或HTTP客户端对“/ingredients”端点发送GET请求进行校验:

$ curl localhost:8080/ingredients
[
    {
        "id": "FLTO",
        "name": "Flour Tortilla",
        "type": "WRAP"
    },

    ...

    {
        "id": "SHMP",
        "name": "Coconut Shrimp",
        "type": "PROTEIN"
    }
]

在“/ingredients”端点返回的配料列表的结尾处,可以看到Coconut Shrimp。我们成功了!

回想一下,访问令牌会在5分钟后过期。令牌过期之后,请求将会再次返回HTTP 401响应。但是,借助与访问令牌一起得到的刷新令牌,可以向授权服务器发送请求从而获取新的访问令牌(请用实际的刷新令牌替换“$refreshToken”),如下所示:

$ curl localhost:9000/oauth2/token \
    -H"Content-type: application/x-www-form-urlencoded" \
    -d"grant_type = refresh_token&refresh_token = $refreshToken" \
    -u taco-admin-client:secret

借助这个新创建的访问令牌,我们可以按需创建新的配料了。

现在,我们已经可以确保 “/ingredients”端点的安全性。最好使用同样的技术来保护API中其他潜在的敏感端点。例如,“/orders”端点可能不应对任何类型的请求开放,即便是HTTP GET请求,因为这能让黑客轻松地获取客户的信息。我将决定权交给你自己,你可以自行决定如何保护“/orders”端点和API的其他组成部分。

使用curl来管理Taco Cloud应用程序,对于熟悉和了解OAuth 2令牌如何允许我们访问资源,是一种很好的方式。但是,最终我们想要有一个真正的可以管理配料的客户端应用。接下来,我们将关注点转移到创建支持OAuth的客户端,以获取访问令牌并对API发送请求上。