Spring Security vs Apache Shiro

1. Gambaran keseluruhan

Keselamatan adalah perhatian utama dalam dunia pengembangan aplikasi, terutama di bidang aplikasi web dan aplikasi mudah alih.

Dalam tutorial ringkas ini, kami akan membandingkan dua kerangka kerja Java Security yang popular - Apache Shiro dan Spring Security .

2. Latar Belakang Sedikit

Apache Shiro dilahirkan pada tahun 2004 sebagai JSecurity dan diterima oleh Yayasan Apache pada tahun 2008. Sehingga kini, ia telah menyaksikan banyak siaran, yang terbaru adalah 1.5.3.

Spring Security bermula sebagai Acegi pada tahun 2003 dan dimasukkan ke dalam Spring Framework dengan siaran awam pertamanya pada tahun 2008. Sejak ditubuhkan, ia telah melalui beberapa lelaran dan versi GA semasa ketika menulis ini adalah 5.3.2.

Kedua-dua teknologi ini menawarkan sokongan pengesahan dan kebenaran bersama dengan kriptografi dan penyelesaian pengurusan sesi . Selain itu, Spring Security memberikan perlindungan kelas pertama terhadap serangan seperti CSRF dan penetapan sesi.

Dalam beberapa bahagian seterusnya, kita akan melihat contoh bagaimana kedua-dua teknologi mengendalikan pengesahan dan kebenaran. Untuk mempermudah, kami akan menggunakan aplikasi MVC berasaskan Spring Boot asas dengan templat FreeMarker.

3. Mengkonfigurasi Apache Shiro

Sebagai permulaan, mari kita lihat bagaimana konfigurasi berbeza antara dua kerangka kerja.

3.1. Ketergantungan Maven

Oleh kerana kita akan menggunakan Shiro di Spring Boot App, kita memerlukan starter dan modul shiro-core :

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

Versi terbaru boleh didapati di Maven Central.

3.2. Menciptakan Alam

Untuk mengisytiharkan pengguna dengan peranan dan izin mereka dalam ingatan, kita perlu membuat ranah yang memperluas Shd 's JdbcRealm . Kami akan menentukan dua pengguna - Tom dan Jerry, masing-masing dengan peranan USER dan 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"))); } }

Seterusnya, untuk membolehkan pengambilan pengesahan dan pengesahan ini, kita perlu mengganti beberapa kaedah:

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

Kaedah doGetAuthorizationInfo menggunakan beberapa kaedah penolong untuk mendapatkan peranan dan kebenaran pengguna:

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

Seterusnya, kita perlu memasukkan CustomRealm ini sebagai kacang dalam Aplikasi Boot kami:

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

Selain itu, untuk mengkonfigurasi pengesahan untuk titik akhir kami, kami memerlukan kacang lain:

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

Di sini, menggunakan contoh DefaultShiroFilterChainDefinition , kami menetapkan bahawa titik akhir / rumah kami hanya dapat diakses oleh pengguna yang disahkan.

Itu sahaja yang kami perlukan untuk konfigurasi, Shiro melakukan yang lain untuk kami.

4. Mengkonfigurasi Keselamatan Musim Semi

Sekarang mari kita lihat bagaimana mencapainya pada musim bunga.

4.1. Ketergantungan Maven

Pertama, kebergantungan:

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

Versi terbaru boleh didapati di Maven Central.

4.2. Kelas Konfigurasi

Seterusnya, kami akan menentukan konfigurasi Spring Security kami di kelas SecurityConfig , memperluas 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(); } } 

Seperti yang kita lihat, kami membina objek AuthenticationManagerBuilder untuk menyatakan pengguna kami dengan peranan dan pihak berkuasa mereka. Selain itu, kami mengekodkan kata laluan menggunakan BCryptPasswordEncoder .

Spring Security juga memberi kami objek HttpSecurity untuk konfigurasi lebih lanjut. Sebagai contoh, kami telah membenarkan:

  • semua orang untuk mengakses halaman indeks dan log masuk kami
  • hanya pengguna yang disahkan untuk memasuki laman utama dan log keluar
  • hanya pengguna dengan peranan ADMIN untuk mengakses halaman pentadbir

Kami juga telah menentukan sokongan untuk pengesahan berasaskan borang untuk menghantar pengguna ke titik akhir log masuk . Sekiranya login gagal, pengguna kami akan dialihkan ke / login-error .

5. Pengawal dan Titik Akhir

Sekarang mari kita lihat pemetaan pengawal web kami untuk kedua-dua aplikasi tersebut. Walaupun mereka akan menggunakan titik akhir yang sama, beberapa pelaksanaan akan berbeza.

5.1. Titik Akhir untuk Pemaparan Paparan

Untuk titik akhir yang memberikan pandangan, pelaksanaannya sama:

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

Kedua-dua pelaksanaan pengawal kami, Shiro dan juga Spring Security, mengembalikan index.ftl pada titik akhir root, login.ftl pada titik akhir masuk, dan home.ftl pada titik akhir rumah.

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. Kesimpulannya

Dalam tutorial ini, kami membandingkan Apache Shiro dengan Spring Security .

Kami baru sahaja melihat permukaan kerangka kerja apa yang ditawarkan dan masih banyak yang perlu diterokai lebih lanjut. Terdapat beberapa alternatif di luar sana seperti JAAS dan OACC. Namun, dengan kelebihannya, Spring Security nampaknya menang pada ketika ini.

Seperti biasa, kod sumber tersedia di GitHub.