Einführung in Apache Shiro

1. Übersicht

In diesem Artikel befassen wir uns mit Apache Shiro, einem vielseitigen Java-Sicherheitsframework.

Das Framework ist hochgradig anpassbar und modular, da es Authentifizierung, Autorisierung, Kryptografie und Sitzungsverwaltung bietet.

2. Abhängigkeit

Apache Shiro hat viele Module. In diesem Tutorial verwenden wir jedoch nur das Shiro-Core- Artefakt.

Fügen wir es unserer pom.xml hinzu :

 org.apache.shiro shiro-core 1.4.0 

Die neueste Version der Apache Shiro-Module finden Sie auf Maven Central.

3. Konfigurieren von Security Manager

Der SecurityManager ist das Herzstück des Apache Shiro-Frameworks. Bei Anwendungen wird normalerweise eine einzelne Instanz ausgeführt.

In diesem Tutorial untersuchen wir das Framework in einer Desktop-Umgebung. Um das Framework zu konfigurieren, müssen wir eine shiro.ini- Datei im Ressourcenordner 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 lautet: p rincipal (Benutzername) = Passwort, Rolle1, Rolle2,…, Rolle .

Die Rollen und die zugehörigen Berechtigungen werden im Abschnitt [Rollen] deklariert . Die Administratorrolle erhält die Berechtigung und den Zugriff auf alle Teile der Anwendung. Dies wird durch das Platzhaltersymbol (*) angezeigt .

Die Editorrolle verfügt über alle Berechtigungen, die mit Artikeln verknüpft sind , während die Autorenrolle nur einen Artikel erstellen und speichern kann .

Der SecurityManager wird zum Konfigurieren der SecurityUtils- Klasse verwendet. Über die SecurityUtils können wir den aktuellen Benutzer abrufen, der mit dem System interagiert, und Authentifizierungs- und Autorisierungsvorgänge ausführen.

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

IniRealm iniRealm = new IniRealm("classpath:shiro.ini"); SecurityManager securityManager = new DefaultSecurityManager(iniRealm); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject();

Nachdem wir nun einen SecurityManager haben , der die in der Datei shiro.ini definierten Benutzeranmeldeinformationen und -rollen kennt , fahren wir mit der Benutzerauthentifizierung und -autorisierung fort .

4. Authentifizierung

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

Der Aufruf SecurityUtils.getSubject () gibt eine Instanz des aktuellen Themas , das das ist current .

Nachdem wir das aktuelle Benutzerobjekt 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); } }

Zunächst prüfen wir, ob der aktuelle Benutzer noch nicht authentifiziert wurde. Anschließend erstellen wir ein Authentifizierungstoken mit dem Prinzipal (Benutzername) und dem Berechtigungsnachweis (Kennwort) des Benutzers .

Als nächstes versuchen wir, uns mit dem Token anzumelden. 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 Anwendungsanforderungen besser entspricht. Dies kann durch Unterklassen der AccountException- Klasse erfolgen.

5. Autorisierung

Bei der Authentifizierung wird versucht, die Identität eines Benutzers zu überprüfen, während bei der Autorisierung versucht wird, 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 Abschnitt Rollen unterschiedliche Berechtigungen oder Zugriffsebenen für jede Rolle.

Lassen Sie uns nun sehen, wie wir dies in unserer Anwendung verwenden können, um die Benutzerzugriffskontrolle zu erzwingen.

In der Datei shiro.ini gewähren wir dem Administrator vollständigen Zugriff auf alle Teile des Systems.

Der Herausgeber hat vollständigen Zugriff auf alle Ressourcen / Vorgänge in Bezug auf Artikel , und ein Autor kann nur Artikel verfassen und speichern .

Lassen Sie uns den aktuellen Benutzer basierend auf seiner 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, das auf einen Speicher mit Benutzeranmeldeinformationen verweist, die für die Authentifizierung und Autorisierung erforderlich sind.

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

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

Erstellen wir einen Realm, indem wir die JdbcRealm- Klasse in Unterklassen unterteilen :

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

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

private Map credentials = new HashMap(); private Map
    
      roles = new HashMap(); private Map
     
       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"))); }
     
    

Fahren wir fort und überschreiben die doGetAuthenticationInfo () :

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

We first cast the AuthenticationToken provided to UsernamePasswordToken. From the uToken, we extract the username (uToken.getUsername()) and use it to get the user credentials (password) from the database.

