跳到主要内容

ASAP (Atlassian) 认证介绍

ASAP (Atlassian Security as a Priority) 是Atlassian开发的一种安全认证协议,用于服务与服务之间的通信。它基于JWT (JSON Web Token),提供了一种安全、可扩展的方式来验证服务身份。ASAP主要用于Atlassian产品生态系统中的微服务架构,但也可以在任何需要安全服务间通信的场景中使用。

ASAP 的工作原理

ASAP基于JWT和公钥/私钥加密技术,工作流程如下:

  1. 服务注册:每个服务都有一个唯一的身份标识符(Issuer)和一对公钥/私钥。
  2. 令牌生成:调用方服务使用其私钥签署JWT令牌,其中包含调用方的身份、目标服务的身份、令牌有效期等信息。
  3. 令牌传递:调用方将生成的JWT令牌作为Authorization头的一部分发送给被调用方服务。
  4. 令牌验证:被调用方服务使用调用方的公钥验证令牌的签名和声明(如发行者、接收者、过期时间等)。
  5. 授权决策:如果令牌验证成功,被调用方服务根据其访问控制策略决定是否允许请求。

ASAP 令牌的结构

ASAP令牌是一个标准的JWT,包含以下主要声明:

  1. iss (Issuer):令牌发行者的身份标识符,通常是服务的名称或ID。
  2. sub (Subject):可选,通常设置为与iss相同的值。
  3. aud (Audience):令牌接收者的身份标识符,可以是单个字符串或字符串数组。
  4. exp (Expiration Time):令牌的过期时间,以Unix时间戳表示。
  5. iat (Issued At):令牌的签发时间,以Unix时间戳表示。
  6. jti (JWT ID):可选,令牌的唯一标识符,用于防止重放攻击。

如何使用 ASAP 认证

设置ASAP认证

  1. 生成密钥对

    首先,为每个服务生成RSA密钥对:

    # 生成私钥
    openssl genrsa -out private.pem 2048

    # 从私钥生成公钥
    openssl rsa -in private.pem -pubout -out public.pem
  2. 分发公钥

    将每个服务的公钥分发给需要验证其令牌的服务。这可以通过密钥服务器、配置管理系统或其他安全的方式完成。

  3. 配置服务身份

    为每个服务配置唯一的身份标识符(Issuer),这通常是服务的名称或ID。

实施ASAP认证

以下是使用ASAP认证的基本步骤:

生成ASAP令牌(调用方)

  1. 创建JWT令牌头部和载荷
  2. 使用私钥对令牌进行签名
  3. 将令牌添加到HTTP请求的Authorization头中

验证ASAP令牌(被调用方)

  1. 从请求的Authorization头中提取JWT令牌
  2. 根据令牌中的Issuer获取对应的公钥
  3. 验证令牌的签名和声明
  4. 根据访问控制策略决定是否允许请求

使用JavaScript实现ASAP认证

下面是使用Node.js实现ASAP认证的示例:

调用方 - 生成ASAP令牌

const fs = require('fs');
const jwt = require('jsonwebtoken');
const axios = require('axios');

// 加载私钥
const privateKey = fs.readFileSync('private.pem');

// 生成ASAP令牌
function generateASAPToken(issuer, audience) {
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: issuer, // 发行者(调用方服务的ID)
sub: issuer, // 主题(通常与发行者相同)
aud: audience, // 接收者(目标服务的ID)
exp: now + 60 * 5, // 过期时间(5分钟后)
iat: now, // 签发时间
jti: `${issuer}-${now}-${Math.random().toString(36).substr(2, 10)}` // 唯一ID
};

const options = {
algorithm: 'RS256', // 使用RSA签名算法
header: {
typ: 'JWT',
kid: issuer // 密钥ID,通常是发行者的ID
}
};

return jwt.sign(payload, privateKey, options);
}

// 发送带ASAP认证的请求
async function makeASAPRequest(method, url, data = null) {
// 配置
const issuer = 'service-a'; // 调用方服务ID
const audience = 'service-b'; // 目标服务ID

// 生成ASAP令牌
const token = generateASAPToken(issuer, audience);

// 发送请求
try {
const response = await axios({
method,
url,
data,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});

return response.data;
} catch (error) {
console.error('请求失败:', error.message);
if (error.response) {
console.error('响应状态:', error.response.status);
console.error('响应数据:', error.response.data);
}
throw error;
}
}

