1. 概述

Cloud Foundry User Account and Authentication (CF UAA) 是一个身份管理和授权服务,更具体地说,它是一个 OAuth 2.0 提供商,用于为客户端应用进行身份认证并发放访问令牌。

在本文中,我们将讲解如何搭建一个 CF UAA 服务器,并演示如何使用它来保护资源服务器应用。在此之前,我们先来明确 UAA 在 OAuth 2.0 授权框架中的角色。

2. Cloud Foundry UAA 与 OAuth 2.0

OAuth 2.0 规范定义了四个参与者:资源拥有者(Resource Owner)、资源服务器(Resource Server)、客户端(Client)和授权服务器(Authorization Server)。

CF UAA 的角色就是充当这个授权服务器。这意味着它主要负责:

  • 为客户端应用颁发访问令牌
  • 供资源服务器验证这些令牌

为了实现这些角色之间的交互,我们需要搭建一个 UAA 服务器,并实现两个应用:一个作为客户端,另一个作为资源服务器。

我们使用 authorization_code 授权模式 与客户端交互,并使用 Bearer Token 与资源服务器通信。为了更安全和高效的握手,我们使用 签名的 JWT(JSON Web Token) 作为访问令牌。

3. 搭建 UAA 服务器

我们首先安装 UAA 并填充一些演示数据。安装完成后,注册一个名为 webappclient 的客户端应用,并创建一个拥有 resource.readresource.write 权限的用户 appuser

3.1. 安装 UAA

UAA 是一个 Java Web 应用,可以部署在任意兼容的 Servlet 容器中。本文使用 Tomcat。

下载 UAA 的 war 包并部署到 Tomcat:

wget -O $CATALINA_HOME/webapps/uaa.war \
  https://search.maven.org/remotecontent?filepath=org/cloudfoundry/identity/cloudfoundry-identity-uaa/4.27.0/cloudfoundry-identity-uaa-4.27.0.war

3.2. 配置文件设置

默认情况下,UAA 从 classpath 中读取 uaa.yml 配置文件。我们可以指定一个自定义路径:

export UAA_CONFIG_PATH=~/.uaa

然后下载默认配置文件并保存:

wget -qO- https://raw.githubusercontent.com/cloudfoundry/uaa/4.27.0/uaa/src/main/resources/required_configuration.yml \
  > $UAA_CONFIG_PATH/uaa.yml

⚠️ 删除最后三行,我们稍后会替换它们。

3.3. 配置数据源

本例中我们使用 HSQLDB 作为 UAA 的数据源:

export SPRING_PROFILES="default,hsqldb"

你也可以在 uaa.yml 中通过 spring.profiles 属性配置。

3.4. 配置 JWS 密钥对

由于我们使用 JWT,UAA 需要一个私钥来签名 JWT,客户端和资源服务器则使用公钥验证签名。

使用 OpenSSL 生成密钥:

openssl genrsa -out signingkey.pem 2048
openssl rsa -in signingkey.pem -pubout -out verificationkey.pem

导出为环境变量:

export JWT_TOKEN_SIGNING_KEY=$(cat signingkey.pem)
export JWT_TOKEN_VERIFICATION_KEY=$(cat verificationkey.pem)

你也可以在 uaa.yml 中通过 jwt.token.signing-keyjwt.token.verification-key 配置。

3.5. 启动 UAA

运行 Tomcat:

$CATALINA_HOME/bin/catalina.sh run

UAA 服务器将在 http://localhost:8080/uaa 上运行。

访问 http://localhost:8080/uaa/info 可查看启动信息。

3.6. 安装 UAA 命令行客户端

CF UAA CLI 是管理 UAA 的主要工具,需先安装 Ruby:

sudo apt install rubygems
gem install cf-uaac

配置 CLI 指向本地 UAA 实例:

uaac target http://localhost:8080/uaa

当然,你也可以直接使用 UAA 的 HTTP API。

3.7. 使用 UAAC 添加客户端和用户

使用 UAAC 添加演示数据:

✅ 登录 admin 账户:

uaac token client get admin -s adminsecret

✅ 注册客户端:

uaac client add webappclient -s webappclientsecret \
--name WebAppClient \
--scope resource.read,resource.write,openid,profile,email,address,phone \
--authorized_grant_types authorization_code,refresh_token,client_credentials,password \
--authorities uaa.resource \
--redirect_uri http://localhost:8081/login/oauth2/code/uaa

✅ 添加用户:

