Spring Security vs Apache Shiro

1. Pregled

Sigurnost je primarna briga u svijetu razvoja aplikacija, posebno na području mrežnih i mobilnih aplikacija poduzeća.

U ovom brzom vodiču, usporedit ćemo dva popularna Java Security okvira - Apache Shiro i Spring Security.

2. Mala pozadina

Apache Shiro rođen je 2004. godine kao JSecurity, a Zaklada Apache prihvatila ga je 2008. Do danas je vidio mnogo izdanja, a najnovije od pisanja je 1.5.3.

Spring Security započeo je kao Acegi 2003. godine, a inkorporiran je u Spring Framework svojim prvim javnim izdanjem 2008. Od svog osnutka prošao je kroz nekoliko ponavljanja, a trenutna verzija GA-a od pisanja ovog teksta je 5.3.2.

Obje tehnologije nude podrška za autentifikaciju i autorizaciju zajedno s rješenjima za kriptografiju i upravljanje sesijama. Uz to, Spring Security pruža prvoklasnu zaštitu od napada poput CSRF-a i fiksiranja sesija.

U sljedećih nekoliko odjeljaka vidjet ćemo primjere kako dvije tehnologije obrađuju autentifikaciju i autorizaciju. Da bi stvari bile jednostavne, koristit ćemo osnovne MVC aplikacije temeljene na Spring Boot-u s FreeMarker predlošcima.

3. Konfiguriranje Apache Shiro

Za početak, pogledajmo kako se konfiguracije razlikuju između dva okvira.

3.1. Ovisnosti Mavena

Budući da ćemo Shiro koristiti u Spring Boot App, trebat će nam njegov pokretač i shiro-core modul:

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

Najnovije verzije možete pronaći na Maven Central.

3.2. Stvaranje carstva

Da bismo deklarirali korisnike s njihovim ulogama i dopuštenjima u memoriji, moramo stvoriti područje koje proširuje Shiro's JdbcRealm. Definirat ćemo dva korisnika - Tom i Jerry, s ulogama KORISNIK i ADMINISTRATOR:

javna klasa CustomRealm proširuje JdbcRealm {vjerodajnice za privatnu mapu = nova HashMap (); uloge privatne mape = novi HashMap (); dozvole privatne karte = novi HashMap (); {vjerodajnice.put ("Tom", "lozinka"); credentials.put ("Jerry", "lozinka"); role.put ("Jerry", novi HashSet (Arrays.asList ("ADMIN"))); role.put ("Tom", novi HashSet (Arrays.asList ("USER"))); permissions.put ("ADMINISTRATOR", novi HashSet (Arrays.asList ("READ", "WRITE"))); permissions.put ("USER", novi HashSet (Arrays.asList ("READ"))); }}

Dalje, da bismo omogućili preuzimanje ove provjere autentičnosti i autorizacije, moramo nadjačati nekoliko metoda:

@Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) baca AuthenticationException {UsernamePasswordToken userToken = (UsernamePasswordToken) token; if (userToken.getUsername () == null || userToken.getUsername (). isEmpty () ||! credentials.containsKey (userToken.getUsername ())) {throw new UnknownAccountException ("Korisnik ne postoji"); } vrati novu SimpleAuthenticationInfo (userToken.getUsername (), credentials.get (userToken.getUsername ()), getName ()); } @Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection Principals) {Postavi uloge = novi HashSet (); Postavi dozvole = novi HashSet (); za (Korisnik objekta: nalogodavci) {try {role.addAll (getRoleNamesForUser (null, (String) user)); permissions.addAll (getPermissions (null, null, uloge)); } catch (SQLException e) {logger.error (e.getMessage ()); }} SimpleAuthorizationInfo authInfo = nova SimpleAuthorizationInfo (uloge); authInfo.setStringPermissions (dozvole); vratiti authInfo; } 

Metoda doGetAuthorizationInfo koristi nekoliko pomoćnih metoda za dobivanje korisnikovih uloga i dozvola:

@Override protected Set getRoleNamesForUser (Connection conn, String username) baca SQLException {if (! Role.containsKey (username)) {throw new SQLException ("Korisnik ne postoji"); } vratiti uloge.get (korisničko ime); } @Override protected Set getPermissions (Connection conn, String username, Collection functions) baca SQLException {Set userPermissions = new HashSet (); for (String role: role) {if (! permissions.containsKey (role)) {throw new SQLException ("Uloga ne postoji"); } userPermissions.addAll (permissions.get (uloga)); } vratiti userPermissions; } 

Dalje, moramo to uključiti CustomRealm kao grah u našoj aplikaciji za pokretanje:

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

Uz to, da bismo konfigurirali provjeru autentičnosti za naše krajnje točke, potreban nam je još jedan grah:

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