// 使用示例
async function example() {
try {
const result = await makeASAPRequest(
'GET',
'https://service-b.example.com/api/resource'
);
console.log('响应数据:', result);
} catch (error) {
console.error('调用失败:', error);
}
}

example();

被调用方 - 验证ASAP令牌

const fs = require('fs');
const jwt = require('jsonwebtoken');
const express = require('express');

const app = express();
app.use(express.json());

// 模拟公钥存储(在实际应用中,这可能来自数据库或密钥服务器)
const publicKeys = {
'service-a': fs.readFileSync('service-a-public.pem')
};

// ASAP认证中间件
function asapAuth(req, res, next) {
// 从Authorization头中提取令牌
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '缺少认证令牌' });
}

const token = authHeader.substring(7); // 移除 "Bearer " 前缀

try {
// 解码令牌头部(不验证),获取发行者
const decoded = jwt.decode(token, { complete: true });
if (!decoded || !decoded.header.kid) {
return res.status(401).json({ error: '无效的令牌格式' });
}

const issuer = decoded.header.kid;

// 获取发行者的公钥
const publicKey = publicKeys[issuer];
if (!publicKey) {
return res.status(401).json({ error: '未知的发行者' });
}

// 验证令牌
const verified = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
audience: 'service-b', // 我们的服务ID
clockTolerance: 30 // 允许30秒的时钟偏差
});

// 将验证结果附加到请求对象
req.auth = {
issuer: verified.iss,
subject: verified.sub,
jti: verified.jti
};

// 继续处理请求
next();
} catch (error) {
console.error('令牌验证失败:', error.message);
res.status(401).json({ error: `认证失败: ${error.message}` });
}
}

// 使用ASAP认证中间件保护路由
app.get('/api/resource', asapAuth, (req, res) => {
res.json({
message: `Hello, ${req.auth.issuer}!`,
data: {
resource_id: '12345',
name: 'Example Resource'
}
});
});

// 启动服务器
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});

使用Java实现ASAP认证

下面是使用Java实现ASAP认证的示例:

调用方 - 生成ASAP令牌

import com.atlassian.asap.api.client.AsapClient;
import com.atlassian.asap.api.client.AsapClientBuilder;
import com.atlassian.asap.api.client.AsapClientConfiguration;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Duration;
import java.util.Base64;

public class AsapExample {

public static void main(String[] args) throws Exception {
// 配置
String issuer = "service-a";
String audience = "service-b";
String keyId = issuer; // 通常使用发行者作为密钥ID
URI targetUri = URI.create("https://service-b.example.com/api/resource");

// 加载私钥
PrivateKey privateKey = loadPrivateKey("private.pem");

// 创建ASAP客户端配置
AsapClientConfiguration configuration = new AsapClientConfiguration.Builder()
.issuer(issuer)
.keyId(keyId)
.privateKey(privateKey)
.defaultAudience(audience)
.defaultTokenExpiry(Duration.ofMinutes(5))
.build();

// 创建ASAP客户端
AsapClient asapClient = new AsapClientBuilder()
.configuration(configuration)
.build();

// 创建并发送请求
String response = asapClient.executeRequest(targetUri)
.get(String.class);

System.out.println("响应数据: " + response);
}

// 从PEM文件加载私钥
private static PrivateKey loadPrivateKey(String filename) throws Exception {
String key = new String(Files.readAllBytes(Paths.get(filename)));

// 移除PEM头尾和换行符
key = key.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");

// 解码Base64
byte[] encoded = Base64.getDecoder().decode(key);

// 创建PKCS8密钥规范
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);

// 生成私钥
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
}

被调用方 - 验证ASAP令牌

import com.atlassian.asap.api.jwk.AsapJwkKeyLoader;
import com.atlassian.asap.api.jwk.AsapPublicKeyLoader;
import com.atlassian.asap.api.Jwt;
import com.atlassian.asap.api.JwtBuilder;
import com.atlassian.asap.api.middleware.AsapRequestMatcher;
import com.atlassian.asap.api.middleware.AsapValidationFilter;
import com.atlassian.asap.api.validator.JwtValidator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import java.security.PublicKey;
import java.util.HashMap;
import java.util.Map;

@SpringBootApplication
public class AsapServiceApplication {

public static void main(String[] args) {
SpringApplication.run(AsapServiceApplication.class, args);
}

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 添加ASAP验证过滤器
http.addFilterBefore(
asapValidationFilter(),
BasicAuthenticationFilter.class
)
.csrf().disable();
}

