web-dev-qa-db-fra.com

Exemple fiable d'utilisation de SFTP à l'aide de l'authentification par clé privée publique avec Java

Récemment, un client de notre a déplacé de manière inattendue certains fichiers importants que nous collectons d'un serveur ftp vers un serveur sftp. Au départ, j'avais l'impression qu'il serait simple d'écrire ou de trouver un Java capable de gérer sftp, cela ne s'est certainement pas avéré être le cas. Ce qui a également aggravé ce problème est que nous essayons de nous connecter au serveur sftp à partir d'une plate-forme Windows (de sorte que la définition de l'emplacement de SSH_HOME sur le client est très confuse).

J'utilise la bibliothèque Apache-commons-vfs et j'ai réussi à obtenir une solution qui fonctionne de manière fiable pour l'authentification par nom d'utilisateur/mot de passe, mais pour l'instant rien ne peut gérer de manière fiable l'authentification par clé privée/publique.

L'exemple suivant fonctionne pour l'authentification par nom d'utilisateur/mot de passe, mais je souhaite l'ajuster pour l'authentification par clé privée/publique.

public static void sftpGetFile(String server, String userName,String password, 
        String remoteDir, String localDir, String fileNameRegex)
    {

       File localDirFile  = new File(localDir);
       FileSystemManager fsManager = null;

       if (!localDirFile.exists()) {
           localDirFile.mkdirs();
       }

       try {
           fsManager = VFS.getManager();
       } catch (FileSystemException ex) {
           LOGGER.error("Failed to get fsManager from VFS",ex);
           throw new RuntimeException("Failed to get fsManager from VFS", ex);
       }

       UserAuthenticator auth = new StaticUserAuthenticator(null, userName,password);

       FileSystemOptions opts = new FileSystemOptions();

       try {
           DefaultFileSystemConfigBuilder.getInstance().setUserAuthenticator(opts,
                   auth);
       } catch (FileSystemException ex) {
           LOGGER.error("setUserAuthenticator failed", ex);
           throw new RuntimeException("setUserAuthenticator failed", ex);
       }
       Pattern filePattern = Pattern.compile(fileNameRegex);
       String startPath = "sftp://" + server + remoteDir;
       FileObject[] children;

       // Set starting path on remote SFTP server.
       FileObject sftpFile;
       try {
           sftpFile = fsManager.resolveFile(startPath, opts);

           LOGGER.info("SFTP connection successfully established to " +
                   startPath);
       } catch (FileSystemException ex) {
           LOGGER.error("SFTP error parsing path " +
                   remoteDir,
                   ex);

           throw new RuntimeException("SFTP error parsing path " +
                   remoteDir,
                   ex);
       }

       // Get a directory listing
       try {
           children = sftpFile.getChildren();
       } catch (FileSystemException ex) {
           throw new RuntimeException("Error collecting directory listing of " +
                   startPath, ex);
       }

       search:
       for (FileObject f : children) {
           try {
               String relativePath =
                       File.separatorChar + f.getName().getBaseName();

               if (f.getType() == FileType.FILE) {
                   System.out.println("Examining remote file " + f.getName());

                   if (!filePattern.matcher(f.getName().getPath()).matches()) {
                       LOGGER.info("  Filename does not match, skipping file ." +
                               relativePath);
                       continue search;
                   }

                   String localUrl = "file://" + localDir + relativePath;
                   String standardPath = localDir + relativePath;
                   System.out.println("  Standard local path is " + standardPath);
                   LocalFile localFile =
                           (LocalFile) fsManager.resolveFile(localUrl);
                   System.out.println("    Resolved local file name: " +
                           localFile.getName());

                   if (!localFile.getParent().exists()) {
                       localFile.getParent().createFolder();
                   }

                   System.out.println("  ### Retrieving file ###");
                   localFile.copyFrom(f,
                           new AllFileSelector());
               } else {
                   System.out.println("Ignoring non-file " + f.getName());
               }
           } catch (FileSystemException ex) {
               throw new RuntimeException("Error getting file type for " +
                       f.getName(), ex);
           }

       }

       FileSystem fs = null;
       if (children.length > 0) {
           fs = children[0].getFileSystem(); // This works even if the src is closed.
           fsManager.closeFileSystem(fs);
       }
    }

J'ai ma clé privée stockée dans un emplacement connu et ma clé publique a été distribuée au serveur (nous avons testé que ces clés fonctionnent correctement lors de la connexion à l'aide d'autres outils)

J'ai joué avec l'ajout de la ligne suivante

SftpFileSystemConfigBuilder.getInstance().setIdentities(this.opts, new File[]{new File("c:/Users/bobtbuilder/.ssh/id_dsa.ppk")});

Cela charge avec succès la clé privée dans l'ensemble du cadre, mais il n'utilise jamais cette clé pour s'authentifier davantage.

Toute aide ou direction reçue chaleureusement

16
grumblebee

Après avoir beaucoup fouillé, j'ai finalement trouvé la réponse moi-même. Semble qu'une grande partie de ma peine était liée au format de la clé privée et publique

privateKey doit être au format openSSH publicKey pour une raison quelconque ne peut être collé qu'à partir de la fenêtre puttyGen (l'exportation de la clé publique semblait toujours lui donner des en-têtes manquants, ce qui signifie que le serveur Windows freeSSHD ne pouvait pas l'utiliser)