Ovdje pomoću a DefaultShiroFilterChainDefinition primjerice, precizirali smo da naš /Dom krajnjoj točki mogu pristupiti samo ovjereni korisnici.

To je sve što nam treba za konfiguraciju, ostalo Shiro radi za nas.

4. Konfiguriranje proljetne sigurnosti

Sada da vidimo kako to postići u proljeće.

4.1. Ovisnosti Mavena

Prvo, ovisnosti:

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

Najnovije verzije možete pronaći na Maven Central.

4.2. Klasa konfiguracije

Dalje ćemo definirati našu konfiguraciju Spring Security u klasi SecurityConfig, produžujući WebSecurityConfigurerAdapter:

@EnableWebSecurity javna klasa SecurityConfig proširuje WebSecurityConfigurerAdapter {@Override zaštićena void konfiguracija (HttpSecurity http) baca izuzetak {http .authorizeRequests (autorizacija -> autorizacija .antMatchers ("/ indeks", "/ prijava"). DozvolaAll (). home "," / logout "). authenticated () .antMatchers (" / admin / ** "). hasRole (" ADMIN ")) .formLogin (formLogin -> formLogin .loginPage (" / login ") .failureUrl (" /Pogreška pri prijavi")); } @Override zaštićena void konfiguracija (AuthenticationManagerBuilder auth) baca iznimku {auth.inMemoryAuthentication () .withUser ("Jerry") .password (passwordEncoder (). Encode ("password")) .authorities ("READ", "WRITE") .roles ("ADMIN"). i () .withUser ("Tom") .password (passwordEncoder (). encode ("password")) .authorities ("READ") .roles ("USER"); } @Bean public PasswordEncoder passwordEncoder () {return new BCryptPasswordEncoder (); }} 

Kao što vidimo, izgradili smo AuthenticationManagerBuilder usprotivi se izjavi naših korisnika s njihovim ulogama i ovlaštenjima. Uz to, šifrirali smo lozinke pomoću a BCryptPasswordEncoder.

Spring Security nam također pruža svoje HttpSecurity objekt za daljnje konfiguracije. Za naš smo primjer dopustili:

  • svima da pristupe našim indeks i prijaviti se stranice
  • samo autentificirani korisnici za ulazak u Dom stranica i Odjavite se
  • samo korisnici s ADMIN ulogom za pristup admin stranice

Također smo definirali podršku za provjeru autentičnosti na temelju obrasca za slanje korisnika na prijaviti se krajnja točka. U slučaju da prijava ne uspije, naši će korisnici biti preusmjereni na /Pogreška pri prijavi.

5. Kontroleri i krajnje točke

Sada ćemo pogledati preslikavanja našeg web kontrolera za dvije aplikacije. Iako će koristiti iste krajnje točke, neke će se implementacije razlikovati.

5.1. Krajnje točke za prikaz prikaza

Za krajnje točke koje generiraju pogled, implementacije su iste:

@GetMapping ("/") javni indeks niza () {return "indeks"; } @GetMapping ("/ login") javni niz showLoginPage () {return "login"; } @GetMapping ("/ home") javni niz getMeHome (model modela) {addUserAttributes (model); Povratak kući"; }

Obje naše implementacije kontrolera, Shiro, kao i Spring Security, vraćaju indeks.ftl na korijenskoj krajnjoj točki, prijava.ftl na krajnjoj točki za prijavu i home.ftl na domaćoj krajnjoj točki.

Međutim, definicija metode addUserAttributes na /Dom krajnja točka razlikovat će se između dva kontrolera. Ova metoda introspektira trenutno prijavljene korisničke atribute.

Shiro pruža a SecurityUtils # getSubject za preuzimanje trenutne Predmet, i njegove uloge i dozvole:

private void addUserAttributes (model modela) {Subject currentUser = SecurityUtils.getSubject (); Dozvola za niz = ""; if (currentUser.hasRole ("ADMIN")) {model.addAttribute ("role", "ADMIN"); } else if (currentUser.hasRole ("USER")) {model.addAttribute ("role", "USER"); } if (currentUser.isPermitted ("READ")) {dozvola = dopuštenje + "PROČITAJ"; } if (currentUser.isPermitted ("WRITE")) {dozvola = dopuštenje + "WRITE"; } model.addAttribute ("korisničko ime", currentUser.getPrincipal ()); model.addAttribute ("dopuštenje", dopuštenje); }

S druge strane, Spring Security pruža Ovjera objekt iz svog SecurityContextHolderKontekst u ovu svrhu:

private void addUserAttributes (model modela) {Authentication auth = SecurityContextHolder.getContext (). getAuthentication (); if (auth! = null &&! auth.getClass (). jednako (AnonymousAuthenticationToken.class)) {Korisnik korisnik = (Korisnik) auth.getPrincipal (); model.addAttribute ("korisničko ime", user.getUsername ()); Tijela za prikupljanje = user.getAuthorities (); za (Odobreno ovlaštenje autoriteta: tijela) {if (organ.getAuthority (). sadrži ("KORISNIK")) {model.addAttribute ("uloga", "KORISNIK"); model.addAttribute ("dozvole", "PROČITAJ"); } inače ako (organ.getAuthority (). sadrži ("ADMINISTRATOR")) {model.addAttribute ("uloga", "ADMINISTRATOR"); model.addAttribute ("dozvole", "READ WRITE"); }}}}

5.2. Krajnja točka prijave za POST

U Shirou mapiramo vjerodajnice koje korisnik unosi u POJO:

javna klasa UserCredentials {korisničko ime String; privatna lozinka za niz; // geteri i postavljači}

Tada ćemo stvoriti UsernamePasswordToken za prijavu korisnika ili Predmet, u:

@PostMapping ("/ login") javni niz doLogin (zahtjev HttpServletRequest, vjerodajnice UserCredentials, RedirectAttributes attr) {Subject subject = SecurityUtils.getSubject (); if (! subject.isAuthenticated ()) {UsernamePasswordToken token = new UsernamePasswordToken (credentials.getUsername (), credentials.getPassword ()); probajte {subject.login (token); } catch (AuthenticationException ae) {logger.error (ae.getMessage ()); attr.addFlashAttribute ("pogreška", "Nevažeće vjerodajnice"); return "preusmjeravanje: / prijava"; }} return "preusmjeravanje: / home"; }

Na proljetnoj sigurnosti ovo je samo pitanje preusmjeravanja na početnu stranicu. Proces prijave Spring, kojim se bavi UsernamePasswordAuthenticationFilter, je za nas transparentno:

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

5.3. Krajnja točka samo za administratore

Pogledajmo sada scenarij u kojem moramo izvršiti pristup temeljen na ulogama. Recimo da imamo / admin krajnja točka, čiji pristup treba biti dopušten samo za ulogu ADMINISTRA.

Pogledajmo kako to učiniti u Shiro-u:

@GetMapping ("/ admin") javni niz adminOnly (ModelMap modelMap) {addUserAttributes (modelMap); Predmet currentUser = SecurityUtils.getSubject (); if (currentUser.hasRole ("ADMIN")) {modelMap.addAttribute ("adminContent", "ovo može vidjeti samo admin"); } Povratak kući"; }

Ovdje smo izdvojili trenutno prijavljenog korisnika, provjerili ima li ulogu ADMINISTRA i dodali sadržaj u skladu s tim.

U Proljetnoj sigurnosti nema potrebe za programskom provjerom uloge, već smo definirali tko može doći do ove krajnje točke u našem SecurityConfig. Dakle, sada je samo pitanje dodavanja poslovne logike:

@GetMapping ("/ admin") javni niz adminOnly (zahtjev HttpServletRequest, model modela) {addUserAttributes (model); model.addAttribute ("adminContent", "samo admin može to vidjeti"); Povratak kući"; }

5.4. Krajnja točka odjave

Konačno, provedimo krajnju točku odjave.

U Shiru ćemo jednostavno nazvati Predmet # odjava:

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

Za proljeće nismo definirali mapiranje za odjavu. U ovom slučaju pokreće se zadani mehanizam odjave koji se automatski primjenjuje otkako smo produžili WebSecurityConfigurerAdapter u našoj konfiguraciji.

6. Apache Shiro vs Spring Security

Sad kad smo pogledali razlike u implementaciji, pogledajmo još nekoliko aspekata.

U pogledu podrške zajednice, Spring Framework općenito ima veliku zajednicu programera, aktivno uključen u njegov razvoj i uporabu. Budući da je Spring Security dio kišobrana, on mora uživati ​​iste prednosti. Shiro, iako popularan, nema tako veliku podršku.

Što se tiče dokumentacije, Proljeće je opet pobjednik.

Međutim, postoji malo krivulje učenja povezane s Spring Security. S druge strane, Shiro je lako razumjeti. Za stolne programe, konfiguracija putem shiro.ini je sve lakše.

Ali opet, kao što smo vidjeli u našem primjeru isječaka, Spring Security sjajno radi u održavanju poslovne logike i sigurnostiodvojiti i zaista nudi sigurnost kao sveobuhvatnu brigu.

7. Zaključak

U ovom vodiču, usporedili smo Apache Shiro s Spring Security.

Upravo smo prokrčili površinu onoga što ti okviri nude i puno toga treba istražiti. Postoji podosta alternativa poput JAAS i OACC. Ipak, čini se da sa svojim prednostima Spring Security u ovom trenutku pobjeđuje.

Kao i uvijek, izvorni kod dostupan je na GitHub-u.