Einführung in Apache Shiro

1. Überblick

In diesem Artikel betrachten wir Apache Shiro , ein vielseitiges Java-Sicherheitsframework.

Das Framework ist sehr anpassbar und modular, da es Authentifizierung, Autorisierung, Kryptographie und Sitzungsverwaltung bietet.

2. Abhängigkeit

Apache Shiro hat viele module .

In diesem Lernprogramm verwenden wir jedoch nur das Artefakt shiro-core .

Fügen wir es unserer pom.xml hinzu:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>

Die neueste Version der Apache Shiro-Module finden Sie unter https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.apache.shiro%22 in Maven Central.

3. Security Manager konfigurieren

Der SecurityManager ist das Kernstück des Apache Shiro-Frameworks.

In Anwendungen wird normalerweise eine einzige Instanz ausgeführt.

In diesem Lernprogramm untersuchen wir das Framework in einer Desktop-Umgebung. Um das Framework zu konfigurieren, müssen Sie im Ressourcenordner eine shiro.ini -Datei mit folgendem Inhalt erstellen:

----[users]user = password, admin
user2 = password2, editor
user3 = password3, author
[roles]admin = **
editor = articles:**
author = articles:compose,articles:save
----

Der Abschnitt [Benutzer] der Konfigurationsdatei shiro.ini definiert die Benutzeranmeldeinformationen, die vom SecurityManager erkannt werden. Das Format ist:

principal (Benutzername) = Kennwort, Rolle1, Rolle2, …​, Rolle .

Die Rollen und die zugehörigen Berechtigungen werden im Abschnitt [Rollen] festgelegt. Die admin -Rolle erhält Berechtigung und Zugriff auf alle Teile der Anwendung. Dies wird durch das Platzhalterzeichen (** ) ​​ angezeigt.

Die editor -Rolle verfügt über alle Berechtigungen, die articles zugeordnet sind, während die author -Rolle nur Compose und save einen Artikel enthalten kann.

Der SecurityManager wird zum Konfigurieren der SecurityUtils -Klasse verwendet.

Von den SecurityUtils können wir den aktuellen Benutzer erhalten, der mit dem System interagiert, und Authentifizierungs- und Autorisierungsvorgänge durchführen.

Verwenden Sie IniRealm , um unsere Benutzer- und Rollendefinitionen aus der Datei shiro.ini zu laden, und konfigurieren Sie dann das Objekt DefaultSecurityManager :

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();

Da wir nun einen SecurityManager haben, der die in der Datei shiro.ini definierten Benutzeranmeldeinformationen und Rollen kennt, gehen wir zur Benutzerauthentifizierung und -autorisierung über.

4. Authentifizierung

In den Terminologien von Apache Shiro ist ein Subject jede Entität, die mit dem System interagiert. Es kann sich dabei entweder um einen Benutzer, ein Skript oder einen REST-Client handeln.

Beim Aufruf von SecurityUtils.getSubject () wird eine Instanz des aktuellen Subject zurückgegeben, d. H. Der currentUser .

Jetzt, da wir das currentUser -Objekt haben, können wir die angegebenen Anmeldeinformationen authentifizieren:

if (!currentUser.isAuthenticated()) {
  UsernamePasswordToken token
    = new UsernamePasswordToken("user", "password");
  token.setRememberMe(true);
  try {
      currentUser.login(token);
  } catch (UnknownAccountException uae) {
      log.error("Username Not Found!", uae);
  } catch (IncorrectCredentialsException ice) {
      log.error("Invalid Credentials!", ice);
  } catch (LockedAccountException lae) {
      log.error("Your Account is Locked!", lae);
  } catch (AuthenticationException ae) {
      log.error("Unexpected Error!", ae);
  }
}

Zuerst prüfen wir, ob der aktuelle Benutzer noch nicht authentifiziert wurde.

Dann erstellen wir ein Authentifizierungs-Token mit dem Principal (Benutzername) des Benutzers und dem Berechtigungsnachweis (Passwort) .

Als Nächstes versuchen wir uns mit dem Token einzuloggen. Wenn die angegebenen Anmeldeinformationen korrekt sind, sollte alles in Ordnung sein.

Es gibt verschiedene Ausnahmen für verschiedene Fälle. Es ist auch möglich, eine benutzerdefinierte Ausnahme auszulösen, die den Anforderungen der Anwendung besser entspricht. Dies kann durch Unterklasse der Klasse AccountException erfolgen.

5. Genehmigung

Die Authentifizierung versucht, die Identität eines Benutzers zu überprüfen, während die Autorisierung versucht, den Zugriff auf bestimmte Ressourcen im System zu steuern.

