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.read 和 resource.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-key
和 jwt.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 上查看。