Keamanan Musim Semi vs Apache Shiro

1. Ikhtisar

Keamanan merupakan perhatian utama dalam dunia pengembangan aplikasi, khususnya di bidang web perusahaan dan aplikasi seluler.

Dalam tutorial singkat ini, kami akan membandingkan dua framework Keamanan Java yang populer - Apache Shiro dan Spring Security .

2. Sedikit Latar Belakang

Apache Shiro lahir pada tahun 2004 sebagai JSecurity dan diterima oleh Apache Foundation pada tahun 2008. Hingga saat ini, telah melihat banyak rilis, yang terbaru adalah 1.5.3.

Keamanan Musim Semi dimulai sebagai Acegi pada tahun 2003 dan dimasukkan ke dalam Kerangka Kerja Musim Semi dengan rilis publik pertamanya pada tahun 2008. Sejak awal, Spring Security telah melalui beberapa iterasi dan versi GA saat ini pada saat penulisan ini adalah 5.3.2.

Kedua teknologi tersebut menawarkan dukungan otentikasi dan otorisasi bersama dengan kriptografi dan solusi manajemen sesi . Selain itu, Keamanan Musim Semi memberikan perlindungan kelas satu terhadap serangan seperti CSRF dan fiksasi sesi.

Di beberapa bagian berikutnya, kita akan melihat contoh bagaimana kedua teknologi menangani otentikasi dan otorisasi. Untuk mempermudah, kami akan menggunakan aplikasi MVC berbasis Spring Boot dasar dengan templat FreeMarker.

3. Konfigurasi Apache Shiro

Untuk memulainya, mari kita lihat bagaimana konfigurasi berbeda antara kedua framework.

3.1. Dependensi Maven

Karena kita akan menggunakan Shiro di Aplikasi Spring Boot, 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 dapat ditemukan di Maven Central.

3.2. Menciptakan Realm

Untuk mendeklarasikan pengguna dengan peran dan izinnya di dalam memori, kita perlu membuat ranah yang memperluas JdbcRealm Shiro . Kami akan menentukan dua pengguna - Tom dan Jerry, dengan peran masing-masing 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"))); } }

Selanjutnya, untuk mengaktifkan pengambilan otentikasi dan otorisasi ini, kita perlu mengganti beberapa metode:

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

Metode doGetAuthorizationInfo menggunakan beberapa metode pembantu untuk mendapatkan peran dan izin 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; } 

Selanjutnya, kita perlu memasukkan CustomRealm ini sebagai bean di Aplikasi Boot kita:

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

Selain itu, untuk mengonfigurasi otentikasi 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, dengan menggunakan instance DefaultShiroFilterChainDefinition , kami menetapkan bahwa endpoint / home kami hanya dapat diakses oleh pengguna yang diautentikasi.

Hanya itu yang kami butuhkan untuk konfigurasi, Shiro melakukan sisanya untuk kami.

4. Konfigurasi Keamanan Musim Semi

Sekarang mari kita lihat bagaimana mencapai hal yang sama di Spring.

4.1. Dependensi Maven

Pertama, dependensi:

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

Versi terbaru dapat ditemukan di Maven Central.

4.2. Kelas Konfigurasi

Selanjutnya, kami akan menentukan konfigurasi Keamanan Musim Semi 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 bisa kita lihat, kita membuat objek AuthenticationManagerBuilder untuk mendeklarasikan pengguna kita dengan peran dan otoritas mereka. Selain itu, kami menyandikan sandi menggunakan BCryptPasswordEncoder .

Spring Security juga memberi kita objek HttpSecurity untuk konfigurasi lebih lanjut. Untuk contoh kami, kami mengizinkan:

  • semua orang untuk mengakses halaman indeks dan login kami
  • hanya pengguna yang diautentikasi untuk masuk ke halaman beranda dan keluar
  • hanya pengguna dengan peran ADMIN yang dapat mengakses halaman admin

Kami juga telah menetapkan dukungan untuk otentikasi berbasis formulir untuk mengirim pengguna ke titik akhir login . Jika gagal masuk, pengguna kami akan dialihkan ke / login-error .

5. Pengontrol dan Titik Akhir

Sekarang mari kita lihat pemetaan pengontrol web untuk kedua aplikasi tersebut. Meskipun mereka akan menggunakan titik akhir yang sama, beberapa penerapan akan berbeda.

5.1. Endpoint untuk View Rendering

Untuk titik akhir yang merender tampilan, implementasinya 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 implementasi pengontrol kami, Shiro dan juga Spring Security, mengembalikan index.ftl pada titik akhir root, login.ftl pada titik akhir login, dan home.ftl pada titik akhir beranda.

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

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

Kami baru saja menjelajahi permukaan dari apa yang ditawarkan kerangka kerja ini dan ada banyak hal yang perlu dijelajahi lebih jauh. Ada beberapa alternatif di luar sana seperti JAAS dan OACC. Namun, dengan kelebihannya, Keamanan Musim Semi tampaknya lebih unggul pada saat ini.

Seperti biasa, kode sumber tersedia di GitHub.