Добрый день! Сейчас я расскажу вам, как запускать код под учётной записью другого пользователя в Windows из Java с помощью JNA.
В общем случае это может понадобиться, когда нужно обратиться к ресурсам, которые доступны какому-либо одному пользователю, а у пользователя, под которым запущен текущий поток, нет прав для доступа к этим ресурсам. Но есть логин, домен и пароль нужного пользователя.
Для примера — есть сервер, который позволяет отдавать клиенту файлы из определенных папок. Доступ к папкам ограничен на уровне прав доступа NTFS. И у самого сервера доступа к этим папкам нет (что правильно – сервер запускается под отдельной учётной записью, у которой есть доступ только к директории сервера и папки temp). Но зато клиент знает учётные данные пользователей, которым разрешён доступ к этим папкам. Вопрос за малым – войти под нужным пользователем и получить доступ.
Для начала нужно знать, что для того что бы воспользоваться правами пользователя нужно запустить процесс или поток под учётными данными нужного пользователя, а потом уже из этого потока/процесса производить доступ к нужным ресурсам. MSDN и форумы советуют нам следующее:
1) CreateProcessWithLogonW — сразу запускает указанное приложение под нужными учётными данными. Самый простой вариант и самый не оптимальный – для того, что бы выполнить пару тройку строк кода мы будет стартовать целое приложение.
2) Связка функций – LogonUser, CreateThread, SetThreadToken, ResumeThread. В первой функции мы входом в систему под указанными учётными данными и получаем токен. Во второй функции создаём спящий поток. В третьей указывает токен для созданного потока. В чётвёртой запускаем созданный спящий поток.
Остановимся на втором варианте. Он хоть и требует некоторых дополнительных строчек кода, но самый оптимальный.
Для начала нужно выбрать, с помощью чего будем запускать native код из Java. Я знаю два способа – JNI и JNA. Первый «родной» для Java и самый быстрый (по заверению некоторых авторов). Но требует написания дополнительной native библиотеки с описание вызываемых функций. А для этого нужны некоторые посторонние телодвижение. Тем более написание native кода, хоть и простого, для Java программиста не самая тривиальная задача. Во втором случае нужно только описать в интерфейсе вызываемые функции и всё – можно использовать. Тем более что многие типы и функции Windows в JNA уже описаны.
Для использования JNA нам нужен сам jna.jar, в котором содержится основной код JNA и platform.jar в котором содержится описание некоторых функций и типов для разных платформ – Mac, Win, *nix.
Теперь перейдём к кодированию. Сначала опишем вызываемые функции – создадим два интерфейса – MoreKernel32, который наследует интерфейс Kernel32 из platform.jar, и MoreAdvapi32, который наследует Advapi32 из того же jar.
Пропишем в следующие методы в этих интерфейсах:
Метод LogonUser уже есть в интерфейсе Advapi32, его описывать не нужно.
Пару слов о том, как JNA узнаёт какие функции ей вызывать у native библиотеки. Для этого нужно описать интерфейс с этими функциями так, что бы их названия и параметры совпадали. Потом нужно вызвать Native.loadLibrary со следующими параметрами — название загружаемой библиотеки, название интерфейса к которому будет мапиться библиотека, и опции загрузки. В результате мы получим ссылку на интерфейс, из которого можно будет использовать эти функции.
Теперь расскажу поподробнее, что мы такое сделали:
1) Описали функции в интерфейсах.
2) Загрузили библиотеки kernel32 и advapi32, и замапили их на созданные интерфейсы, вызвав функцию Native.loadLibrary. Ссылку на загруженные библиотеки сохранили в переменной instance в каждом из интерфейсов.
Теперь рассмотрим два класса. Первый класс ThreadFunc предназначен для выполнения в потоке пользователя. Код очень прост – здесь только открывается файл на чтение и производиться проверка на доступность файла. Самое главное, что бы класс реализовывал интерфейс Callback, и в нём была создана функция callback(). Это позволит JNA определить что класс предназначен для обратного вызова. Если посмотреть в код интерфейса Callback можно увидеть, что в нём описано название вызываемой функции и описаны название функций, которые будут игнорироваться.
Второй класс JnaTest основной – здесь происходит создание потока, вход пользователя в систему, установка токена и самое главное, ради чего происходила вся эта канитель – чтение файла.
Рассмотрим поподробнее этот код.
Сначала создаём класс для потока и указываем какой файл открывать. Потом выполняем функцию CreateThread с двумя параметрами – ссылка на класс для открытия файла и флаг создания CREATE_SUSPENDED – указание системе что поток нужно создать «спящим». После выполнения полу��им хендл потока. Дальше проверяем, если хендл равен null, то поток не создан и нужно завершить работу.
Второй блок кода предназначен для выполнения входа в систему. Здесь перед запуском функции LogonUser нужно создать ссылку на хендл, в которую потом будет записан токен пользователя. В качестве параметров для запуска функции указываем логин, домен, пароль, указатель память, куда будет записан токен и дополнительные параметры входа пользователя. В результате выполнения функции мы получим либо true и токен пользователя либо false. Дальше идёт проверка возвращаемого значения и, если оно false, выход из программы.
Следующий блок довольно прост – там мы запускаем поток, ждём, пока он не завершится, и закрываем токены.
Чтение файла тоже не сложное – получаем файловые каналы из FileInputStream и FileOutputStream и производим копирование файла.
Закрытие ресурсов стандартное, принятое в Java – в блоке finally.
Теперь несколько замечаний:
1) При создании потока нужно сохранять ссылку на класс, которые будет вызываться в потоке, на всём протяжении времени жизни потока. Даже если доступ извне к этому классу не нужен. Иначе класс может удалиться сборщиком мусора и будет попытка доступа к памяти, заполненной неправильными данными. Собственно, это написано на сайте JNA, но это я узнал уже потом, когда словил несколько Access error.
2) Если открыть ресурс и передать его в другой поток, то права доступа сохраняться. Здесь мы можем это наблюдать – файл открыт в потоке с правами доступа одного пользователя, а само чтение файла происходит в другом потоке, с другими правами.
Теперь проверим выполнение программы. Сначала создадим папку и файл.
Потом назначит права доступа к этой папке:

