Перейти к основному содержимому

Руководство по службе аутентификации и авторизации Java (JAAS)

· 8 мин. чтения

1. Обзор

Служба аутентификации и авторизации Java (JAAS) — это низкоуровневая структура безопасности Java SE, которая расширяет модель безопасности от безопасности на основе кода до безопасности на основе пользователей . Мы можем использовать JAAS для двух целей:

  • Аутентификация: идентификация объекта, который в данный момент выполняет код.
  • Авторизация: после аутентификации убедитесь, что этот объект имеет необходимые права управления доступом или разрешения для выполнения конфиденциального кода.

В этом руководстве мы расскажем, как настроить JAAS в примере приложения, внедрив и настроив его различные API, особенно LoginModule .

2. Как работает JAAS

При использовании JAAS в приложении задействованы несколько API:

  • CallbackHandler : используется для сбора учетных данных пользователя и необязательно предоставляется при создании LoginContext.
  • Конфигурация : отвечает за загрузку реализаций LoginModule и может быть дополнительно предоставлен при создании LoginContext.
  • LoginModule : эффективно используется для аутентификации пользователей.

Мы будем использовать реализацию по умолчанию для API конфигурации и предоставим собственные реализации для API CallbackHandler и LoginModule .

3. Предоставление реализации CallbackHandler

Прежде чем углубляться в реализацию LoginModule , нам сначала нужно предоставить реализацию интерфейса CallbackHandler , который используется для сбора учетных данных пользователя .

Он имеет единственный метод handle() , который принимает массив Callback s. Кроме того, JAAS уже предоставляет множество реализаций обратного вызова , и мы будем использовать NameCallback и PasswordCallback для сбора имени пользователя и пароля соответственно.

Давайте посмотрим на нашу реализацию интерфейса CallbackHandler :

public class ConsoleCallbackHandler implements CallbackHandler {

@Override
public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
Console console = System.console();
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
NameCallback nameCallback = (NameCallback) callback;
nameCallback.setName(console.readLine(nameCallback.getPrompt()));
} else if (callback instanceof PasswordCallback) {
PasswordCallback passwordCallback = (PasswordCallback) callback;
passwordCallback.setPassword(console.readPassword(passwordCallback.getPrompt()));
} else {
throw new UnsupportedCallbackException(callback);
}
}
}
}

Итак, чтобы запросить и прочитать имя пользователя, мы использовали:

NameCallback nameCallback = (NameCallback) callback;
nameCallback.setName(console.readLine(nameCallback.getPrompt()));

Аналогично, чтобы запросить и прочитать пароль:

PasswordCallback passwordCallback = (PasswordCallback) callback;
passwordCallback.setPassword(console.readPassword(passwordCallback.getPrompt()));

Позже мы увидим, как вызывать CallbackHandler при реализации LoginModule .

4. Предоставление реализации LoginModule

Для простоты мы предоставим реализацию, в которой хранятся жестко заданные пользователи. Итак, назовем его InMemoryLoginModule :

public class InMemoryLoginModule implements LoginModule {

private static final String USERNAME = "testuser";
private static final String PASSWORD = "testpassword";

private Subject subject;
private CallbackHandler callbackHandler;
private Map<String, ?> sharedState;
private Map<String, ?> options;

private boolean loginSucceeded = false;
private Principal userPrincipal;
//...
}

В следующих подразделах мы представим реализацию наиболее важных методов: initialize() , login() и commit() .

4.1. инициализировать()

LoginModule сначала загружается, а затем инициализируется с помощью Subject и CallbackHandler . Кроме того, LoginModule могут использовать карту для обмена данными между собой и другую карту для хранения личных данных конфигурации:

public void initialize(
Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.sharedState = sharedState;
this.options = options;
}

4.2. авторизоваться()

В методе login() мы вызываем метод CallbackHandler.handle() с NameCallback и PasswordCallback , чтобы запросить и получить имя пользователя и пароль. Затем мы сравниваем эти предоставленные учетные данные с жестко заданными:

@Override
public boolean login() throws LoginException {
NameCallback nameCallback = new NameCallback("username: ");
PasswordCallback passwordCallback = new PasswordCallback("password: ", false);
try {
callbackHandler.handle(new Callback[]{nameCallback, passwordCallback});
String username = nameCallback.getName();
String password = new String(passwordCallback.getPassword());
if (USERNAME.equals(username) && PASSWORD.equals(password)) {
loginSucceeded = true;
}
} catch (IOException | UnsupportedCallbackException e) {
//...
}
return loginSucceeded;
}

Метод login() должен возвращать true в случае успешной операции и false в случае неудачного входа .

4.3. совершить()

Если все вызовы LoginModule#login успешны, мы обновляем Subject дополнительным Principal :

@Override
public boolean commit() throws LoginException {
if (!loginSucceeded) {
return false;
}
userPrincipal = new UserPrincipal(username);
subject.getPrincipals().add(userPrincipal);
return true;
}

В противном случае вызывается метод abort() .

На данный момент наша реализация LoginModule готова, и ее необходимо настроить так, чтобы ее можно было загружать динамически с помощью поставщика службы конфигурации .

5. Конфигурация модуля входа в систему

JAAS использует поставщика услуг конфигурации для загрузки LoginModule во время выполнения. По умолчанию он предоставляет и использует реализацию ConfigFile , в которой LoginModule настраиваются через файл входа в систему. Например, вот содержимое файла, используемого для нашего LoginModule :

