
# 通过凭据管理服务免AK/SK硬编码访问OBS服务
#### 应用场景
在代码中将认证所需的 AK 和 SK 硬编码或以明文方式存储，会带来较大的安全风险。针对初始化Java OBS SDK客户端的场景，介绍一种通过动态获取托管于凭据管理服务（CSMS）的凭证，避免硬编码 AK/SK，从而安全地创建和配置 OBS 客户端的解决方案。
#### 实现原理
本示例基于AK和SK已托管于凭据管理服务（CSMS）的场景，演示如何通过实现OBS SDK提供的 IObsCredentialsProvider接口来创建ObsClient实例。
CsmsObsCredentialsProvider 类通过从ECS服务器自动获取临时访问凭证，进一步访问CSMS获取托管的AK和SK，并将其用作OBS客户端的访问凭证。
#### 前提条件
1. IAM创建ECS云服务委托并获取临时凭证。具体操作请参见[IAM创建ECS委托](https://support.huaweicloud.com/bestpractice-dew/dew_06_0022.html#section4)。
2. 已将目标AK/SK存入凭据管理服务，可以通过以下两种方式：
   1. AK/SK按照存入凭据值的方式手动托管在凭据管理服务中，具体操作可参见[存入凭据值](https://support.huaweicloud.com/usermanual-dew/dew_01_8882.html#section1)。
   
   2. 通过函数工作流轮转IAM凭据的方式自动化托管AK/SK，具体操作可参见[通过函数工作流轮转IAM凭证](https://support.huaweicloud.com/bestpractice-dew/dew_06_0035.html)。
    
 
#### 代码示例
1. 获取CSMS SDK和OBS SDK的依赖声明
```
<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>
```
2. 创建并配置OBS客户端示例代码如下：
```
// 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();
```
3. 实现OBS SDK中的IObsCredentialsProvider的示例代码如下：
```
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; 
    } 
}
```