uaac user add appuser -p appusersecret --emails [email protected]

✅ 添加权限组:

uaac group add resource.read
uaac group add resource.write

✅ 给用户分配权限:

uaac member add resource.read appuser
uaac member add resource.write appuser

现在我们已经完成了 UAA 的基本配置,包括客户端、用户和权限的设置。

4. OAuth 2.0 客户端应用

接下来,我们使用 Spring Boot 创建一个 OAuth 2.0 客户端应用。

4.1. 初始化项目

访问 Spring Initializr,选择如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

配置客户端信息:

# 客户端注册信息
spring.security.oauth2.client.registration.uaa.client-id=webappclient
spring.security.oauth2.client.registration.uaa.client-secret=webappclientsecret
spring.security.oauth2.client.registration.uaa.scope=resource.read,resource.write,openid,profile

# UAA 提供商信息
spring.security.oauth2.client.provider.uaa.issuer-uri=http://localhost:8080/uaa/oauth/token

# 服务器端口
server.port=8081

4.2. 登录流程

访问 /login,将跳转到 UAA 登录页面。使用 appuser/appusersecret 登录后,会看到授权页面。

选择除 resource.write 以外的所有权限,提交后将获得一个包含所选 scope 的 JWT。

访问首页将显示访问令牌,使用 JWT Debugger 解码,可以看到 scope 列表:

{
  "jti": "f228d8d7486942089ff7b892c796d3ac",
  "sub": "0e6101d8-d14b-49c5-8c33-fc12d8d1cc7d",
  "scope": [
    "resource.read",
    "openid",
    "profile"
  ],
  "client_id": "webappclient"
}

现在我们已经有了客户端应用,下一步是搭建资源服务器。

5. 资源服务器

资源服务器用于托管用户受保护的资源,并通过 UAA 进行认证。

5.1. 初始化项目

再次使用 Spring Initializr 创建 Spring Boot 项目,选择以下依赖:

<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-web</artifactId>
</dependency>

配置资源服务器连接 UAA:

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/uaa/oauth/token
server.port=8082

5.2. 保护 API 接口

添加两个受保护的接口:

@RestController
public class ResourceController {

    @GetMapping("/read")
    public String read(Principal principal) {
        return "Hello read: " + principal.getName();
    }

    @GetMapping("/write")
    public String write(Principal principal) {
        return "Hello write: " + principal.getName();
    }
}

配置 Spring Security 保护策略:

@EnableWebSecurity
public class CFUAAOAuth2ResourceServerSecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
            .requestMatchers("/read/**")
            .hasAuthority("SCOPE_resource.read")
            .requestMatchers("/write/**")
            .hasAuthority("SCOPE_resource.write")
            .anyRequest()
            .authenticated())
            .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }
}

⚠️ 注意:OAuth2 的 scope 在 Spring Security 中会被自动加上 SCOPE_ 前缀。

5.3. 从客户端调用资源接口

客户端通过 RestTemplate 调用资源服务器接口:

private String callResourceServer(OAuth2AuthenticationToken authenticationToken, String url) {
    OAuth2AuthorizedClient oAuth2AuthorizedClient = this.authorizedClientService
        .loadAuthorizedClient(authenticationToken.getAuthorizedClientRegistrationId(), authenticationToken.getName());
    OAuth2AccessToken oAuth2AccessToken = oAuth2AuthorizedClient.getAccessToken();

    HttpHeaders headers = new HttpHeaders();
    headers.add("Authorization", "Bearer " + oAuth2AccessToken.getTokenValue());

    ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), String.class);
    return response.getBody();
}

添加两个接口调用:

@GetMapping("/read")
public String read(OAuth2AuthenticationToken authenticationToken) {
    String url = "http://localhost:8082/read";
    return callResourceServer(authenticationToken, url);
}

@GetMapping("/write")
public String write(OAuth2AuthenticationToken authenticationToken) {
    String url = "http://localhost:8082/write";
    return callResourceServer(authenticationToken, url);
}

调用 /read 成功,但 /write 返回 403,说明用户没有 resource.write 权限。

6. 总结

我们介绍了 CF UAA 的基本概念及其在 OAuth 2.0 框架中的角色,搭建了 UAA 服务器,并注册了客户端和用户。接着创建了一个 OAuth 2.0 客户端应用和一个资源服务器应用,实现了完整的认证和授权流程。

完整示例代码可在 GitHub 上查看。


原始标题:A Quick Guide To Using Cloud Foundry UAA