Spring Security contro Apache Shiro

1. Panoramica

La sicurezza è una preoccupazione primaria nel mondo dello sviluppo di applicazioni, in particolare nell'area delle applicazioni web e mobili aziendali.

In questo breve tutorial, confronteremo due popolari framework di Java Security: Apache Shiro e Spring Security .

2. Un po 'di storia

Apache Shiro è nato nel 2004 come JSecurity ed è stato accettato dalla Apache Foundation nel 2008. Ad oggi, ha visto molte versioni, l'ultima al momento della scrittura è la 1.5.3.

Spring Security è nata come Acegi nel 2003 ed è stata incorporata nello Spring Framework con la sua prima versione pubblica nel 2008. Sin dal suo inizio, ha subito diverse iterazioni e l'attuale versione GA al momento della stesura è la 5.3.2.

Entrambe le tecnologie offrono supporto per l'autenticazione e l'autorizzazione insieme a soluzioni di crittografia e gestione delle sessioni . Inoltre, Spring Security fornisce una protezione di prima classe contro attacchi come CSRF e riparazione di sessioni.

Nelle prossime sezioni vedremo esempi di come le due tecnologie gestiscono l'autenticazione e l'autorizzazione. Per mantenere le cose semplici, utilizzeremo le applicazioni MVC di base basate su Spring Boot con i modelli FreeMarker.

3. Configurazione di Apache Shiro

Per cominciare, vediamo come differiscono le configurazioni tra i due framework.

3.1. Dipendenze di Maven

Dato che useremo Shiro in un'app Spring Boot, avremo bisogno del suo starter e del modulo shiro-core :

 org.apache.shiro shiro-spring-boot-web-starter 1.5.3   org.apache.shiro shiro-core 1.5.3 

Le ultime versioni possono essere trovate su Maven Central.

3.2. Creazione di un regno

Per dichiarare gli utenti con i loro ruoli e permessi in memoria, dobbiamo creare un regno che estenda JdbcRealm di Shiro . Definiremo due utenti: Tom e Jerry, rispettivamente con i ruoli USER e ADMIN:

public class CustomRealm extends JdbcRealm { private Map credentials = new HashMap(); private Map roles = new HashMap(); private Map permissions = new HashMap(); { credentials.put("Tom", "password"); credentials.put("Jerry", "password"); roles.put("Jerry", new HashSet(Arrays.asList("ADMIN"))); roles.put("Tom", new HashSet(Arrays.asList("USER"))); permissions.put("ADMIN", new HashSet(Arrays.asList("READ", "WRITE"))); permissions.put("USER", new HashSet(Arrays.asList("READ"))); } }

Successivamente, per consentire il recupero di questa autenticazione e autorizzazione, dobbiamo sovrascrivere alcuni metodi:

@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken userToken = (UsernamePasswordToken) token; if (userToken.getUsername() == null || userToken.getUsername().isEmpty() || !credentials.containsKey(userToken.getUsername())) { throw new UnknownAccountException("User doesn't exist"); } return new SimpleAuthenticationInfo(userToken.getUsername(), credentials.get(userToken.getUsername()), getName()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Set roles = new HashSet(); Set permissions = new HashSet(); for (Object user : principals) { try { roles.addAll(getRoleNamesForUser(null, (String) user)); permissions.addAll(getPermissions(null, null, roles)); } catch (SQLException e) { logger.error(e.getMessage()); } } SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(roles); authInfo.setStringPermissions(permissions); return authInfo; } 

Il metodo doGetAuthorizationInfo utilizza un paio di metodi di supporto per ottenere i ruoli e le autorizzazioni dell'utente:

@Override protected Set getRoleNamesForUser(Connection conn, String username) throws SQLException { if (!roles.containsKey(username)) { throw new SQLException("User doesn't exist"); } return roles.get(username); } @Override protected Set getPermissions(Connection conn, String username, Collection roles) throws SQLException { Set userPermissions = new HashSet(); for (String role : roles) { if (!permissions.containsKey(role)) { throw new SQLException("Role doesn't exist"); } userPermissions.addAll(permissions.get(role)); } return userPermissions; } 