@Bean
public AsapValidationFilter asapValidationFilter() {
// 配置
String audience = "service-b"; // 我们的服务ID

// 创建公钥加载器(使用内存中的公钥)
AsapPublicKeyLoader publicKeyLoader = createPublicKeyLoader();

// 创建JWT验证器
JwtValidator jwtValidator = new JwtValidator.Builder()
.audience(audience)
.allowedClockSkewInSeconds(30)
.build();

// 创建请求匹配器(哪些请求需要ASAP认证)
AsapRequestMatcher requestMatcher = request -> true; // 所有请求

// 创建ASAP验证过滤器
return new AsapValidationFilter.Builder()
.requestMatcher(requestMatcher)
.keyLoader(publicKeyLoader)
.validator(jwtValidator)
.build();
}

// 创建公钥加载器(示例使用内存中的公钥)
private AsapPublicKeyLoader createPublicKeyLoader() {
// 在实际应用中,这可能从文件、数据库或密钥服务器加载
Map<String, PublicKey> keyMap = new HashMap<>();
try {
// 添加已知服务的公钥
keyMap.put("service-a", loadPublicKey("service-a-public.pem"));

// 创建一个简单的公钥加载器
return keyId -> keyMap.getOrDefault(keyId, null);
} catch (Exception e) {
throw new RuntimeException("无法加载公钥", e);
}
}

// 从PEM文件加载公钥(简化版,实际实现可能更复杂)
private PublicKey loadPublicKey(String filename) {
// 实现略...
return null; // 实际实现应返回加载的公钥
}
}
}

ASAP 的优势

  • 安全性:基于非对称加密,私钥只需要存储在发行方服务,公钥可以安全分发。
  • 自包含:令牌包含所有必要的认证和授权信息,减少数据库查询。
  • 灵活性:可以适应各种服务间通信场景,并支持微服务架构。
  • 性能:不需要中央认证服务器参与每次请求,减少网络延迟。
  • 审计能力:每个令牌都有唯一标识符,可以用于跟踪和审计。
  • 防重放:通过设置较短的过期时间和唯一标识符,可以防止重放攻击。
  • 开放标准:基于JWT等开放标准,易于集成和实现。

安全建议

在使用ASAP认证时,应注意以下安全措施:

  • 安全存储私钥:私钥应安全存储,避免泄露。考虑使用密钥管理服务或硬件安全模块(HSM)。
  • 设置合理的过期时间:令牌的有效期应尽可能短,通常为几分钟。
  • 验证所有必要的声明:接收方应验证令牌的所有必要声明,包括发行者、接收者、过期时间等。
  • 使用强加密算法:使用推荐的加密算法,如RS256或更强的算法。
  • 定期轮换密钥:定期更换密钥对,减少密钥泄露的风险。
  • 使用公钥分发机制:使用安全的机制分发和更新公钥,如密钥服务器或PKI基础设施。
  • 防止重放攻击:考虑实现令牌黑名单或使用nonce机制,防止重放攻击。
  • 使用HTTPS:所有服务间通信应通过HTTPS进行,防止中间人攻击。

常见问题

  1. ASAP与JWT-Bearer有什么区别?

    ASAP是基于JWT的认证协议,但专门针对服务间通信场景进行了优化。它定义了一套特定的声明和验证规则,并专注于使用公钥/私钥加密而非共享密钥。JWT-Bearer是一种更通用的令牌传递方式,可以使用多种不同的签名算法。

  2. 如何在多个环境(开发、测试、生产)中管理ASAP密钥?

    最佳实践是为每个环境使用不同的密钥对,并使用环境特定的发行者标识符(如"service-a-dev"、"service-a-prod")。密钥管理应与环境分离,可以使用专用的密钥管理服务或配置管理系统。

  3. 如何处理密钥轮换?

    密钥轮换应该是无缝的,可以通过以下步骤实现:

    • 生成新的密钥对
    • 分发新的公钥,但保留旧公钥
    • 开始使用新私钥签发令牌
    • 设置过渡期,在此期间接收方同时接受新旧密钥签名的令牌
    • 过渡期结束后,移除旧公钥
  4. ASAP是否适用于用户认证?

    ASAP主要设计用于服务间通信,而非用户认证。对于用户认证,应考虑使用OAuth 2.0、OpenID Connect或SAML等专门的用户认证协议。