Pengenalan kepada Apache Shiro

1. Gambaran keseluruhan

Dalam artikel ini, kita akan melihat Apache Shiro, kerangka keselamatan Java yang serba boleh.

Rangka kerja ini sangat disesuaikan dan modular, kerana ia menawarkan pengesahan, kebenaran, kriptografi dan pengurusan sesi.

2. Kebergantungan

Apache Shiro mempunyai banyak modul. Walau bagaimanapun, dalam tutorial ini, kami menggunakan artifak shiro-core sahaja.

Mari tambahkan ke pom.xml kami :

 org.apache.shiro shiro-core 1.4.0 

Versi terbaru modul Apache Shiro boleh didapati di Maven Central.

3. Mengkonfigurasi Pengurus Keselamatan

The SecurityManager adalah sekeping pusat rangka kerja Apache Shiro ini. Aplikasi biasanya akan mempunyai satu contoh ia berjalan.

Dalam tutorial ini, kita meneroka kerangka dalam persekitaran desktop. Untuk mengkonfigurasi rangka kerja, kita perlu membuat fail shiro.ini dalam folder sumber dengan kandungan berikut:

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

Bahagian [pengguna] pada fail konfigurasi shiro.ini menentukan kelayakan pengguna yang dikenali oleh SecurityManager . Formatnya adalah: p rincipal (username) = kata laluan, role1, role2,…, role .

Peranan dan kebenaran yang berkaitan dinyatakan di bahagian [peranan] . The admin peranan diberikan kebenaran dan akses ke setiap bahagian permohonan. Ini ditunjukkan oleh simbol wildcard (*) .

The editor peranan mempunyai semua kebenaran yang berkaitan dengan artikel manakala pengarang peranan hanya boleh mengarang dan menyimpan artikel.

The SecurityManager digunakan untuk mengkonfigurasi SecurityUtils kelas. Dari SecurityUtils kita dapat memperoleh pengguna semasa berinteraksi dengan sistem dan melakukan operasi pengesahan dan kebenaran.

Mari gunakan IniRealm untuk memuatkan definisi pengguna dan peranan kami dari fail shiro.ini dan kemudian menggunakannya untuk mengkonfigurasi objek DefaultSecurityManager :

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

Sekarang kita mempunyai SecurityManager yang mengetahui kelayakan dan peranan pengguna yang ditentukan dalam fail shiro.ini , mari lanjutkan ke pengesahan dan keizinan pengguna.

4. Pengesahan

Dalam terminologi Apache Shiro, Subjek adalah entiti yang berinteraksi dengan sistem. Ini mungkin manusia, naskah, atau Pelanggan REST.

Memanggil SecurityUtils.getSubject () mengembalikan contoh Subjek semasa , iaitu CurrentUser .

Sekarang kita mempunyai currentUser Objek, kita boleh melaksanakan pengesahan pada kelayakan yang dibekalkan:

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

Pertama, kami memeriksa sama ada pengguna semasa belum disahkan. Kemudian kami membuat token pengesahan dengan prinsipal pengguna (nama pengguna) dan kelayakan (kata laluan).

Seterusnya, kami cuba log masuk dengan token. Sekiranya kelayakan yang diberikan betul, semuanya akan berjalan lancar.

Terdapat pengecualian yang berbeza untuk kes yang berbeza. Anda juga boleh membuang pengecualian khusus yang lebih sesuai dengan keperluan aplikasi. Ini dapat dilakukan dengan menundukkan kelas AccountException .

5. Kebenaran

Pengesahan cuba mengesahkan identiti pengguna sementara autoriti berusaha untuk mengawal akses ke sumber tertentu dalam sistem.

Ingatlah bahawa kami memberikan satu atau lebih peranan kepada setiap pengguna yang telah kami buat dalam fail shiro.ini . Selanjutnya, di bahagian peranan, kami menentukan kebenaran atau tahap akses yang berbeza untuk setiap peranan.

Sekarang mari kita lihat bagaimana kita dapat menggunakannya dalam aplikasi kita untuk menegakkan kawalan akses pengguna.

Dalam fail shiro.ini , kami memberi admin akses sepenuhnya ke setiap bahagian sistem.

Editor mempunyai akses penuh ke setiap sumber / operasi yang berkaitan dengan artikel , dan seorang pengarang dibatasi hanya untuk menyusun dan menyimpan artikel saja.

Mari sambut pengguna semasa berdasarkan peranan:

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

Sekarang, mari kita lihat apa yang dibenarkan pengguna semasa dalam sistem:

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. Konfigurasi Realm

Dalam aplikasi sebenar, kita memerlukan kaedah untuk mendapatkan bukti pengguna dari pangkalan data dan bukannya dari fail shiro.ini . Di sinilah konsep Realm mula dimainkan.

Dalam terminologi Apache Shiro, Realm adalah DAO yang menunjuk ke kedai bukti kelayakan pengguna yang diperlukan untuk pengesahan dan kebenaran.

Untuk mencipta dunia, kita hanya perlu melaksanakan antara muka Realm . Itu boleh membosankan; namun, kerangka ini dilengkapi dengan implementasi lalai yang dapat kita subkelas. Salah satu pelaksanaan ini adalah JdbcRealm .

Kami membuat pelaksanaan ranah khas yang memperluas kelas JdbcRealm dan mengatasi kaedah berikut: doGetAuthenticationInfo () , doGetAuthorizationInfo () , getRoleNamesForUser () dan getPermissions () .

Mari buat dunia dengan menundukkan kelas JdbcRealm :

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

Demi kesederhanaan, kami menggunakan java.util.Map untuk mensimulasikan pangkalan data:

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

Mari kita teruskan dan ganti 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

Mengintegrasikan Apache Shiro ke dalam aplikasi JEE hanyalah masalah mengkonfigurasi fail web.xml . Seperti biasa, konfigurasi menjangka shiro.ini berada di laluan kelas. Contoh konfigurasi terperinci boleh didapati di sini. Tag JSP boleh didapati di sini.

11. Kesimpulannya

Dalam tutorial ini, kami melihat mekanisme pengesahan dan kebenaran Apache Shiro. Kami juga memfokuskan pada cara menentukan wilayah khusus dan memasangnya ke SecurityManager .

Seperti biasa, kod sumber lengkap boleh didapati di GitHub.