Denken Sie daran, dass wir jedem Benutzer, den wir in der Datei shiro.ini erstellt haben, eine oder mehrere Rollen zuweisen. Darüber hinaus definieren wir im Rollenabschnitt für jede Rolle unterschiedliche Berechtigungen oder Zugriffsebenen.

Nun wollen wir sehen, wie wir das in unserer Anwendung einsetzen können, um die Zugriffssteuerung für Benutzer durchzusetzen.

In der Datei shiro.ini geben wir dem Administrator vollständigen Zugriff auf alle Systembereiche.

Der Editor hat vollständigen Zugriff auf alle Ressourcen/Vorgänge in Bezug auf articles . Ein Autor kann sich nur darauf beschränken, articles zu erstellen und zu speichern.

Lasst uns den aktuellen Benutzer nach Rolle begrüßen:

if (currentUser.hasRole("admin")) {
    log.info("Welcome Admin");
} else if(currentUser.hasRole("editor")) {
    log.info("Welcome, Editor!");
} else if(currentUser.hasRole("author")) {
    log.info("Welcome, Author");
} else {
    log.info("Welcome, Guest");
}

Nun wollen wir sehen, was der aktuelle Benutzer im System tun darf:

if(currentUser.isPermitted("articles:compose")) {
    log.info("You can compose an article");
} else {
    log.info("You are not permitted to compose an article!");
}

if(currentUser.isPermitted("articles:save")) {
    log.info("You can save articles");
} else {
    log.info("You can not save articles");
}

if(currentUser.isPermitted("articles:publish")) {
    log.info("You can publish articles");
} else {
    log.info("You can not publish articles");
}

6. Realm-Konfiguration

In realen Anwendungen benötigen wir eine Möglichkeit, Benutzeranmeldeinformationen aus einer Datenbank und nicht aus der Datei shiro.ini abzurufen. Hier kommt das Konzept von Realm ins Spiel.

In der Terminologie von Apache Shiro ist ein Realm ein DAO, der auf einen Speicher von Benutzeranmeldeinformationen verweist, die für die Authentifizierung und Autorisierung benötigt werden.

Um einen Realm zu erstellen, müssen Sie nur die Realm -Schnittstelle implementieren. Das kann langweilig sein; Das Framework verfügt jedoch über Standardimplementierungen, aus denen wir Unterklassen erstellen können. Eine dieser Implementierungen ist JdbcRealm .

Wir erstellen eine benutzerdefinierte Realm-Implementierung, die die Klasse JdbcRealm erweitert und die folgenden Methoden überschreibt: doGetAuthenticationInfo () , doGetAuthorizationInfo () , getRoleNamesForUser () und getPermissions () .

Erstellen Sie einen Bereich, indem Sie die Klasse JdbcRealm unterteilen:

public class MyCustomRealm extends JdbcRealm {
   //...
}

Der Einfachheit halber verwenden wir java.util.Map , um eine Datenbank zu simulieren:

private Map<String, String> credentials = new HashMap<>();
private Map<String, Set<String>> roles = new HashMap<>();
private Map<String, Set<String>> perm = new HashMap<>();

{
    credentials.put("user", "password");
    credentials.put("user2", "password2");
    credentials.put("user3", "password3");

    roles.put("user", new HashSet<>(Arrays.asList("admin")));
    roles.put("user2", new HashSet<>(Arrays.asList("editor")));
    roles.put("user3", new HashSet<>(Arrays.asList("author")));

    perm.put("admin", new HashSet<>(Arrays.asList("** ")));
    perm.put("editor", new HashSet<>(Arrays.asList("articles:** ")));
    perm.put("author",
      new HashSet<>(Arrays.asList("articles:compose",
      "articles:save")));
}

Fortfahren und doGetAuthenticationInfo () überschreiben:

protected AuthenticationInfo
  doGetAuthenticationInfo(AuthenticationToken token)
  throws AuthenticationException {

    UsernamePasswordToken uToken = (UsernamePasswordToken) token;

    if(uToken.getUsername() == null
      || uToken.getUsername().isEmpty()
      || !credentials.containsKey(uToken.getUsername())) {
          throw new UnknownAccountException("username not found!");
    }

    return new SimpleAuthenticationInfo(
      uToken.getUsername(),
      credentials.get(uToken.getUsername()),
      getName());
}

Wir konvertieren zuerst das AuthenticationToken , das in UsernamePasswordToken bereitgestellt wird. Aus dem uToken extrahieren wir den Benutzernamen ( uToken.getUsername () ) und verwenden ihn, um die Anmeldeinformationen (Kennwort) des Benutzers aus der Datenbank zu erhalten.