Successivamente, dobbiamo includere questo CustomRealm come bean nella nostra applicazione di avvio:

@Bean public Realm customRealm() { return new CustomRealm(); }

Inoltre, per configurare l'autenticazione per i nostri endpoint, abbiamo bisogno di un altro bean:

@Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition(); filter.addPathDefinition("/home", "authc"); filter.addPathDefinition("/**", "anon"); return filter; }

Qui, utilizzando un'istanza DefaultShiroFilterChainDefinition , abbiamo specificato che il nostro endpoint / home è accessibile solo agli utenti autenticati.

Questo è tutto ciò di cui abbiamo bisogno per la configurazione, Shiro fa il resto per noi.

4. Configurazione di Spring Security

Ora vediamo come ottenere lo stesso risultato in primavera.

4.1. Dipendenze di Maven

Innanzitutto, le dipendenze:

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-security 

Le ultime versioni possono essere trovate su Maven Central.

4.2. Classe di configurazione

Successivamente, definiremo la nostra configurazione Spring Security in una classe SecurityConfig , estendendo WebSecurityConfigurerAdapter :

@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authorize -> authorize .antMatchers("/index", "/login").permitAll() .antMatchers("/home", "/logout").authenticated() .antMatchers("/admin/**").hasRole("ADMIN")) .formLogin(formLogin -> formLogin .loginPage("/login") .failureUrl("/login-error")); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("Jerry") .password(passwordEncoder().encode("password")) .authorities("READ", "WRITE") .roles("ADMIN") .and() .withUser("Tom") .password(passwordEncoder().encode("password")) .authorities("READ") .roles("USER"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } 

Come possiamo vedere, abbiamo creato un oggetto AuthenticationManagerBuilder per dichiarare i nostri utenti con i loro ruoli e autorità. Inoltre, abbiamo codificato le password utilizzando BCryptPasswordEncoder .

Spring Security ci fornisce anche il suo oggetto HttpSecurity per ulteriori configurazioni. Per il nostro esempio, abbiamo consentito:

  • tutti per accedere al nostro indice e alle pagine di accesso
  • solo gli utenti autenticati per accedere alla home page e disconnettersi
  • solo gli utenti con ruolo ADMIN per accedere alle pagine di amministrazione

Abbiamo anche definito il supporto per l'autenticazione basata su form per inviare gli utenti all'endpoint di accesso . Nel caso in cui il login fallisca, i nostri utenti verranno reindirizzati a / login-error .

5. Controller ed endpoint

Ora diamo uno sguardo alle mappature del nostro web controller per le due applicazioni. Sebbene utilizzeranno gli stessi endpoint, alcune implementazioni saranno diverse.

5.1. Endpoint per il rendering della vista

Per gli endpoint che rendono la vista, le implementazioni sono le stesse:

@GetMapping("/") public String index() { return "index"; } @GetMapping("/login") public String showLoginPage() { return "login"; } @GetMapping("/home") public String getMeHome(Model model) { addUserAttributes(model); return "home"; }

Entrambe le implementazioni del nostro controller, Shiro e Spring Security, restituiscono index.ftl sull'endpoint root, login.ftl sull'endpoint di accesso e home.ftl sull'endpoint home.

However, the definition of the method addUserAttributes at the /home endpoint will differ between the two controllers. This method introspects the currently logged in user's attributes.

Shiro provides a SecurityUtils#getSubject to retrieve the current Subject, and its roles and permissions:

private void addUserAttributes(Model model) { Subject currentUser = SecurityUtils.getSubject(); String permission = ""; if (currentUser.hasRole("ADMIN")) { model.addAttribute("role", "ADMIN"); } else if (currentUser.hasRole("USER")) { model.addAttribute("role", "USER"); } if (currentUser.isPermitted("READ")) { permission = permission + " READ"; } if (currentUser.isPermitted("WRITE")) { permission = permission + " WRITE"; } model.addAttribute("username", currentUser.getPrincipal()); model.addAttribute("permission", permission); }