If no record is found – we throw an UnknownAccountException, else we use the credential and username to construct a SimpleAuthenticatioInfo object that's returned from the method.

If the user credential is hashed with a salt, we need to return a SimpleAuthenticationInfo with the associated salt:

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

We also need to override the doGetAuthorizationInfo(), as well as getRoleNamesForUser() and getPermissions().

Finally, let's plug the custom realm into the securityManager. All we need to do is replace the IniRealm above with our custom realm, and pass it to the DefaultSecurityManager‘s constructor:

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

Every other part of the code is the same as before. This is all we need to configure the securityManager with a custom realm properly.

Now the question is – how does the framework match the credentials?

By default, the JdbcRealm uses the SimpleCredentialsMatcher, which merely checks for equality by comparing the credentials in the AuthenticationToken and the AuthenticationInfo.

If we hash our passwords, we need to inform the framework to use a HashedCredentialsMatcher instead. The INI configurations for realms with hashed passwords can be found here.

7. Logging Out

Now that we've authenticated the user, it's time to implement log out. That's done simply by calling a single method – which invalidates the user session and logs the user out:

currentUser.logout();

8. Session Management

The framework naturally comes with its session management system. If used in a web environment, it defaults to the HttpSession implementation.

For a standalone application, it uses its enterprise session management system. The benefit is that even in a desktop environment you can use a session object as you would do in a typical web environment.

Let's have a look at a quick example and interact with the session of the current user:

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 for a Web Application With Spring

So far we've outlined the basic structure of Apache Shiro and we have implemented it in a desktop environment. Let's proceed by integrating the framework into a Spring Boot application.

Note that the main focus here is Shiro, not the Spring application – we're only going to use that to power a simple example app.

9.1. Dependencies

First, we need to add the Spring Boot parent dependency to our pom.xml:

 org.springframework.boot spring-boot-starter-parent 2.2.6.RELEASE 

Next, we have to add the following dependencies to the same pom.xml file:

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-freemarker   org.apache.shiro shiro-spring-boot-web-starter ${apache-shiro-core-version} 

9.2. Configuration

Adding the shiro-spring-boot-web-starter dependency to our pom.xml will by default configure some features of the Apache Shiro application such as the SecurityManager.

However, we still need to configure the Realm and Shiro security filters. We will be using the same custom realm defined above.

And so, in the main class where the Spring Boot application is run, let's add the following Bean definitions:

@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 the ShiroFilterChainDefinition, we applied the authc filter to /secure path and applied the anon filter on other paths using the Ant pattern.

Both authc and anon filters come along by default for web applications. Other default filters can be found here.

If we did not define the Realm bean, ShiroAutoConfiguration will, by default, provide an IniRealm implementation that expects to find a shiro.ini file in src/main/resources or src/main/resources/META-INF.

If we do not define a ShiroFilterChainDefinition bean, the framework secures all paths and sets the login URL as login.jsp.

We can change this default login URL and other defaults by adding the following entries to our application.properties:

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

Now that the authc filter has been applied to /secure, all requests to that route will require a form authentication.

9.3. Authentication and Authorization

Let's create a ShiroSpringController with the following path mappings: /index, /login, /logout and /secure.

The login() method is where we implement actual user authentication as described above. If authentication is successful, the user is redirected to the secure page:

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

And now in the secure() implementation, the currentUser was obtained by invoking the SecurityUtils.getSubject(). The role and permissions of the user are passed on to the secure page, as well the user's principal:

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

And we're done. That's how we can integrate Apache Shiro into a Spring Boot Application.

Also, note that the framework offers additional annotations that can be used alongside filter chain definitions to secure our application.

10. JEE Integration

Die Integration von Apache Shiro in eine JEE-Anwendung ist nur eine Frage der Konfiguration der Datei web.xml . Wie üblich erwartet die Konfiguration, dass sich shiro.ini im Klassenpfad befindet. Eine detaillierte Beispielkonfiguration finden Sie hier. Die JSP-Tags finden Sie auch hier.

11. Schlussfolgerung

In diesem Tutorial haben wir uns die Authentifizierungs- und Autorisierungsmechanismen des Apache Shiro angesehen. Wir haben uns auch darauf konzentriert, wie ein benutzerdefinierter Bereich definiert und in den SecurityManager eingebunden wird .

Wie immer ist der vollständige Quellcode auf GitHub verfügbar.