Wenn kein Datensatz gefunden wird, werfen wir eine UnknownAccountException aus. Andernfalls verwenden wir die Anmeldeinformationen und den Benutzernamen, um ein SimpleAuthenticatioInfo -Objekt zu erstellen, das von der Methode zurückgegeben wird.

Wenn der Benutzer-Berechtigungsnachweis mit einem Salt gehashed wird, müssen wir eine SimpleAuthenticationInfo mit dem zugehörigen Salt zurückgeben:

return new SimpleAuthenticationInfo(
  uToken.getUsername(),
  credentials.get(uToken.getUsername()),
  ByteSource.Util.bytes("salt"),
  getName()
);

Wir müssen auch doGetAuthorizationInfo () sowie getRoleNamesForUser () und getPermissions () überschreiben.

Als letztes fügen wir den benutzerdefinierten Bereich in den securityManager ein. Alles, was wir tun müssen, ist, den IniRealm oben durch unseren benutzerdefinierten Realm zu ersetzen und ihn an den Konstruktor DefaultSecurityManager zu übergeben:

Realm realm = new MyCustomRealm();
SecurityManager securityManager = new DefaultSecurityManager(realm);

Jeder andere Teil des Codes ist derselbe wie zuvor. Dies ist alles, was wir brauchen, um den securityManager mit einem benutzerdefinierten Bereich richtig zu konfigurieren.

Nun stellt sich die Frage: Wie passt das Framework zu den Anmeldeinformationen?

Standardmäßig verwendet JdbcRealm den SimpleCredentialsMatcher , der lediglich die Gleichheit prüft, indem er die Anmeldeinformationen im AuthenticationToken und AuthenticationInfo vergleicht.

Wenn wir unsere Passwörter verschlüsseln, müssen wir das Framework informieren, stattdessen einen HashedCredentialsMatcher zu verwenden. Die INI-Konfigurationen für Realms mit gehashten Kennwörtern finden Sie unter https://shiro.apache.org/realm.html#Realm-HashingCredentials (hier).

7. Ausloggen

Nachdem wir den Benutzer authentifiziert haben, ist es an der Zeit, die Abmeldung zu implementieren.

Dies geschieht einfach durch Aufrufen einer einzelnen Methode. Dadurch wird die Benutzersitzung ungültig und der Benutzer wird abgemeldet:

currentUser.logout();

8. Sitzungsmanagement

Das Framework wird natürlich mit einem Session-Management-System geliefert. Bei Verwendung in einer Webumgebung wird standardmäßig die Implementierung von HttpSession verwendet.

Für eine eigenständige Anwendung wird das Sitzungsverwaltungssystem für Unternehmen verwendet. Der Vorteil ist, dass Sie selbst in einer Desktopumgebung ein Sitzungsobjekt wie in einer typischen Webumgebung verwenden können.

Schauen wir uns ein schnelles Beispiel an und interagieren mit der Sitzung des aktuellen Benutzers:

Session session = currentUser.getSession();
session.setAttribute("key", "value");
String value = (String) session.getAttribute("key");
if (value.equals("value")) {
    log.info("Retrieved the correct value![" + value + "]");
}

9. Shiro für eine Webanwendung mit Spring

Bisher haben wir die grundlegende Struktur von Apache Shiro beschrieben und in einer Desktop-Umgebung implementiert. Fahren wir fort, indem wir das Framework in eine Spring Boot-Anwendung integrieren.

Beachten Sie, dass der Schwerpunkt hier auf Shiro liegt, nicht auf der Spring-Anwendung. Wir werden dies nur für eine einfache Beispielanwendung verwenden.

9.1. Abhängigkeiten

Zuerst müssen wir die übergeordnete Spring Boot-Abhängigkeit zu unserer pom.xml hinzufügen:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.2.RELEASE</version>
</parent>

Als Nächstes müssen wir die folgenden Abhängigkeiten in dieselbe pom.xml -Datei einfügen:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>${apache-shiro-core-version}</version>
</dependency>

9.2. Aufbau

Durch das Hinzufügen der Abhängigkeit von shiro-spring-boot-web-starter zu unserer pom.xml werden standardmäßig einige Funktionen der Apache Shiro-Anwendung konfiguriert, beispielsweise der SecurityManager .

Die Sicherheitsfilter Realm und Shiro müssen jedoch noch konfiguriert werden. Wir werden den gleichen benutzerdefinierten Bereich verwenden, der oben definiert wurde.

Fügen Sie in der Hauptklasse, in der die Spring Boot-Anwendung ausgeführt wird, die folgenden Bean -Definitionen hinzu:

