Pull to refresh

Запуск кода под другим пользователем в Windows из Java

Reading time7 min
Views4.6K
Добрый день! Сейчас я расскажу вам, как запускать код под учётной записью другого пользователя в 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.
Пропишем в следующие методы в этих интерфейсах:
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
Tags:
Hubs:
+23
Comments8

Articles

Change theme settings