jaasApplication {
com.foreach.jaas.loginmodule.InMemoryLoginModule required debug=true;
};

Как мы видим, мы предоставили полное имя класса реализации LoginModule , обязательный флаг и параметр для отладки.

Наконец, обратите внимание, что мы также можем указать файл входа в систему через системное свойство java.security.auth.login.config :

$ java -Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config

Мы также можем указать один или несколько файлов входа через свойство login.config.url в файле безопасности Java, ${java.home}/jre/lib/security/java.security :

login.config.url.1=file:${user.home}/.java.login.config

6. Аутентификация

Во- первых, приложение инициализирует процесс аутентификации, создавая экземпляр LoginContext . Для этого мы можем взглянуть на полный конструктор, чтобы иметь представление о том, что нам нужно в качестве параметров:

LoginContext(String name, Subject subject, CallbackHandler callbackHandler, Configuration config)
  • name : используется в качестве индекса для загрузки только соответствующих LoginModule s
  • subject : представляет пользователя или службу, которая хочет войти в систему
  • callbackHandler : отвечает за передачу учетных данных пользователя из приложения в LoginModule.
  • config : отвечает за загрузку LoginModule , соответствующих параметру name

Здесь мы будем использовать перегруженный конструктор, в котором мы будем предоставлять нашу реализацию CallbackHandler :

LoginContext(String name, CallbackHandler callbackHandler)

Теперь, когда у нас есть CallbackHandler и настроенный LoginModule , мы можем начать процесс аутентификации, инициализировав объект LoginContext :

LoginContext loginContext = new LoginContext("jaasApplication", new ConsoleCallbackHandler());

На этом этапе мы можем вызвать метод login() для аутентификации пользователя :

loginContext.login();

Метод login() , в свою очередь, создает новый экземпляр нашего LoginModule и вызывает его метод login() . И после успешной аутентификации мы можем получить аутентифицированный Subject :

Subject subject = loginContext.getSubject();

Теперь давайте запустим пример приложения, в котором подключен LoginModule :

$ mvn clean package
$ java -Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config \
-classpath target/core-java-security-2-0.1.0-SNAPSHOT.jar com.foreach.jaas.JaasAuthentication

Когда нас попросят указать имя пользователя и пароль, мы будем использовать testuser и testpassword в качестве учетных данных.

7. Авторизация

Авторизация вступает в игру, когда пользователь впервые подключается и связывается с AccessControlContext . Используя политику безопасности Java, мы можем предоставить одно или несколько прав управления доступом для Principal s. Затем мы можем предотвратить доступ к конфиденциальному коду, вызвав метод SecurityManager#checkPermission :

SecurityManager.checkPermission(Permission perm)

7.1. Определение разрешений

Право или разрешение на управление доступом — это возможность выполнить действие над ресурсом . Мы можем реализовать разрешение, создав подкласс абстрактного класса Permission . Для этого нам нужно указать имя ресурса и набор возможных действий. Например, мы можем использовать FilePermission для настройки прав доступа к файлам. Возможные действия: чтение , запись , выполнение и так далее. Для сценариев, где действия не требуются, мы можем просто использовать BasicPermision .

Далее мы обеспечим реализацию разрешения через класс ResourcePermission , где пользователи могут иметь разрешение на доступ к ресурсу:

public final class ResourcePermission extends BasicPermission {
public ResourcePermission(String name) {
super(name);
}
}

Позже мы настроим запись для этого разрешения через политику безопасности Java.

7.2. Предоставление разрешений

Обычно нам не нужно знать синтаксис файла политики, потому что мы всегда можем использовать инструмент политики для его создания. Давайте посмотрим на наш файл политики:

grant principal com.sun.security.auth.UserPrincipal testuser {
permission com.foreach.jaas.ResourcePermission "test_resource"
};

В этом примере мы предоставили разрешение test_resource пользователю testuser .

7.3. Проверка разрешений

После аутентификации субъекта и настройки разрешений мы можем проверить наличие доступа, вызвав статические методы Subject#doAs или Subject#doAsPrivilieged . Для этой цели мы предоставим PrivilegedAction , с помощью которого мы сможем защитить доступ к конфиденциальному коду. В методе run() мы вызываем метод SecurityManager#checkPermission , чтобы убедиться, что у аутентифицированного пользователя есть разрешение test_resource :

public class ResourceAction implements PrivilegedAction {
@Override
public Object run() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new ResourcePermission("test_resource"));
}
System.out.println("I have access to test_resource !");
return null;
}
}

Последнее, что нужно сделать, это вызвать метод Subject#doAsPrivileged :

Subject subject = loginContext.getSubject();
PrivilegedAction privilegedAction = new ResourceAction();
Subject.doAsPrivileged(subject, privilegedAction, null);

Как и в случае с аутентификацией, мы запустим простое приложение для авторизации, в котором в дополнение к LoginModule мы предоставим файл конфигурации разрешений:

$ mvn clean package
$ java -Djava.security.manager -Djava.security.policy=src/main/resources/jaas/jaas.policy \
-Djava.security.auth.login.config=src/main/resources/jaas/jaas.login.config \
-classpath target/core-java-security-2-0.1.0-SNAPSHOT.jar com.foreach.jaas.JaasAuthorization

8. Заключение

В этой статье мы продемонстрировали, как реализовать JAAS, изучив основные классы и интерфейсы и показав, как их настроить. В частности, мы внедрили сервис-провайдера LoginModule .

Как обычно, код из этой статьи доступен на GitHub .