@Bean
public Realm realm() {
    return new MyCustomRealm();
}

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter
      = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/secure", "authc");
    filter.addPathDefinition("/** ** ", "anon");

    return filter;
}

In ShiroFilterChainDefinition haben wir den authc -Filter auf /secure -Pfad angewendet und den anon -Filter auf andere Pfade mit dem Ant-Muster angewendet.

Sowohl authc - als auch anon -Filter werden standardmäßig für Webanwendungen mitgeliefert. Andere Standardfilter finden Sie unter hier .

Wenn wir das Realm -Bean nicht definiert haben, stellt ShiroAutoConfiguration standardmäßig eine IniRealm -Implementierung bereit, die erwartet, dass eine shiro.ini -Datei in src/main/resources oder src/main/resources/META-INF. gefunden wird.

Wenn wir keine ShiroFilterChainDefinition -Bean definieren, sichert das Framework alle Pfade und setzt die Anmelde-URL als login.jsp .

Wir können diese Standard-Login-URL und andere Standardeinstellungen ändern, indem Sie unserer application.properties die folgenden Einträge hinzufügen:

shiro.loginUrl =/login
shiro.successUrl =/secure
shiro.unauthorizedUrl =/login

Nachdem der authc -Filter auf /secure angewendet wurde, erfordern alle Anforderungen an diese Route eine Formularauthentifizierung.

9.3. Authentifizierung und Autorisierung

Erstellen wir einen ShiroSpringController mit den folgenden Pfadzuordnungen:

/index , /login,/logout und /secure.

Mit der Methode login () implementieren wir die tatsächliche Benutzerauthentifizierung wie oben beschrieben. Wenn die Authentifizierung erfolgreich ist, wird der Benutzer zur sicheren Seite weitergeleitet:

Subject subject = SecurityUtils.getSubject();

if(!subject.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(
      cred.getUsername(), cred.getPassword(), cred.isRememberMe());
    try {
        subject.login(token);
    } catch (AuthenticationException ae) {
        ae.printStackTrace();
        attr.addFlashAttribute("error", "Invalid Credentials");
        return "redirect:/login";
    }
}

return "redirect:/secure";

In der secure () -Implementierung wurde der currentUser jetzt durch Aufrufen von __SecurityUtils.getSubject () abgerufen. Die Rolle und die Berechtigungen des Benutzers werden an die sichere Seite sowie den Principal des Benutzers übergeben:

Subject currentUser = SecurityUtils.getSubject();
String role = "", permission = "";

if(currentUser.hasRole("admin")) {
    role = role  + "You are an Admin";
} else if(currentUser.hasRole("editor")) {
    role = role + "You are an Editor";
} else if(currentUser.hasRole("author")) {
    role = role + "You are an Author";
}

if(currentUser.isPermitted("articles:compose")) {
    permission = permission + "You can compose an article, ";
} else {
    permission = permission + "You are not permitted to compose an article!, ";
}

if(currentUser.isPermitted("articles:save")) {
    permission = permission + "You can save articles, ";
} else {
    permission = permission + "\nYou can not save articles, ";
}

if(currentUser.isPermitted("articles:publish")) {
    permission = permission  + "\nYou can publish articles";
} else {
    permission = permission + "\nYou can not publish articles";
}

modelMap.addAttribute("username", currentUser.getPrincipal());
modelMap.addAttribute("permission", permission);
modelMap.addAttribute("role", role);

return "secure";
  • Und wir sind fertig. ** So können wir Apache Shiro in eine Spring Boot-Anwendung integrieren.

Beachten Sie außerdem, dass das Framework zusätzliche annotations bietet, die neben den Definitionen der Filterkette zum Schutz unserer Anwendung verwendet werden können.

  1. JEE-Integration

Um Apache Shiro in eine JEE-Anwendung zu integrieren, müssen Sie nur die Datei web.xml konfigurieren. Wie üblich erwartet die Konfiguration, dass shiro.ini sich im Klassenpfad befindet. Eine detaillierte Beispielkonfiguration ist verfügbar unter hier . Die JSP-Tags finden Sie auch unter https://shiro.apache.org/web.html#Web-JSP%2FGSPTagLibrary (hier).

11. Fazit

In diesem Tutorial haben wir uns die Authentifizierungs- und Autorisierungsmechanismen von Apache Shiro angesehen. Wir haben uns auch darauf konzentriert, wie Sie einen benutzerdefinierten Bereich definieren und ihn in den SecurityManager einfügen.

Der komplette Quellcode ist wie immer verfügbar: über GitHub .