Guida al servizio di autenticazione e autorizzazione Java (JAAS)

Java Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Panoramica

Java Authentication And Authorization Service (JAAS) è un framework di sicurezza di basso livello Java SE che amplia il modello di sicurezza dalla sicurezza basata sul codice alla sicurezza basata sull'utente . Possiamo usare JAAS per due scopi:

  • Autenticazione: identificazione dell'entità che sta attualmente eseguendo il codice
  • Autorizzazione: una volta autenticata, assicurarsi che questa entità disponga dei diritti o delle autorizzazioni di controllo dell'accesso necessari per eseguire codice sensibile

In questo tutorial, tratteremo come impostare JAAS in un'applicazione di esempio implementando e configurando le sue varie API, in particolare LoginModule .

2. Come funziona JAAS

Quando si utilizza JAAS in un'applicazione, sono coinvolte diverse API:

  • CallbackHandler : utilizzato per raccogliere le credenziali dell'utente e facoltativamente fornito durante la creazione del LoginContext
  • Configurazione : responsabile del caricamento delle implementazioni di LoginModule e può essere fornito facoltativamente durante la creazione di LoginContext
  • LoginModule : utilizzato in modo efficace per autenticare gli utenti

Useremo l'implementazione predefinita per l' API di configurazione e forniremo le nostre implementazioni per le API CallbackHandler e LoginModule .

3. Fornire l' implementazione di CallbackHandler

Prima di approfondire l' implementazione di LoginModule , dobbiamo prima fornire un'implementazione per l' interfaccia CallbackHandler , che viene utilizzata per raccogliere le credenziali dell'utente .

Ha un unico metodo, handle () , che accetta un array di Callback . Inoltre, JAAS fornisce già molte implementazioni di callback e utilizzeremo NameCallback e PasswordCallback per raccogliere rispettivamente il nome utente e la password.

Vediamo la nostra implementazione dell'interfaccia 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); } } } }

Quindi, per richiedere e leggere il nome utente, abbiamo utilizzato:

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

Allo stesso modo, per richiedere e leggere la password:

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

Successivamente, vedremo come chiamare CallbackHandler durante l'implementazione del LoginModule .

4. Fornire l' implementazione di LoginModule

Per semplicità, forniremo un'implementazione che memorizza gli utenti hard-coded. Quindi chiamiamolo 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 sharedState; private Map options; private boolean loginSucceeded = false; private Principal userPrincipal; //... }

Nelle prossime sottosezioni forniremo un'implementazione per i metodi più importanti: initialize () , login () e commit () .

4.1. inizializzare()

Il LoginModule viene prima caricato e quindi inizializzato con un Subject e un CallbackHandler . Inoltre, i LoginModule possono utilizzare una mappa per condividere i dati tra di loro e un'altra mappa per memorizzare i dati di configurazione privati:

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

4.2. accesso()

Nel metodo login () , invochiamo il metodo CallbackHandler.handle () con NameCallback e PasswordCallback per richiedere e ottenere il nome utente e la password. Quindi, confrontiamo queste credenziali fornite con quelle hardcoded:

@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; }

Il metodo login () dovrebbe restituire true per un'operazione riuscita e false per un accesso non riuscito .

4.3. commettere()

Se tutte le chiamate a LoginModule # login hanno esito positivo, aggiorniamo l' oggetto con un Principal aggiuntivo :

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

Altrimenti, viene chiamato il metodo abort () .

A questo punto, l' implementazione del nostro LoginModule è pronta e deve essere configurata in modo che possa essere caricata dinamicamente utilizzando il provider di servizi di configurazione .

5. Configurazione LoginModule

JAAS utilizza il provider di servizi di configurazione per caricare i LoginModule in fase di esecuzione. Per impostazione predefinita, fornisce e utilizza il ConfigFile implementazione dove LoginModule s sono configurati attraverso un file di accesso. Ad esempio, ecco il contenuto del file utilizzato per il nostro LoginModule :

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

Come possiamo vedere, abbiamo fornito il nome completo della classe del LoginModule realizzazione , una richiesta di bandiera, e un'opzione per il debug.

Infine, nota che possiamo anche specificare il file di login tramite la proprietà di sistema java.security.auth.login.config :

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

Possiamo anche specificare uno o più file di accesso tramite la proprietà login.config.url nel file di sicurezza Java, $ {java.home} /jre/lib/security/java.security :

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

6. Autenticazione

Innanzitutto, un'applicazione inizializza il processo di autenticazione creando un'istanza di LoginContext . Per fare ciò, possiamo dare un'occhiata al costruttore completo per avere un'idea di ciò di cui abbiamo bisogno come parametri:

LoginContext(String name, Subject subject, CallbackHandler callbackHandler, Configuration config)
  • name: used as an index for loading only the corresponding LoginModules
  • subject: represents a user or service that wants to log in
  • callbackHandler: responsible for passing user credentials from the application to the LoginModule
  • config: responsible for loading LoginModules that correspond to the name parameter

Here, we'll be using the overloaded constructor where we'll be providing our CallbackHandler implementation:

LoginContext(String name, CallbackHandler callbackHandler)

Now that we have a CallbackHandler and a configured LoginModule, we can start the authentication process by initializing a LoginContext object:

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

At this point, we can invoke the login() method to authenticate the user:

loginContext.login();

The login() method, in turn, creates a new instance of our LoginModule and calls its login() method. And, upon successful authentication, we can retrieve the authenticated Subject:

Subject subject = loginContext.getSubject();

Now, let's run a sample application that has the LoginModule wired in:

$ 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.baeldung.jaas.JaasAuthentication

When we're prompted to provide the username and password, we'll use testuser and testpassword as credentials.

7. Authorization

Authorization comes into play when the user is first connected and associated with the AccessControlContext. Using the Java security policy, we can grant one or more access control rights to Principals. We can then prevent access to sensitive code by calling the SecurityManager#checkPermission method:

SecurityManager.checkPermission(Permission perm)

7.1. Defining Permissions

An access control right or permission is the ability to execute an action on a resource. We can implement a permission by subclassing the Permission abstract class. To do so, we need to provide a resource name and a set of possible actions. For example, we can use FilePermission to configure access control rights on files. Possible actions are read, write, execute, and so on. For scenarios where actions are not necessary, we may simply use the BasicPermision.

Next, we'll provide an implementation of permission through the ResourcePermission class where users may have permission to access a resource:

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

Later, we'll configure an entry for this permission through the Java security policy.

7.2. Granting Permissions

Usually, we don't need to know the policy file syntax because we can always use the Policy Tool to create one. Let's take a look at our policy file:

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

In this sample, we've granted the test_resource permission to the testuser user.

7.3. Checking Permissions

Once the Subject is authenticated and permissions are configured, we can check for access by calling the Subject#doAs or Subject#doAsPrivilieged static methods. For this purpose, we'll provide a PrivilegedAction where we can protect access to sensitive code. In the run() method, we call the SecurityManager#checkPermission method to ensure that the authenticated user has the test_resource permission:

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; } }

The last thing is to call the Subject#doAsPrivileged method:

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

Come per l'autenticazione, eseguiremo una semplice applicazione per l'autorizzazione dove, oltre al LoginModule , forniamo un file di configurazione delle autorizzazioni:

$ 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.baeldung.jaas.JaasAuthorization

8. Conclusione

In questo articolo, abbiamo mostrato come implementare JAAS esplorando le principali classi e interfacce e mostrando come configurarle. In particolare, abbiamo implementato un LoginModule del provider di servizi .

Come al solito, il codice in questo articolo è disponibile su GitHub.

Fondo Java

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO