基於阿里巴巴技術體系的 API 鑒權組件設計與實現,涵蓋動態配置、MD5 簽名驗證、時間戳防重放、Pipeline 攔截器鏈整合等企業級安全實踐。
Maven 插件在打包階段根據環境變量替換模板變量,實現「一次構建,多環境部署」。
<plugin>
<groupId>com.alibaba.citrus.tool</groupId>
<artifactId>autoconfig-maven-plugin</artifactId>
<version>1.2</version>
<configuration>
<includeDescriptorPatterns>
<includeDescriptorPattern>autoconf/auto-config.xml</includeDescriptorPattern>
</includeDescriptorPatterns>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals><goal>autoconfig</goal></goals>
</execution>
</executions>
</plugin>
核心思想:配置模板 + 環境變量 → 具體配置文件。
DigestUtils.md5Hex(str) (Apache Commons Codec)<bean id="apiValve" class="com.alibaba.tboss.common.services.login.ApiAuthValve" init-method="initial">
<property name="includeStr">
<list><value><![CDATA[^/tboss/web/api/.*\.json$]]></value></list>
</property>
<property name="excludeStr"><list></list></property>
</bean>
includeStr:需鑒權的 URL 正則excludeStr:模塊內跳過鑒權的 URLinit-method="initial":容器啟動時編譯正則模式<valve class="com.alibaba.tboss.common.services.login.ApiAuthValve" />
位於 checkCsrfToken 之前,確保 API 請求優先通過鑒權。
public class ApiAuthValve extends AbstractValve {
private static final Logger logger = LoggerFactory.getLogger(ApiAuthValve.class);
@Resource ApiUserBo apiUserBo;
@Autowired HttpServletRequest request;
@Autowired HttpServletResponse response;
protected static String[] includeStr;
protected static List includes = new ArrayList<>();
protected static String[] excludeStr;
protected static List excludes = new ArrayList<>();
public void initial() {
if (includeStr != null) {
for (String s : includeStr) includes.add(Pattern.compile(s.toLowerCase()));
}
if (excludeStr != null) {
for (String s : excludeStr) excludes.add(Pattern.compile(s.toLowerCase()));
}
}
@Override
public void invoke(PipelineContext pipelineContext) throws Exception {
String uri = request.getRequestURI();
try {
if (isInControl(uri)) {
String apiName = request.getParameter(API_AUTH_NAME);
String timestamp = request.getParameter(API_AUTH_TIMESTAMP);
String signature = request.getParameter(API_AUTH_SIGNATURE);
apiUserBo.checkAuthorization(apiName, timestamp, signature, uri);
}
} catch (Exception e) {
logger.error(uri + " fail - " + e.getMessage(), e);
// 統一錯誤響應
ErrorContext ctx = new DefaultErrorContext();
ctx.addError(ErrorItem.create("rpc_500", "500",
"API auth fail: " + e.getMessage()));
Object result = new DefaultResultGenerator.GenericWebRPCResult(
"API auth fail", null, ctx);
response.getWriter().print(JSON.toJSONString(result));
return; // 終止後續 Valve
}
pipelineContext.invokeNext(); // 繼續鏈
}
private boolean isInControl(String uri) {
boolean match = false;
for (Pattern p : includes) {
if (p.matcher(uri.toLowerCase()).matches()) { match = true; break; }
}
if (match) {
for (Pattern p : excludes) {
if (p.matcher(uri.toLowerCase()).matches()) { match = false; break; }
}
}
return match;
}
}
public void checkAuthorization(String apiName, String timestamp,
String signature, String uri) {
// 1. 參數非空
if (StringUtils.isAnyBlank(apiName, timestamp, signature))
throw new ApiAuthException("apiName, timestamp, signature is missing");
// 2. 時間戳格式 & 有效期 (15分鐘)
Date gmtRequest;
try {
gmtRequest = DateUtils.parseDate(timestamp, API_AUTH_TIMEFORMAT);
} catch (ParseException e) {
throw new ApiAuthException("Unsupported timestamp format");
}
if (Math.abs(System.currentTimeMillis() - gmtRequest.getTime()) >= 15 * 60 * 1000)
throw new ApiAuthException("Request has been expired");
// 3. 用戶存在性 & 權限
ApiUser user = getApiUserByName(apiName);
if (user == null) throw new ApiAuthException("Api user not exist");
if (!isAuthorized(user, uri)) throw new ApiAuthException("No permission");
// 4. 簽名驗證
String realCode = decode(user.getCode()); // 解密存儲的密鑰
String signBase = String.format("%s%s%s", apiName, timestamp, realCode);
if (!signature.equals(DigestUtils.md5Hex(signBase)))
throw new ApiAuthException("Signature mismatch");
}
| 防禦維度 | 實現手段 |
|---|---|
| 身份認證 | apiName + 密鑰對稱加密存儲 |
| 防重放攻擊 | timestamp 有效期 15 分鐘 + 單次使用檢查(可擴展) |
| 完整性校驗 | MD5(apiName + timestamp + secret) 簽名 |
| 授權控制 | URL 級別白名單 + 用戶權限矩陣 |
| 靈活配置 | 正則 include/exclude + Autoconfig 多環境 |
// 客戶端生成簽名
String apiName = "partner_001";
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
String secret = "shared_secret_from_platform";
String signBase = apiName + timestamp + secret;
String signature = DigestUtils.md5Hex(signBase);
// HTTP 請求參數
GET /tboss/web/api/query.json?apiName=partner_001×tamp=2024-01-15%2010:30:00&signature=abc123...