通过凭据管理服务免AKSK硬编码访问OBS服务
应用场景
在代码中将认证所需的 AK 和 SK 硬编码或以明文方式存储,会带来较大的安全风险。针对初始化Java OBS SDK客户端的场景,介绍一种通过动态获取托管于凭据管理服务(CSMS)的凭证,避免硬编码 AK/SK,从而安全地创建和配置 OBS 客户端的解决方案。
实现原理
本示例基于AK和SK已托管于凭据管理服务(CSMS)的场景,演示如何通过实现OBS SDK提供的 IObsCredentialsProvider接口来创建ObsClient实例。
CsmsObsCredentialsProvider 类通过从ECS服务器自动获取临时访问凭证,进一步访问CSMS获取托管的AK和SK,并将其用作OBS客户端的访问凭证。
前提条件
- IAM创建ECS云服务委托并获取临时凭证。具体操作请参见IAM创建ECS委托。
- 已将目标AK/SK存入凭据管理服务,可以通过以下两种方式:
- AK/SK按照存入凭据值的方式手动托管在凭据管理服务中,具体操作可参见存入凭据值。
- 通过函数工作流轮转IAM凭据的方式自动化托管AK/SK,具体操作可参见通过函数工作流轮转IAM凭证。
代码示例
- 获取CSMS SDK和OBS SDK的依赖声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<dependencies> <dependency> <groupId>com.huaweicloud</groupId> <artifactId>esdk-obs-java-bundle</artifactId> <!-- obs java SDK 版本号,以3.24.9为例,其他版本替换成相应的版本号--> <version>3.24.9</version> </dependency> <dependency> <groupId>com.huaweicloud.sdk</groupId> <artifactId>huaweicloud-sdk-csms</artifactId> <!-- csms java SDK 版本号,以3.1.94为例,其他版本替换成相应的版本号--> <version>3.1.94</version> </dependency> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <!-- json 版本号,以20231013为例,其他版本替换成相应的版本号--> <version>20231013</version> </dependency> </dependencies> |
- 创建并配置OBS客户端示例代码如下:
1 2 3 4 5 6 7 8 9 |
// Endpoint以北京四为例,其他地区请按实际情况填写。 String endPoint = "https://obs.cn-north-4.myhuaweicloud.com"; // Secret name为托管AKSK凭据的凭据名称 String secretName = "<Your CSMS_SECRET_NAME>"; // ECS场景从凭据管理服务获取认证信息,并初始化OBS客户端 ObsClient obsClient = new ObsClient(new CsmsObsCredentialsProvider(secretName), endPoint); // 使用访问OBS // 关闭obsClient obsClient.close(); |
- 实现OBS SDK中的IObsCredentialsProvider的示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
import static com.obs.services.internal.security.LimitedTimeSecurityKey.getUtcTime; import com.huaweicloud.sdk.core.auth.BasicCredentials; import com.huaweicloud.sdk.core.auth.ICredential; import com.huaweicloud.sdk.core.region.Region; import com.huaweicloud.sdk.csms.v1.CsmsClient; import com.huaweicloud.sdk.csms.v1.model.ShowSecretVersionRequest; import com.huaweicloud.sdk.csms.v1.model.ShowSecretVersionResponse; import com.obs.log.ILogger; import com.obs.log.LoggerBuilder; import com.obs.services.IObsCredentialsProvider; import com.obs.services.internal.security.EcsSecurityUtils; import com.obs.services.internal.security.LimitedTimeSecurityKey; import com.obs.services.internal.security.SecurityKey; import com.obs.services.internal.security.SecurityKeyBean; import com.obs.services.internal.utils.JSONChange; import com.obs.services.model.ISecurityKey; import kotlin.Pair; import org.json.JSONObject; import java.io.IOException; import java.util.Calendar; import java.util.Date; import java.util.concurrent.atomic.AtomicBoolean; public class CsmsObsCredentialsProvider implements IObsCredentialsProvider { private volatile LimitedTimeSecurityKey securityKey; private AtomicBoolean getNewKeyFlag = new AtomicBoolean(false); private static final ILogger ILOG = LoggerBuilder.getLogger(CsmsObsCredentialsProvider.class); // default is -1, not retry private int maxRetryTimes = -1; // csms secret name private final String secretName; // region code 以北京四为例,其他地区请按实际情况填写。 private final String regionCode = "cn-north-4"; // Csms endpoint以北京四为例,其他地区请按实际情况填写。 private final String csmsEndpoint = "https://kms.cn-north-4.myhuaweicloud.com"; // Iam endpoint以北京四为例,其他地区请按实际情况填写。 private final String iamEndpoint = "https://iam.cn-north-4.myhuaweicloud.com"; // Rotation time轮转刷新时间。注意:该值要小于凭据轮转时间,此处以15分钟为例。 private final String rotationTime = "15m"; public CsmsObsCredentialsProvider(String secretName) { this.maxRetryTimes = 3; this.secretName = secretName; } public CsmsObsCredentialsProvider(int maxRetryTimes, String secretName) { this.maxRetryTimes = maxRetryTimes; this.secretName = secretName; } @Override public void setSecurityKey(ISecurityKey securityKey) { throw new UnsupportedOperationException("CsmsObsCredentialsProvider class does not support this method"); } @Override public ISecurityKey getSecurityKey() { if (getNewKeyFlag.compareAndSet(false, true)) { try { if (securityKey == null || securityKey.willSoonExpire()) { refresh(false); } else if (securityKey.aboutToExpire()) { refresh(true); } } finally { getNewKeyFlag.set(false); } } else { if (ILOG.isDebugEnabled()) { ILOG.debug("some other thread is refreshing."); } } return securityKey; } /** * refresh * * @param ignoreException ignore exception */ private void refresh(boolean ignoreException) { int times = 0; do { try { securityKey = getNewSecurityKey(); break; } catch (IOException | RuntimeException e) { ILOG.warn("refresh new security key failed. times : " + times + "; maxRetryTimes is : " + maxRetryTimes + "; ignoreException : " + ignoreException, e); if (times >= this.maxRetryTimes) { ILOG.error("refresh new security key failed.", e); if (!ignoreException) { throw new IllegalArgumentException(e); } } } } while (times++ < maxRetryTimes); } private LimitedTimeSecurityKey getNewSecurityKey() throws IOException, IllegalArgumentException { String content = EcsSecurityUtils.getSecurityKeyInfoWithDetail(); SecurityKey securityInfo = (SecurityKey) JSONChange.jsonToObj(new SecurityKey(), content); if (securityInfo == null) { throw new IllegalArgumentException("Invalid securityKey : " + content); } SecurityKeyBean securityKeyBean = securityInfo.getBean(); ICredential auth = new BasicCredentials().withIamEndpoint(iamEndpoint) .withAk(securityKeyBean.getAccessKey()).withSk(securityKeyBean.getSecretKey()) .withSecurityToken(securityKeyBean.getSecurityToken()); Pair<String, String> akSkFromCSMS = getAKSKFromCSMS(auth, secretName); // 当前时间加上轮转时间为此访问凭据的过期时间。 Date expiryDate = getUtcTimeAfterMinuteAdd(rotationTime); StringBuilder strAccess = new StringBuilder(); String accessKey = akSkFromCSMS.getFirst(); int length = accessKey.length(); strAccess.append(accessKey.substring(0, length / 3)); strAccess.append("******"); strAccess.append(accessKey.substring(2 * length / 3, length - 1)); ILOG.warn("the AccessKey : " + strAccess.toString() + "will expiry at UTC time : " + expiryDate); return new LimitedTimeSecurityKey(akSkFromCSMS.getFirst(), akSkFromCSMS.getSecond(), null, expiryDate); } /** * Pair * first is access key id * second is access key secret */ private Pair<String, String> getAKSKFromCSMS(ICredential auth, String secretName) { ILOG.info("Get ak sk from csms secret name " + secretName + " begin."); CsmsClient client = CsmsClient.newBuilder().withCredential(auth) .withRegion(new Region(regionCode, csmsEndpoint)).build(); try { ShowSecretVersionRequest showSecretVersionRequest = new ShowSecretVersionRequest(); showSecretVersionRequest.withSecretName(secretName); showSecretVersionRequest.withVersionId("latest"); ShowSecretVersionResponse showSecretVersionResponse = client.showSecretVersion(showSecretVersionRequest); String secretString = showSecretVersionResponse.getVersion().getSecretString(); JSONObject secretJsonObject = new JSONObject(secretString); return new Pair<>((String) secretJsonObject.get("access_key_id"), (String) secretJsonObject.get("access_key_secret")); } catch (Exception e) { // 异常情况打印,此处可以替换成业务相关的异常,以打印日志和抛出RuntimeException为例。 ILOG.info("error message:" + e.getMessage()); throw new RuntimeException(e.getMessage()); } } private static Date getUtcTimeAfterMinuteAdd(String afterMinute) { Calendar calendar = Calendar.getInstance(); int offset = calendar.get(Calendar.ZONE_OFFSET); int dstOffset = calendar.get(Calendar.DST_OFFSET); // 使用Calendar类的add方法,将当前时间增加afterMinute分钟 calendar.add(Calendar.MINUTE, convertToMinute(afterMinute)); // 减去时区的偏移量和夏令时的偏移量,获取UTC时间 calendar.add(Calendar.MILLISECOND, -(offset + dstOffset)); return calendar.getTime(); } private static int convertToMinute(String period) { String unit = period.substring(period.length() - 1); int time = Integer.parseInt(period.substring(0, period.length() - 1)); switch (unit) { case "d": time = time * 24 * 60; break; case "h": time = time * 60; break; case "m": break; default: time = 0; break; } return time; } } |