인증 토큰 만들기

인증 절차에 사용할 인증서 및 개인키는 Salesforce Connected App 설정 설명에서 사용된 인증서(company.crt)에 대응하는 만든 키스토어 또는 개인키를 사용합니다. company.jks, company.key 파일을 프로젝트의 루트 폴더에 복사를 해둡니다. 실제로는 jks, key 둘 중에 한가지만 필요합니다. 뒤에서 설명하는 인증서/개인키 사용 방법에 따라 선택을 하시면 됩니다. Client Key와 Connected App에 접속할 수 있는 사용자 Id도 준비를 해둡니다.

Class 만들기

인증 받기 코드에 사용할 Java Class를 생성합니다.

이 설명에서는 Maven 프로젝트를 기준으로 설명을 합니다.

  1. JAVA PROJECTSApp 오른쪽의 '+' 기호를 클릭합니다.

Create Class 1

  1. Select resource type to create. 목록에서 Class를 선택합니다.

Create Class 2

  1. 생성하려는 클래스 이름을 입력하고 엔터키를 누릅니다. 여기서는 Auth 클래스를 생성합니다.

Create Class 3

  1. Auth.java Java 클래스 파일이 생성됩니다. 같은 방법으로 ConnectionInfo 클래스를 생성합니다.

Create Class 4

ConnectionInfo.java 파일을 열고 아래와 같이 내용을 추가한 후에 저장을 합니다. 데이터 조작을 위한 API를 사용할때 필요한 instanceUrlaccessToken을 처리하기 위한 클래스입니다.

``` java
public class ConnectionInfo {

    private String instanceUrl;
    private String accessToken;

    public void setInstanceUrl(String param){
        this.instanceUrl = param;
    }

    public String getInstanceUrl(){
        return this.instanceUrl;
    }

    public void setAccessToken(String param){
        this.accessToken = param;
    }

    public String getAccessToken(){
        return this.accessToken;
    }

}
```

코드 구성

인증을 받기 위하여 Salesforce Connnected App에 전송하는 토큰은 다음과 같이 .으로 연결된 3부분으로 이루어져 있습니다.

[JWT 헤더].[인증 요청정보].[개인키로 암호화된 헤더+요청정보] (JWT Header.Claim Payload.Header + Signed Payload)