On the other hand, Spring Security provides an Authentication object from its SecurityContextHolder‘s context for this purpose:

private void addUserAttributes(Model model) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && !auth.getClass().equals(AnonymousAuthenticationToken.class)) { User user = (User) auth.getPrincipal(); model.addAttribute("username", user.getUsername()); Collection authorities = user.getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().contains("USER")) { model.addAttribute("role", "USER"); model.addAttribute("permissions", "READ"); } else if (authority.getAuthority().contains("ADMIN")) { model.addAttribute("role", "ADMIN"); model.addAttribute("permissions", "READ WRITE"); } } } }

5.2. POST Login Endpoint

In Shiro, we map the credentials the user enters to a POJO:

public class UserCredentials { private String username; private String password; // getters and setters }

Then we'll create a UsernamePasswordToken to log the user, or Subject, in:

@PostMapping("/login") public String doLogin(HttpServletRequest req, UserCredentials credentials, RedirectAttributes attr) { Subject subject = SecurityUtils.getSubject(); if (!subject.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken(credentials.getUsername(), credentials.getPassword()); try { subject.login(token); } catch (AuthenticationException ae) { logger.error(ae.getMessage()); attr.addFlashAttribute("error", "Invalid Credentials"); return "redirect:/login"; } } return "redirect:/home"; }

On the Spring Security side, this is just a matter of redirection to the home page. Spring's logging-in process, handled by its UsernamePasswordAuthenticationFilter, is transparent to us:

@PostMapping("/login") public String doLogin(HttpServletRequest req) { return "redirect:/home"; }

5.3. Admin-Only Endpoint

Now let's look at a scenario where we have to perform role-based access. Let's say we have an /admin endpoint, access to which should only be allowed for the ADMIN role.

Let's see how to do this in Shiro:

@GetMapping("/admin") public String adminOnly(ModelMap modelMap) { addUserAttributes(modelMap); Subject currentUser = SecurityUtils.getSubject(); if (currentUser.hasRole("ADMIN")) { modelMap.addAttribute("adminContent", "only admin can view this"); } return "home"; }

Here we extracted the currently logged in user, checked if they have the ADMIN role, and added content accordingly.

In Spring Security, there is no need for checking the role programmatically, we've already defined who can reach this endpoint in our SecurityConfig. So now, it's just a matter of adding business logic:

@GetMapping("/admin") public String adminOnly(HttpServletRequest req, Model model) { addUserAttributes(model); model.addAttribute("adminContent", "only admin can view this"); return "home"; }

5.4. Logout Endpoint

Finally, let's implement the logout endpoint.

In Shiro, we'll simply call Subject#logout:

@PostMapping("/logout") public String logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "redirect:/"; }

For Spring, we've not defined any mapping for logout. In this case, its default logout mechanism kicks in, which is automatically applied since we extended WebSecurityConfigurerAdapter in our configuration.

6. Apache Shiro vs Spring Security

Now that we've looked at the implementation differences, let's look at a few other aspects.

In terms of community support, the Spring Framework in general has a huge community of developers, actively involved in its development and usage. Since Spring Security is part of the umbrella, it must enjoy the same advantages. Shiro, though popular, does not have such humongous support.

Concerning documentation, Spring again is the winner.

However, there's a bit of a learning curve associated with Spring Security. Shiro, on the other hand, is easy to understand. For desktop applications, configuration via shiro.ini is all the easier.

But again, as we saw in our example snippets, Spring Security does a great job of keeping business logic and securityseparate and truly offers security as a cross-cutting concern.

7. Conclusione

In questo tutorial, abbiamo confrontato Apache Shiro con Spring Security .

Abbiamo appena sfiorato la superficie di ciò che questi framework hanno da offrire e c'è ancora molto da esplorare. Ci sono alcune alternative là fuori come JAAS e OACC. Tuttavia, con i suoi vantaggi, Spring Security sembra vincere a questo punto.

Come sempre, il codice sorgente è disponibile su GitHub.