Quoi qu'il en soit, voici mon code que j'ai finalement trouvé, y compris le javadoc, donc j'espère que cela devrait sauver d'autres personnes de la douleur que j'ai endurée

/**
* Fetches a file from a remote sftp server and copies it to a local file location.  The authentication method used
* is public/private key authentication. <br><br>

* IMPORTANT: Your private key must be in the OpenSSH format, also it must not have a passphrase associated with it.
*    (currently the Apache-commons-vfs2 library does not support passphrases)<p>
* 
* Also remember your public key needs to be on the sftp server.  If you were connecting as user 'bob' then your
* public key will need to be in '.ssh/bob' on the server (the location of .ssh will change depending on the type
* of sftp server)
* 
* @param server The server we care connection to 
* @param userName The username we are connection as
* @param openSSHPrivateKey The location of the  private key (which must be in openSSH format) on the local machine
* @param remoteDir The directory from where you want to retrieve the file on the remote machine (this is in reference to SSH_HOME, SSH_HOME is the direcory you 
* automatically get directed to when connecting)
* @param remoteFile The name of the file on the remote machine to be collected (does not support wild cards)
* @param localDir The direcoty on the local machine where you want the file to be copied to
* @param localFileName The name you wish to give to retrieved file on the local machine
* @throws IOException - Gets thrown is there is any problem fetching the file
*/
public static void sftpGetFile_keyAuthentication(String server, String userName, String openSSHPrivateKey,
    String remoteDir,String remoteFile, String localDir, String localFileName) throws IOException
{

   FileSystemOptions fsOptions = new FileSystemOptions();
   FileSystemManager fsManager = null;
   String remoteURL = "sftp://" + userName + "@" + server + "/" + remoteDir + "/" + remoteFile;
   String localURL  = "file://" + localDir + "/" + localFileName;

    try {
        SftpFileSystemConfigBuilder.getInstance().setStrictHostKeyChecking(fsOptions, "no");
        SftpFileSystemConfigBuilder.getInstance().setIdentities(fsOptions, new File[]{new File(openSSHPrivateKey)});
        fsManager = VFS.getManager();
        FileObject remoteFileObject = fsManager.resolveFile(remoteURL, fsOptions);
        LocalFile localFile =
                   (LocalFile) fsManager.resolveFile(localURL);
        localFile.copyFrom(remoteFileObject,
                   new AllFileSelector());
    } catch (FileSystemException e) {
        LOGGER.error("Problem retrieving from " + remoteURL + " to " + localURL,e );
        throw new IOException(e);
    }
}
17
grumblebee

Je suppose que c'est ce que vous cherchez -

/**
* @param args
*/
public static void main(String[] args) {

    /*Below we have declared and defined the SFTP Host, PORT, USER
            and Local private key from where you will make connection */
    String SFTPHOST = "10.20.30.40";
    int    SFTPPORT = 22;
    String SFTPUSER = "kodehelp";
    // this file can be id_rsa or id_dsa based on which algorithm is used to create the key
    String privateKey = "/home/kodehelp/.ssh/id_rsa";
    String SFTPWORKINGDIR = "/home/kodehelp/";

    JSch jSch = new JSch();
    Session     session     = null;
    Channel     channel     = null;
    ChannelSftp channelSftp = null;
    try {
        jSch.addIdentity(privateKey);
        System.out.println("Private Key Added.");
        session = jSch.getSession(SFTPUSER,SFTPHOST,SFTPPORT);
        System.out.println("session created.");

        Java.util.Properties config = new Java.util.Properties();
        config.put("StrictHostKeyChecking", "no");
        session.setConfig(config);
        session.connect();
        channel = session.openChannel("sftp");
        channel.connect();
        System.out.println("Shell channel connected....");
        channelSftp = (ChannelSftp)channel;
        channelSftp.cd(SFTPWORKINGDIR);
        System.out.println("Changed the directory...");
    } catch (JSchException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (SftpException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }finally{
        if(channelSftp!=null){
            channelSftp.disconnect();
            channelSftp.exit();
        }
        if(channel!=null) channel.disconnect();

        if(session!=null) session.disconnect();
    }
}

Voir plus à

http://kodehelp.com/sftp-connection-public-key-authentication-Java/

8
Dinesh K

Ce message et cette réponse ont été très utiles, merci beaucoup.

Je veux juste ajouter une intégration à la déclaration "actuellement la bibliothèque Apache-commons-vfs2 ne prend pas en charge les phrases secrètes", comme je l'ai fait également avec la phrase secrète et cela a fonctionné.

Vous devez importer la bibliothèque jsch dans votre projet (j'ai utilisé 0.1.49) et implémenter l'interface "com.jcraft.jsch.UserInfo".

Quelque chose comme ça devrait bien se passer:

public class SftpUserInfo implements UserInfo {

    public String getPassphrase() {
        return "yourpassphrase";
    }

    public String getPassword() {
        return null;
    }

    public boolean promptPassphrase(String arg0) {
        return true;
    }

    public boolean promptPassword(String arg0) {
        return false;
    }
}

Et puis vous pouvez l'ajouter au SftpFileSystemConfigBuilder de cette façon:

SftpFileSystemConfigBuilder.getInstance().setUserInfo(fsOptions, new SftpUserInfo());

J'espère que cela t'aides.

6
Gianluca Greco