Рис.1 Папка user1

Рис.2 Разрешаем доступ для пользователя user1

Рис.3 Разрешаем доступ для пользователя Tim
Запускаем программу:
Файл можно прочитать из основного потока
Файл можно прочитать из дополнительного потока
Теперь запретим пользователю Tim доступ к папке.

Рис. 4 Запрещаем права для доступа к папке для пользователя Tim
Попробуем открыть папку в Explorer. Получаем сообщение об ошибке:

Рис. 5 Сообщение об ошибке при открытии папки
Теперь запустим программу:
Файл нельзя прочитать из основного потока
Файл можно прочитать из дополнительного потока
JNA
LogonUser
CreateThread
ResumeThread
SetThreadToken
Практическое значени
В общем случае это может понадобиться, когда нужно обратиться к ресурсам, которые доступны какому-либо одному пользователю, а у пользователя, под которым запущен текущий поток, нет прав для доступа к этим ресурсам. Но есть логин, домен и пароль нужного пользователя.
Для примера — есть сервер, который позволяет отдавать клиенту файлы из определенных папок. Доступ к папкам ограничен на уровне прав доступа NTFS. И у самого сервера доступа к этим папкам нет (что правильно – сервер запускается под отдельной учётной записью, у которой есть доступ только к директории сервера и папки temp). Но зато клиент знает учётные данные пользователей, которым разрешён доступ к этим папкам. Вопрос за малым – войти под нужным пользователем и получить доступ.
Теория
Для начала нужно знать, что для того что бы воспользоваться правами пользователя нужно запустить процесс или поток под учётными данными нужного пользователя, а потом уже из этого потока/процесса производить доступ к нужным ресурсам. MSDN и форумы советуют нам следующее:
1) CreateProcessWithLogonW — сразу запускает указанное приложение под нужными учётными данными. Самый простой вариант и самый не оптимальный – для того, что бы выполнить пару тройку строк кода мы будет стартовать целое приложение.
2) Связка функций – LogonUser, CreateThread, SetThreadToken, ResumeThread. В первой функции мы входом в систему под указанными учётными данными и получаем токен. Во второй функции создаём спящий поток. В третьей указывает токен для созданного потока. В чётвёртой запускаем созданный спящий поток.
Остановимся на втором варианте. Он хоть и требует некоторых дополнительных строчек кода, но самый оптимальный.
Реализация
Для начала нужно выбрать, с помощью чего будем запускать native код из Java. Я знаю два способа – JNI и JNA. Первый «родной» для Java и самый быстрый (по заверению некоторых авторов). Но требует написания дополнительной native библиотеки с описание вызываемых функций. А для этого нужны некоторые посторонние телодвижение. Тем более написание native кода, хоть и простого, для Java программиста не самая тривиальная задача. Во втором случае нужно только описать в интерфейсе вызываемые функции и всё – можно использовать. Тем более что многие типы и функции Windows в JNA уже описаны.
Для использования JNA нам нужен сам jna.jar, в котором содержится основной код JNA и platform.jar в котором содержится описание некоторых функций и типов для разных платформ – Mac, Win, *nix.
Теперь перейдём к кодированию. Сначала опишем вызываемые функции – создадим два интерфейса – MoreKernel32, который наследует интерфейс Kernel32 из platform.jar, и MoreAdvapi32, который наследует Advapi32 из того же jar.
Пропишем в следующие методы в этих интерфейсах:
public interface MoreAdvapi32 extends Advapi32 { static final MoreAdvapi32 instance = (MoreAdvapi32) Native.loadLibrary("advapi32", MoreAdvapi32.class, W32APIOptions.DEFAULT_OPTIONS); Boolean SetThreadToken(HANDLEByReference pointer, HANDLE Token); }
public interface MoreKernel32 extends Kernel32 { static final MoreKernel32 instance = (MoreKernel32) Native.loadLibrary("kernel32", MoreKernel32.class,W32APIOptions.DEFAULT_OPTIONS); DWORD ResumeThread(WinNT.HANDLE hThread); WinNT.HANDLE CreateThread(Pointer lpThreadAttributes, Pointer dwStackSize, Callback lpStartAddress, Pointer lpParameter, DWORD dwCreationFlags, Pointer lpThreadId); }
Метод LogonUser уже есть в интерфейсе Advapi32, его описывать не нужно.
Пару слов о том, как JNA узнаёт какие функции ей вызывать у native библиотеки. Для этого нужно описать интерфейс с этими функциями так, что бы их названия и параметры совпадали. Потом нужно вызвать Native.loadLibrary со следующими параметрами — название загружаемой библиотеки, название интерфейса к которому будет мапиться библиотека, и опции загрузки. В результате мы получим ссылку на интерфейс, из которого можно будет использовать эти функции.
Теперь расскажу поподробнее, что мы такое сделали:
1) Описали функции в интерфейсах.
2) Загрузили библиотеки kernel32 и advapi32, и замапили их на созданные интерфейсы, вызвав функцию Native.loadLibrary. Ссылку на загруженные библиотеки сохранили в переменной instance в каждом из интерфейсов.
Теперь рассмотрим два класса. Первый класс ThreadFunc предназначен для выполнения в потоке пользователя. Код очень прост – здесь только открывается файл на чтение и производиться проверка на доступность файла. Самое главное, что бы класс реализовывал интерфейс Callback, и в нём была создана функция callback(). Это позволит JNA определить что класс предназначен для обратного вызова. Если посмотреть в код интерфейса Callback можно увидеть, что в нём описано название вызываемой функции и описаны название функций, которые будут игнорироваться.
package jna; import java.io.FileInputStream; import java.io.FileNotFoundException; import com.sun.jna.Callback; public class ThreadFunc implements Callback { private String fileName; private FileInputStream in; public FileInputStream getIn() { return in; } public String getFile() { return fileName; } public void setFile(String file) { this.fileName = file; } public void callback() { File testAccess = new File(fileName); if (testAccess.canRead()) { System.out.println("Файл можно прочитать из дополнительного потока"); try { in = new FileInputStream(fileName); } catch (FileNotFoundException e) { e.printStackTrace(); } }else{ System.out.println("Файл нельзя прочитать из дополнительного потока"); } } }
Второй класс JnaTest основной – здесь происходит создание потока, вход пользователя в систему, установка токена и самое главное, ради чего происходила вся эта канитель – чтение файла.
package jna; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import com.sun.jna.platform.win32.Advapi32; import com.sun.jna.platform.win32.WinBase; import com.sun.jna.platform.win32.WinDef.DWORD; import com.sun.jna.platform.win32.WinNT.HANDLE; import com.sun.jna.platform.win32.WinNT.HANDLEByReference; public class JnaTest { private static final String fileName = "D:\\user1\\hitman.jpg"; public static void main(String[] args) { FileInputStream in = null; FileOutputStream out = null; FileChannel fcin = null; FileChannel fcout = null; try { File testAccess = new File(fileName); if (testAccess.canRead()) { System.out.println("Файл можно прочитать из основного потока"); }else{ System.out.println("Файл нельзя прочитать из основного потока"); } //Создание потока //флаг CREATE_SUSPENDED DWORD flag = new DWORD(4L); ThreadFunc func = new ThreadFunc(); func.setFile(fileName); HANDLE hThread = MoreKernel32.instance.CreateThread(null, null, func, null, flag, null); if (hThread == null) { System.out.println("Поток не создан"); return; } //Вход пользователя в систему HANDLEByReference phToken = new HANDLEByReference(); Boolean logonResult = Advapi32.INSTANCE.LogonUser("user1", "HORROR", "1234567q-",WinBase.LOGON32_LOGON_NETWORK, WinBase.LOGON32_PROVIDER_DEFAULT, phToken); if (logonResult == false) { System.out.println("Вход не выполнен"); return; } //Назначение токена потоку HANDLEByReference pThread = new HANDLEByReference(hThread); Boolean setTokenResult = MoreAdvapi32.instance.SetThreadToken(pThread, phToken.getValue()); if (setTokenResult == false) { System.out.println("Токен не назначен"); return; } //Запуск и ожидание завершения потока MoreKernel32.instance.ResumeThread(hThread); MoreKernel32.instance.WaitForSingleObject(hThread, -1); MoreKernel32.instance.CloseHandle(hThread); MoreKernel32.instance.CloseHandle(phToken.getValue()); //Чтение файла in = func.getIn(); if (in == null) { return; } out = new FileOutputStream("D:\\test.zip"); fcin = in.getChannel(); fcout = out.getChannel(); fcin.transferTo(0, fcin.size(), fcout); } catch (Exception e) { e.printStackTrace(); } finally { //Закрытие ресурсов if (fcin != null) { try { fcin.close(); } catch (IOException e) { e.printStackTrace(); } } if (fcout != null) { try { fcout.close(); } catch (IOException e) { e.printStackTrace(); } } if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
Рассмотрим поподробнее этот код.
Сначала создаём класс для потока и указываем какой файл открывать. Потом выполняем функцию CreateThread с двумя параметрами – ссылка на класс для открытия файла и флаг создания CREATE_SUSPENDED – указание системе что поток нужно создать «спящим». После выполнения полу��им хендл потока. Дальше проверяем, если хендл равен null, то поток не создан и нужно завершить работу.
Второй блок кода предназначен для выполнения входа в систему. Здесь перед запуском функции LogonUser нужно создать ссылку на хендл, в которую потом будет записан токен пользователя. В качестве параметров для запуска функции указываем логин, домен, пароль, указатель память, куда будет записан токен и дополнительные параметры входа пользователя. В результате выполнения функции мы получим либо true и токен пользователя либо false. Дальше идёт проверка возвращаемого значения и, если оно false, выход из программы.
Следующий блок довольно прост – там мы запускаем поток, ждём, пока он не завершится, и закрываем токены.
Чтение файла тоже не сложное – получаем файловые каналы из FileInputStream и FileOutputStream и производим копирование файла.
Закрытие ресурсов стандартное, принятое в Java – в блоке finally.
Теперь несколько замечаний:
1) При создании потока нужно сохранять ссылку на класс, которые будет вызываться в потоке, на всём протяжении времени жизни потока. Даже если доступ извне к этому классу не нужен. Иначе класс может удалиться сборщиком мусора и будет попытка доступа к памяти, заполненной неправильными данными. Собственно, это написано на сайте JNA, но это я узнал уже потом, когда словил несколько Access error.
2) Если открыть ресурс и передать его в другой поток, то права доступа сохраняться. Здесь мы можем это наблюдать – файл открыт в потоке с правами доступа одного пользователя, а само чтение файла происходит в другом потоке, с другими правами.
Пример выполнения
Теперь проверим выполнение программы. Сначала создадим папку и файл.
Потом назначит права доступа к этой папке:

Рис.1 Папка user1

Рис.2 Разрешаем доступ для пользователя user1

Рис.3 Разрешаем доступ для пользователя Tim
Запускаем программу:
Файл можно прочитать из основного потока
Файл можно прочитать из дополнительного потока
Теперь запретим пользователю Tim доступ к папке.

Рис. 4 Запрещаем права для доступа к папке для пользователя Tim
Попробуем открыть папку в Explorer. Получаем сообщение об ошибке:

Рис. 5 Сообщение об ошибке при открытии папки
Теперь запустим программу:
Файл нельзя прочитать из основного потока
Файл можно прочитать из дополнительного потока
Ссылки на ресурсы
JNA
LogonUser
CreateThread
ResumeThread
SetThreadToken