순서대로 만들어 보도록 하겠습니다.

  1. Auth.java 클래스 윗 부분에 아래의 내용을 추가를 합니다.

    package com.example;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.UnsupportedEncodingException;
    import java.nio.charset.Charset;
    import java.nio.file.Files;
    import java.security.KeyFactory;
    import java.security.KeyStore;
    import java.security.KeyStoreException;
    import java.security.NoSuchAlgorithmException;
    import java.security.PrivateKey;
    import java.security.Signature;
    import java.security.UnrecoverableKeyException;
    import java.security.cert.CertificateException;
    import java.security.spec.PKCS8EncodedKeySpec;
    import java.text.MessageFormat;
    import java.util.ArrayList;
    import java.util.List;
    
    import org.apache.commons.codec.binary.Base64;
    import org.apache.http.HttpResponse;
    import org.apache.http.HttpStatus;
    import org.apache.http.NameValuePair;
    import org.apache.http.client.ClientProtocolException;
    import org.apache.http.client.HttpClient;
    import org.apache.http.client.entity.UrlEncodedFormEntity;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.impl.client.HttpClientBuilder;
    import org.apache.http.message.BasicNameValuePair;
    import org.apache.http.util.EntityUtils;
    
    import com.google.gson.JsonElement;
    import com.google.gson.JsonObject;
    import com.google.gson.JsonParseException;
    import com.google.gson.JsonParser;
    
    public class Auth {
    
    }

    사용하지 않는 패키지는 나중에 제거할 수 있습니다.

  2. 가장 먼저 개인키(PrivateKey)를 프로그램에 적재하여야 합니다. 개인키를 적재하는 방법은 아래의 세가지 방법이 있으니 상황에 맞는 방법을 사용하시면 됩니다.

    • JKS 키스토어의 개인키를 적재하는 방법

      아래의 메소드 코드를 Auth 클래스에 추가합니다.

      public static PrivateKey getPrivateKeyFromJKS(String filePath, String alias, String password) throws Exception {
          PrivateKey privateKey = null;
          try {
              //Load the private key from a jks keystore
              KeyStore keystore = KeyStore.getInstance("JKS");
              keystore.load(new FileInputStream(filePath), password.toCharArray());
      
              privateKey = (PrivateKey) keystore.getKey(alias, password.toCharArray());
          } catch (Exception e) {
              System.out.println(e);
          }
      
          return privateKey;
      }     

      App.java 파일의 main 메소드에 아래와 같이 호출 코드를 추가하고 'Run'을 실행해 봅니다.

      public static void main( String[] args ) throws Exception {
          try {
              PrivateKey privateKey = Auth.getPrivateKeyFromJKS("./company.jks", "company", "password");
      
              if(privateKey != null) {
                  System.out.println("Private Key loaded...");
              }
      
          } catch(Exception e) {
              System.out.println(e);
          }
      }
      • company.jks : jks 키스토어 파일입니다.
      • company : jks 키스토어를 생성할 때 -destalias로 지정한 alias입니다.
      • password : jks 키스토어를 생성할 떄 지정한 암호입니다.
    • 개인키 파일의 개인키를 스트링으로 적재하는 방법

      아래의 메소드 코드를 Auth 클래스에 추가합니다.

      public static PrivateKey getPrivateKeyFromString(String pkcs8PrivateKeyString) throws Exception {
          PrivateKey privateKey = null;
          try {            
              KeyFactory factory;
              factory = KeyFactory.getInstance("RSA");
              byte[] encodedPK = Base64.decodeBase64(pkcs8PrivateKeyString);
              PKCS8EncodedKeySpec keySpecPv = new PKCS8EncodedKeySpec(encodedPK);
              privateKey = factory.generatePrivate(keySpecPv);
          } catch (Exception e) {
              System.out.println(e);
          }
      
          return privateKey;
      }

      App.java 파일의 main 메소드에 아래와 같이 호출 코드를 추가하고 'Run'을 실행해 봅니다.

      company.key 파일을 노트패드에서 열고, "-----BEGIN PRIVATE KEY-----" 행과 "-----END PRIVATE KEY-----" 행 사이의 문자열을 개행 문자를 제외하고 pkcs8PrivateKeyString 변수에 지정을 합니다.

      public static void main( String[] args ) throws Exception {
          try {
              String pkcs8PrivateKeyString = "" +
              "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDN4c80VU9yjbAu" +
              "JWq3RYPBsLnP2wcipVdGIzPYLqyXUq2GBlhyaLjN0NI5mO+AyXE8FSJtD/Y22ILQ" +
              "..." +
              "..." +
              "..." +
              "Qmh4OV5ZHXwx5R6ffAOAwAY=";
              PrivateKey privateKey = Auth.getPrivateKeyFromString(pkcs8PrivateKeyString);
      
              if(privateKey != null) {
                  System.out.println("Private Key loaded...");
              }
      
          } catch(Exception e) {
              System.out.println(e);
          }
      }
    • 개인키 파일의 개인키를 적재하는 방법

      이 메소드를 사용하려면 아래의 라이브러리를 추가로 지정하여야 합니다.

      • Maven dependency에 추가 (pom.xml)

        bcpkix-jdk18on(org.bouncycastle)

      • Gradle dependency에 추가 (build.gradle)

        org.bouncycastle:bcpkix-jdk18on:1.77

      Auth.java 파일에 import할 패키지를 추가합니다.

      import java.io.FileReader;
      import org.bouncycastle.util.io.pem.PemObject;
      import org.bouncycastle.util.io.pem.PemReader;

      Auth.java 파일에 아래의 메소드를 추가합니다.

      public static PrivateKey getPrivateKeyFromKeyFile(String filePath) throws Exception {
          PrivateKey privateKey = null;
          File file = new File(filePath);
      
          try (FileReader keyReader = new FileReader(file);
              PemReader pemReader = new PemReader(keyReader)){
                  PemObject pemObject = pemReader.readPemObject();
                  byte[] content = pemObject.getContent();
                  PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content);
                  KeyFactory factory = KeyFactory.getInstance("RSA");
      
                  privateKey = (PrivateKey) factory.generatePrivate(privKeySpec);
      
          } catch (Exception e) {
              System.out.println(e);
          }
      
          return privateKey;
      }

      App.java 파일의 main 메소드에 아래와 같이 호출 코드를 추가하고 'Run'을 실행해 봅니다.

      public static void main( String[] args ) throws Exception {
          try {
              PrivateKey privateKey = Auth.getPrivateKeyFromKeyFile("./company.key");
      
              if(privateKey != null) {
                  System.out.println("Private Key loaded...");
              }
      
          } catch(Exception e) {
              System.out.println(e);
          }
      }

      company.key : 개인키 파일입니다.

    위의 세가지 방법중의 하나를 사용하도록 합니다.

  3. 인증 요청 Token을 만들기 위하여 Auth 클래스에 아래의 메소드를 추가합니다.

    public static StringBuffer getToken(String clientId, String loginUrl, String loginId, PrivateKey privateKey) {
        StringBuffer token = new StringBuffer();
        try {
            //////////////////////
            // JWT Token Header //
            //////////////////////
            String header = "{\"alg\":\"RS256\"}";
    
            //Encode the JWT Header and add it to our string to sign
            token.append(Base64.encodeBase64URLSafeString(header.getBytes("UTF-8")));
    
            //////////////////////
            // Claim Payload    //
            //////////////////////
    
            //Separate with a period
            token.append(".");
    
            //Create the JWT Claims Object
            String claimTemplate = "'{'\"iss\": \"{0}\", \"sub\": \"{1}\", \"aud\": \"{2}\", \"exp\": \"{3}\"'}'";
            String[] claimArray = new String[4];
            claimArray[0] = clientId;
            claimArray[1] = loginId;
            claimArray[2] = loginUrl;
            claimArray[3] = Long.toString((System.currentTimeMillis()/1000) + 300);
            MessageFormat claims;
            claims = new MessageFormat(claimTemplate);
            String payload = claims.format(claimArray);
    
            //Add the encoded claims object
            token.append(Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8")));
    
            //////////////////////
            // Private Key      //
            //////////////////////
    
            //Sign the JWT Header + "." + JWT Claims Object
            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(privateKey);
            signature.update(token.toString().getBytes("UTF-8"));
            String signedPayload = Base64.encodeBase64URLSafeString(signature.sign());
    
            //Separate with a period
            token.append(".");
    
            //Add the encoded signature
            token.append(signedPayload);
    
            System.out.println(token.toString());
    
        } catch(Exception e){
            System.out.println(e);
        }
        return token;
    }
    • clientId : ConnectedApp에서 제공하는 Consumer Key 입니다.

    • loginUrl : login.salesforce.com 또는 test.salesforce.com을 사용합니다.

    • loginId : ConnectedApp에서 Profile 또는 Permission Set으로 잡속이 허용된 이메일 형식의 사용자 계정입니다.

    이 코드로 생성되는 Token 값은 Salesforce의 JWT Bearer 플로우를 위한 값입니다. 다른 시스템들은 Heade와 Claim이 다른 형식을 사용할 수 있습니다.

    App.java 파일의 main 메소드에 아래와 같이 호출 코드를 추가하고 'Run'을 실행해 봅니다.

    public static void main( String[] args ) throws Exception {
        try {
            PrivateKey privateKey = Auth.getPrivateKeyFromKeyFile("./company.key");
    
            //////////////////
            // Make a Token //
            //////////////////
            String consumerKey = "3MVG9YDQS5WtC11q60F............QYX68awYhUzKVMMB";
            String loginUrl    = "https://login.salesforce.com/";
            String loginUser   = "username@example.com";
            StringBuffer token = Auth.getToken(consumerKey, loginUrl, loginUser, privateKey);
    
            if(token != null) {
                System.out.println(token);
            }
    
        } catch(Exception e) {
            System.out.println(e);
        }
    }

    eyJhbGciOiJSUzI1NiJ9.ey.... 와 같은 인코딩된 문자열이 출력되면 정상입니다.